Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 67
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 / 64
0.00% covered (danger)
0.00%
0 / 7
210
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 10
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 / 26
0.00% covered (danger)
0.00%
0 / 1
42
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(
40            'expiry',
41            'Expire accounts older than this number of days. Use 0 to expire all temporary accounts',
42            false,
43            true
44        );
45        $this->addOption( 'verbose', 'Verbose logging output' );
46    }
47
48    /**
49     * Construct services the script needs to use
50     *
51     * @stable to override
52     */
53    protected function initServices(): void {
54        $services = $this->getServiceContainer();
55
56        $this->userIdentityLookup = $services->getUserIdentityLookup();
57        $this->userFactory = $services->getUserFactory();
58        $this->authManager = $services->getAuthManager();
59        $this->tempUserConfig = $services->getTempUserConfig();
60        $this->userIdentityUtils = $services->getUserIdentityUtils();
61    }
62
63    /**
64     * If --verbose is passed, log to output
65     *
66     * @param string $log
67     * @return void
68     */
69    protected function verboseLog( string $log ) {
70        if ( $this->hasOption( 'verbose' ) ) {
71            $this->output( $log );
72        }
73    }
74
75    /**
76     * Return a SelectQueryBuilder that returns temp accounts to invalidate
77     *
78     * This method should return temporary accounts that registered before $registeredBeforeUnix.
79     * To avoid returning an ever-growing set of accounts, the method should skip users that were
80     * supposedly invalidated by a previous script run (script runs each $frequencyDays days).
81     *
82     * If you override this method, you probably also want to override
83     * queryBuilderToUserIdentities().
84     *
85     * @stable to override
86     * @param int $registeredBeforeUnix Cutoff Unix timestamp
87     * @param int $frequencyDays Script runs each $frequencyDays days
88     * @return SelectQueryBuilder
89     */
90    protected function getTempAccountsToExpireQueryBuilder(
91        int $registeredBeforeUnix,
92        int $frequencyDays
93    ): SelectQueryBuilder {
94        return $this->userIdentityLookup->newSelectQueryBuilder()
95            ->temp()
96            ->whereRegisteredTimestamp( wfTimestamp(
97                TS_MW,
98                $registeredBeforeUnix
99            ), true )
100            ->whereRegisteredTimestamp( wfTimestamp(
101                TS_MW,
102                $registeredBeforeUnix - ExpirationAwareness::TTL_DAY * $frequencyDays
103            ), false );
104    }
105
106    /**
107     * Convert a SelectQueryBuilder into a list of user identities
108     *
109     * Default implementation expects $queryBuilder is an instance of UserSelectQueryBuilder. If
110     * you override getTempAccountsToExpireQueryBuilder() to work with a different query builder,
111     * this method should be overriden to properly convert the query builder into user identities.
112     *
113     * @throws LogicException if $queryBuilder is not UserSelectQueryBuilder
114     * @stable to override
115     * @param SelectQueryBuilder $queryBuilder
116     * @return Iterator<UserIdentity>
117     */
118    protected function queryBuilderToUserIdentities( SelectQueryBuilder $queryBuilder ): Iterator {
119        if ( $queryBuilder instanceof UserSelectQueryBuilder ) {
120            return $queryBuilder->fetchUserIdentities();
121        }
122
123        throw new LogicException(
124            '$queryBuilder is not UserSelectQueryBuilder. Did you forget to override ' .
125            __METHOD__ . '?'
126        );
127    }
128
129    /**
130     * Expire a temporary account
131     *
132     * Default implementation calls AuthManager::revokeAccessForUser and
133     * SessionManager::invalidateSessionsForUser.
134     *
135     * @stable to override
136     * @param UserIdentity $tempAccountUserIdentity
137     */
138    protected function expireTemporaryAccount( UserIdentity $tempAccountUserIdentity ): void {
139        $this->authManager->revokeAccessForUser( $tempAccountUserIdentity->getName() );
140        SessionManager::singleton()->invalidateSessionsForUser(
141            $this->userFactory->newFromUserIdentity( $tempAccountUserIdentity )
142        );
143    }
144
145    /**
146     * @inheritDoc
147     */
148    public function execute() {
149        $this->initServices();
150
151        if ( !$this->tempUserConfig->isKnown() ) {
152            $this->output( 'Temporary accounts are disabled' . PHP_EOL );
153            return;
154        }
155
156        $frequencyDays = (int)$this->getOption( 'frequency' );
157        if ( $this->getOption( 'expiry' ) !== null ) {
158            $expiryAfterDays = (int)$this->getOption( 'expiry' );
159        } else {
160            $expiryAfterDays = $this->tempUserConfig->getExpireAfterDays();
161        }
162        if ( $expiryAfterDays === null ) {
163            $this->output( 'Temporary account expiry is not enabled' . PHP_EOL );
164            return;
165        }
166        $registeredBeforeUnix = (int)wfTimestamp( TS_UNIX ) - ExpirationAwareness::TTL_DAY * $expiryAfterDays;
167
168        $tempAccounts = $this->queryBuilderToUserIdentities( $this->getTempAccountsToExpireQueryBuilder(
169            $registeredBeforeUnix,
170            $frequencyDays
171        )->caller( __METHOD__ ) );
172
173        $revokedUsers = 0;
174        foreach ( $tempAccounts as $tempAccountUserIdentity ) {
175            if ( !$this->userIdentityUtils->isTemp( $tempAccountUserIdentity ) ) {
176                // Not a temporary account, skip it.
177                continue;
178            }
179
180            $this->expireTemporaryAccount( $tempAccountUserIdentity );
181
182            $this->verboseLog(
183                'Revoking access for ' . $tempAccountUserIdentity->getName() . PHP_EOL
184            );
185            $revokedUsers++;
186        }
187
188        $this->output( "Revoked access for $revokedUsers temporary users." . PHP_EOL );
189    }
190}
191
192$maintClass = ExpireTemporaryAccounts::class;
193require_once RUN_MAINTENANCE_IF_MAIN;