aboutsummaryrefslogtreecommitdiffstats
path: root/maintenance/expireTemporaryAccounts.php
blob: 1a40e7006c26cabe60a589336b2fa3f927d9dc2e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
<?php

use MediaWiki\Auth\AuthManager;
use MediaWiki\Session\SessionManager;
use MediaWiki\User\TempUser\TempUserConfig;
use MediaWiki\User\UserFactory;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserIdentityLookup;
use MediaWiki\User\UserIdentityUtils;
use MediaWiki\User\UserSelectQueryBuilder;
use Wikimedia\LightweightObjectStore\ExpirationAwareness;
use Wikimedia\Rdbms\SelectQueryBuilder;

require_once __DIR__ . '/Maintenance.php';

/**
 * Expire temporary accounts that are registered for longer than `expiryAfterDays` days
 * (defined in $wgAutoCreateTempUser) by forcefully logging them out.
 *
 * Extensions can extend this class to provide their own logic of determining a list
 * of temporary accounts to expire.
 *
 * @stable to extend
 * @since 1.42
 */
class ExpireTemporaryAccounts extends Maintenance {

	protected UserIdentityLookup $userIdentityLookup;
	protected UserFactory $userFactory;
	protected AuthManager $authManager;
	protected TempUserConfig $tempUserConfig;
	protected UserIdentityUtils $userIdentityUtils;

	public function __construct() {
		parent::__construct();

		$this->addDescription( 'Expire temporary accounts that exist for more than N days' );
		$this->addOption( 'frequency', 'How frequently the script runs [days]', true, true );
		$this->addOption(
			'expiry',
			'Expire accounts older than this number of days. Use 0 to expire all temporary accounts',
			false,
			true
		);
		$this->addOption( 'verbose', 'Verbose logging output' );
	}

	/**
	 * Construct services the script needs to use
	 *
	 * @stable to override
	 */
	protected function initServices(): void {
		$services = $this->getServiceContainer();

		$this->userIdentityLookup = $services->getUserIdentityLookup();
		$this->userFactory = $services->getUserFactory();
		$this->authManager = $services->getAuthManager();
		$this->tempUserConfig = $services->getTempUserConfig();
		$this->userIdentityUtils = $services->getUserIdentityUtils();
	}

	/**
	 * If --verbose is passed, log to output
	 *
	 * @param string $log
	 * @return void
	 */
	protected function verboseLog( string $log ) {
		if ( $this->hasOption( 'verbose' ) ) {
			$this->output( $log );
		}
	}

	/**
	 * Return a SelectQueryBuilder that returns temp accounts to invalidate
	 *
	 * This method should return temporary accounts that registered before $registeredBeforeUnix.
	 * To avoid returning an ever-growing set of accounts, the method should skip users that were
	 * supposedly invalidated by a previous script run (script runs each $frequencyDays days).
	 *
	 * If you override this method, you probably also want to override
	 * queryBuilderToUserIdentities().
	 *
	 * @stable to override
	 * @param int $registeredBeforeUnix Cutoff Unix timestamp
	 * @param int $frequencyDays Script runs each $frequencyDays days
	 * @return SelectQueryBuilder
	 */
	protected function getTempAccountsToExpireQueryBuilder(
		int $registeredBeforeUnix,
		int $frequencyDays
	): SelectQueryBuilder {
		return $this->userIdentityLookup->newSelectQueryBuilder()
			->temp()
			->whereRegisteredTimestamp( wfTimestamp(
				TS_MW,
				$registeredBeforeUnix
			), true )
			->whereRegisteredTimestamp( wfTimestamp(
				TS_MW,
				$registeredBeforeUnix - ExpirationAwareness::TTL_DAY * $frequencyDays
			), false );
	}

	/**
	 * Convert a SelectQueryBuilder into a list of user identities
	 *
	 * Default implementation expects $queryBuilder is an instance of UserSelectQueryBuilder. If
	 * you override getTempAccountsToExpireQueryBuilder() to work with a different query builder,
	 * this method should be overriden to properly convert the query builder into user identities.
	 *
	 * @throws LogicException if $queryBuilder is not UserSelectQueryBuilder
	 * @stable to override
	 * @param SelectQueryBuilder $queryBuilder
	 * @return Iterator<UserIdentity>
	 */
	protected function queryBuilderToUserIdentities( SelectQueryBuilder $queryBuilder ): Iterator {
		if ( $queryBuilder instanceof UserSelectQueryBuilder ) {
			return $queryBuilder->fetchUserIdentities();
		}

		throw new LogicException(
			'$queryBuilder is not UserSelectQueryBuilder. Did you forget to override ' .
			__METHOD__ . '?'
		);
	}

	/**
	 * Expire a temporary account
	 *
	 * Default implementation calls AuthManager::revokeAccessForUser and
	 * SessionManager::invalidateSessionsForUser.
	 *
	 * @stable to override
	 * @param UserIdentity $tempAccountUserIdentity
	 */
	protected function expireTemporaryAccount( UserIdentity $tempAccountUserIdentity ): void {
		$this->authManager->revokeAccessForUser( $tempAccountUserIdentity->getName() );
		SessionManager::singleton()->invalidateSessionsForUser(
			$this->userFactory->newFromUserIdentity( $tempAccountUserIdentity )
		);
	}

	/**
	 * @inheritDoc
	 */
	public function execute() {
		$this->initServices();

		if ( !$this->tempUserConfig->isKnown() ) {
			$this->output( 'Temporary accounts are disabled' . PHP_EOL );
			return;
		}

		$frequencyDays = (int)$this->getOption( 'frequency' );
		if ( $this->getOption( 'expiry' ) !== null ) {
			$expiryAfterDays = (int)$this->getOption( 'expiry' );
		} else {
			$expiryAfterDays = $this->tempUserConfig->getExpireAfterDays();
		}
		if ( $expiryAfterDays === null ) {
			$this->output( 'Temporary account expiry is not enabled' . PHP_EOL );
			return;
		}
		$registeredBeforeUnix = (int)wfTimestamp( TS_UNIX ) - ExpirationAwareness::TTL_DAY * $expiryAfterDays;

		$tempAccounts = $this->queryBuilderToUserIdentities( $this->getTempAccountsToExpireQueryBuilder(
			$registeredBeforeUnix,
			$frequencyDays
		)->caller( __METHOD__ ) );

		$revokedUsers = 0;
		foreach ( $tempAccounts as $tempAccountUserIdentity ) {
			if ( !$this->userIdentityUtils->isTemp( $tempAccountUserIdentity ) ) {
				// Not a temporary account, skip it.
				continue;
			}

			$this->expireTemporaryAccount( $tempAccountUserIdentity );

			$this->verboseLog(
				'Revoking access for ' . $tempAccountUserIdentity->getName() . PHP_EOL
			);
			$revokedUsers++;
		}

		$this->output( "Revoked access for $revokedUsers temporary users." . PHP_EOL );
	}
}

$maintClass = ExpireTemporaryAccounts::class;
require_once RUN_MAINTENANCE_IF_MAIN;