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