Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
75.00% |
54 / 72 |
|
42.86% |
3 / 7 |
CRAP | |
0.00% |
0 / 1 |
| ExpireTemporaryAccounts | |
75.00% |
54 / 72 |
|
42.86% |
3 / 7 |
17.06 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
1 | |||
| initServices | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
| verboseLog | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
| getTempAccountsToExpireQueryBuilder | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
| queryBuilderToUserIdentities | |
33.33% |
2 / 6 |
|
0.00% |
0 / 1 |
3.19 | |||
| expireTemporaryAccount | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
| execute | |
69.23% |
18 / 26 |
|
0.00% |
0 / 1 |
7.05 | |||
| 1 | <?php |
| 2 | |
| 3 | use MediaWiki\Auth\AuthManager; |
| 4 | use MediaWiki\Maintenance\Maintenance; |
| 5 | use MediaWiki\Session\SessionManager; |
| 6 | use MediaWiki\User\TempUser\TempUserConfig; |
| 7 | use MediaWiki\User\UserFactory; |
| 8 | use MediaWiki\User\UserIdentity; |
| 9 | use MediaWiki\User\UserIdentityLookup; |
| 10 | use MediaWiki\User\UserIdentityUtils; |
| 11 | use MediaWiki\User\UserSelectQueryBuilder; |
| 12 | use Wikimedia\Rdbms\SelectQueryBuilder; |
| 13 | use Wikimedia\Timestamp\ConvertibleTimestamp; |
| 14 | use Wikimedia\Timestamp\TimestampFormat as TS; |
| 15 | |
| 16 | // @codeCoverageIgnoreStart |
| 17 | require_once __DIR__ . '/Maintenance.php'; |
| 18 | // @codeCoverageIgnoreEnd |
| 19 | |
| 20 | /** |
| 21 | * Expire temporary accounts that are registered for longer than `expiryAfterDays` days |
| 22 | * (defined in $wgAutoCreateTempUser) by forcefully logging them out. |
| 23 | * |
| 24 | * Extensions can extend this class to provide their own logic of determining a list |
| 25 | * of temporary accounts to expire. |
| 26 | * |
| 27 | * @stable to extend |
| 28 | * @since 1.42 |
| 29 | */ |
| 30 | class ExpireTemporaryAccounts extends Maintenance { |
| 31 | |
| 32 | protected UserIdentityLookup $userIdentityLookup; |
| 33 | protected UserFactory $userFactory; |
| 34 | protected AuthManager $authManager; |
| 35 | protected TempUserConfig $tempUserConfig; |
| 36 | protected UserIdentityUtils $userIdentityUtils; |
| 37 | |
| 38 | public function __construct() { |
| 39 | parent::__construct(); |
| 40 | |
| 41 | $this->addDescription( 'Expire temporary accounts that exist for more than N days' ); |
| 42 | $this->addOption( |
| 43 | 'frequency', |
| 44 | 'How frequently the script runs [days]. When used with "expiry", determines the ' . |
| 45 | 'cutoff for registration of accounts to be expired. For example, if "expiry" is 90 ' . |
| 46 | 'days and "frequency" is 1 day, then the script will expire accounts that ' . |
| 47 | 'registered more than 90 days ago but not more than 90 + 1 days ago.', |
| 48 | true, |
| 49 | true |
| 50 | ); |
| 51 | $this->addOption( |
| 52 | 'expiry', |
| 53 | 'Expire accounts older than this number of days. Use 0 to expire all temporary accounts', |
| 54 | false, |
| 55 | true |
| 56 | ); |
| 57 | $this->addOption( 'verbose', 'Verbose logging output' ); |
| 58 | } |
| 59 | |
| 60 | /** |
| 61 | * Construct services the script needs to use |
| 62 | * |
| 63 | * @stable to override |
| 64 | */ |
| 65 | protected function initServices(): void { |
| 66 | $services = $this->getServiceContainer(); |
| 67 | |
| 68 | $this->userIdentityLookup = $services->getUserIdentityLookup(); |
| 69 | $this->userFactory = $services->getUserFactory(); |
| 70 | $this->authManager = $services->getAuthManager(); |
| 71 | $this->tempUserConfig = $services->getTempUserConfig(); |
| 72 | $this->userIdentityUtils = $services->getUserIdentityUtils(); |
| 73 | } |
| 74 | |
| 75 | /** |
| 76 | * If --verbose is passed, log to output |
| 77 | * |
| 78 | * @param string $log |
| 79 | * @return void |
| 80 | */ |
| 81 | protected function verboseLog( string $log ) { |
| 82 | if ( $this->hasOption( 'verbose' ) ) { |
| 83 | $this->output( $log ); |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | /** |
| 88 | * Return a SelectQueryBuilder that returns temp accounts to invalidate |
| 89 | * |
| 90 | * This method should return temporary accounts that registered before $registeredBeforeUnix. |
| 91 | * To avoid returning an ever-growing set of accounts, the method should skip users that were |
| 92 | * supposedly invalidated by a previous script run (script runs each $frequencyDays days). |
| 93 | * |
| 94 | * If you override this method, you probably also want to override |
| 95 | * queryBuilderToUserIdentities(). |
| 96 | * |
| 97 | * @stable to override |
| 98 | * @param int $registeredBeforeUnix Cutoff Unix timestamp |
| 99 | * @param int $frequencyDays Script runs each $frequencyDays days |
| 100 | * @return SelectQueryBuilder |
| 101 | */ |
| 102 | protected function getTempAccountsToExpireQueryBuilder( |
| 103 | int $registeredBeforeUnix, |
| 104 | int $frequencyDays |
| 105 | ): SelectQueryBuilder { |
| 106 | return $this->userIdentityLookup->newSelectQueryBuilder() |
| 107 | ->temp() |
| 108 | ->whereRegisteredTimestamp( wfTimestamp( |
| 109 | TS::MW, |
| 110 | $registeredBeforeUnix |
| 111 | ), true ) |
| 112 | ->whereRegisteredTimestamp( wfTimestamp( |
| 113 | TS::MW, |
| 114 | $registeredBeforeUnix - ( 86_400 * $frequencyDays ) |
| 115 | ), false ); |
| 116 | } |
| 117 | |
| 118 | /** |
| 119 | * Convert a SelectQueryBuilder into a list of user identities |
| 120 | * |
| 121 | * Default implementation expects $queryBuilder is an instance of UserSelectQueryBuilder. If |
| 122 | * you override getTempAccountsToExpireQueryBuilder() to work with a different query builder, |
| 123 | * this method should be overriden to properly convert the query builder into user identities. |
| 124 | * |
| 125 | * @throws LogicException if $queryBuilder is not UserSelectQueryBuilder |
| 126 | * @stable to override |
| 127 | * @param SelectQueryBuilder $queryBuilder |
| 128 | * @return Iterator<UserIdentity> |
| 129 | */ |
| 130 | protected function queryBuilderToUserIdentities( SelectQueryBuilder $queryBuilder ): Iterator { |
| 131 | if ( $queryBuilder instanceof UserSelectQueryBuilder ) { |
| 132 | return $queryBuilder->fetchUserIdentities(); |
| 133 | } |
| 134 | |
| 135 | throw new LogicException( |
| 136 | '$queryBuilder is not UserSelectQueryBuilder. Did you forget to override ' . |
| 137 | __METHOD__ . '?' |
| 138 | ); |
| 139 | } |
| 140 | |
| 141 | /** |
| 142 | * Expire a temporary account |
| 143 | * |
| 144 | * Default implementation calls AuthManager::revokeAccessForUser and |
| 145 | * SessionManager::invalidateSessionsForUser. |
| 146 | * |
| 147 | * @stable to override |
| 148 | * @param UserIdentity $tempAccountUserIdentity |
| 149 | */ |
| 150 | protected function expireTemporaryAccount( UserIdentity $tempAccountUserIdentity ): void { |
| 151 | $this->authManager->revokeAccessForUser( $tempAccountUserIdentity->getName() ); |
| 152 | SessionManager::singleton()->invalidateSessionsForUser( |
| 153 | $this->userFactory->newFromUserIdentity( $tempAccountUserIdentity ) |
| 154 | ); |
| 155 | } |
| 156 | |
| 157 | /** |
| 158 | * @inheritDoc |
| 159 | */ |
| 160 | public function execute() { |
| 161 | $this->initServices(); |
| 162 | |
| 163 | if ( !$this->tempUserConfig->isKnown() ) { |
| 164 | $this->output( 'Temporary accounts are disabled' . PHP_EOL ); |
| 165 | return; |
| 166 | } |
| 167 | |
| 168 | $frequencyDays = (int)$this->getOption( 'frequency' ); |
| 169 | if ( $this->getOption( 'expiry' ) !== null ) { |
| 170 | $expiryAfterDays = (int)$this->getOption( 'expiry' ); |
| 171 | } else { |
| 172 | $expiryAfterDays = $this->tempUserConfig->getExpireAfterDays(); |
| 173 | } |
| 174 | if ( $expiryAfterDays === null ) { |
| 175 | $this->output( 'Temporary account expiry is not enabled' . PHP_EOL ); |
| 176 | return; |
| 177 | } |
| 178 | $registeredBeforeUnix = (int)ConvertibleTimestamp::now( TS::UNIX ) - ( 86_400 * $expiryAfterDays ); |
| 179 | |
| 180 | $tempAccounts = $this->queryBuilderToUserIdentities( $this->getTempAccountsToExpireQueryBuilder( |
| 181 | $registeredBeforeUnix, |
| 182 | $frequencyDays |
| 183 | )->caller( __METHOD__ ) ); |
| 184 | |
| 185 | $revokedUsers = 0; |
| 186 | foreach ( $tempAccounts as $tempAccountUserIdentity ) { |
| 187 | if ( !$this->userIdentityUtils->isTemp( $tempAccountUserIdentity ) ) { |
| 188 | // Not a temporary account, skip it. |
| 189 | continue; |
| 190 | } |
| 191 | |
| 192 | $this->expireTemporaryAccount( $tempAccountUserIdentity ); |
| 193 | |
| 194 | $this->verboseLog( |
| 195 | 'Revoking access for ' . $tempAccountUserIdentity->getName() . PHP_EOL |
| 196 | ); |
| 197 | $revokedUsers++; |
| 198 | } |
| 199 | |
| 200 | $this->output( "Revoked access for $revokedUsers temporary users." . PHP_EOL ); |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | // @codeCoverageIgnoreStart |
| 205 | $maintClass = ExpireTemporaryAccounts::class; |
| 206 | require_once RUN_MAINTENANCE_IF_MAIN; |
| 207 | // @codeCoverageIgnoreEnd |