Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 70 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
LocalRenameJob | |
0.00% |
0 / 70 |
|
0.00% |
0 / 7 |
506 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
run | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
42 | |||
doRun | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
setRenameUserStatus | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getRenameUser | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
42 | |||
done | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
updateStatus | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
scheduleNextWiki | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
42 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\CentralAuth\GlobalRename\LocalRenameJob; |
4 | |
5 | use Exception; |
6 | use IDBAccessObject; |
7 | use Job; |
8 | use MediaWiki\Extension\CentralAuth\CentralAuthServices; |
9 | use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameUserStatus; |
10 | use MediaWiki\Extension\CentralAuth\User\CentralAuthUser; |
11 | use MediaWiki\Logger\LoggerFactory; |
12 | use MediaWiki\MediaWikiServices; |
13 | use MediaWiki\Title\Title; |
14 | use MediaWiki\User\User; |
15 | use MediaWiki\WikiMap\WikiMap; |
16 | use RequestContext; |
17 | use Wikimedia\ScopedCallback; |
18 | |
19 | /** |
20 | * Base class for jobs that change a user's |
21 | * name. Intended to be run on local wikis |
22 | * indvidually. |
23 | * |
24 | * Parameters: |
25 | * - from: current username |
26 | * - to: new username to rename to |
27 | * - renamer: username of the performer |
28 | * - ignorestatus: when true, the rename will be done even if another job is supposed to be |
29 | * already doing it. This should only be used for stuck renames. |
30 | * - session: array of session data from RequestContext::exportSession() |
31 | */ |
32 | abstract class LocalRenameJob extends Job { |
33 | /** |
34 | * @var GlobalRenameUserStatus |
35 | */ |
36 | private $renameuserStatus; |
37 | |
38 | /** |
39 | * @param Title $title |
40 | * @param array $params |
41 | */ |
42 | public function __construct( Title $title, $params ) { |
43 | parent::__construct( $this->command, $title, $params ); |
44 | } |
45 | |
46 | /** |
47 | * @throws Exception |
48 | */ |
49 | public function run(): bool { |
50 | $this->setRenameUserStatus( |
51 | CentralAuthServices::getGlobalRenameFactory() |
52 | ->newGlobalRenameUserStatus( $this->params['to'] ) |
53 | ); |
54 | |
55 | // Bail if it's already done or in progress. Use a locking read to block until the |
56 | // transaction adding this job is done, so we can see its changes. This is similar to |
57 | // the trick that the RenameUser extension does. |
58 | $status = $this->renameuserStatus->getStatus( IDBAccessObject::READ_LOCKING ); |
59 | // Clear any REPEATABLE-READ snapshot in case the READ_LOCKING blocked above. We want |
60 | // regular non-locking SELECTs to see all the changes from that transaction we waited on. |
61 | // Making a new transaction also reduces deadlocks from the locking read. |
62 | // T145596 |
63 | $fnameTrxOwner = get_class( $this ) . '::' . __FUNCTION__; |
64 | $factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); |
65 | $factory->commitPrimaryChanges( $fnameTrxOwner ); |
66 | |
67 | if ( empty( $this->params['ignorestatus'] ) ) { |
68 | if ( $status !== 'queued' && $status !== 'failed' ) { |
69 | $logger = LoggerFactory::getInstance( 'CentralAuth' ); |
70 | $logger->info( 'Skipping duplicate rename from {oldName} to {newName}', [ |
71 | 'oldName' => $this->params['from'], |
72 | 'newName' => $this->params['to'], |
73 | 'component' => 'GlobalRename', |
74 | 'status' => $status, |
75 | ] ); |
76 | return true; |
77 | } |
78 | } |
79 | |
80 | if ( isset( $this->params['session'] ) ) { |
81 | // Don't carry over users or sessions because it's going to be wrong |
82 | // across wikis |
83 | $this->params['session']['userId'] = 0; |
84 | $this->params['session']['sessionId'] = ''; |
85 | $callback = RequestContext::importScopedSession( $this->params['session'] ); |
86 | $this->addTeardownCallback( static function () use ( &$callback ) { |
87 | ScopedCallback::consume( $callback ); |
88 | } ); |
89 | } |
90 | try { |
91 | $this->doRun( $fnameTrxOwner ); |
92 | $this->addTeardownCallback( [ $this, 'scheduleNextWiki' ] ); |
93 | } catch ( Exception $e ) { |
94 | // This will lock the user out of their account until a sysadmin intervenes |
95 | $factory->rollbackPrimaryChanges( $fnameTrxOwner ); |
96 | $this->updateStatus( 'failed' ); |
97 | $factory->commitPrimaryChanges( $fnameTrxOwner ); |
98 | // Record job failure in CentralAuth channel (T217211) |
99 | $logger = LoggerFactory::getInstance( 'CentralAuth' ); |
100 | $logger->error( 'Failed to rename {oldName} to {newName} ({error})', [ |
101 | 'oldName' => $this->params['from'], |
102 | 'newName' => $this->params['to'], |
103 | 'component' => 'GlobalRename', |
104 | 'error' => $e->getMessage() |
105 | ] ); |
106 | throw $e; |
107 | } |
108 | |
109 | return true; |
110 | } |
111 | |
112 | /** |
113 | * Actually do the work for the job class |
114 | * @param string $fnameTrxOwner Caller name |
115 | */ |
116 | abstract protected function doRun( $fnameTrxOwner ); |
117 | |
118 | /** |
119 | * @param GlobalRenameUserStatus $status |
120 | */ |
121 | protected function setRenameUserStatus( GlobalRenameUserStatus $status ) { |
122 | $this->renameuserStatus = $status; |
123 | } |
124 | |
125 | /** |
126 | * Get the user object for the user who is doing the renaming |
127 | * "Auto-create" if it doesn't exist yet. |
128 | * @return User |
129 | */ |
130 | protected function getRenameUser() { |
131 | $user = User::newFromName( $this->params['renamer'] ); |
132 | $userNameUtils = MediaWikiServices::getInstance()->getUserNameUtils(); |
133 | // If the username is a reserved name, don't worry about the account |
134 | // existing, just use it. |
135 | if ( !$userNameUtils->isUsable( $user->getName() ) ) { |
136 | return $user; |
137 | } |
138 | |
139 | $caUser = CentralAuthUser::getPrimaryInstance( $user ); |
140 | // Race condition where the renamer isn't attached here, but |
141 | // someone creates an account in the meantime and then bad |
142 | // stuff could happen... |
143 | // For the meantime, just use a system account |
144 | if ( !$caUser->attachedOn( WikiMap::getCurrentWikiId() ) && $user->getId() !== 0 ) { |
145 | return User::newSystemUser( 'Global rename script', [ 'steal' => true ] ); |
146 | } |
147 | |
148 | if ( $user->getId() == 0 ) { |
149 | // No local user, lets "auto-create" one |
150 | if ( CentralAuthServices::getUtilityService()->autoCreateUser( $user )->isGood() ) { |
151 | // So the internal cache is reloaded |
152 | return User::newFromName( $user->getName() ); |
153 | } |
154 | |
155 | // Auto-creation didn't work, fallback on the system account. |
156 | return User::newSystemUser( 'Global rename script', [ 'steal' => true ] ); |
157 | } |
158 | |
159 | // Account is attached and exists, just use it :) |
160 | return $user; |
161 | } |
162 | |
163 | protected function done() { |
164 | $this->renameuserStatus->done( WikiMap::getCurrentWikiId() ); |
165 | |
166 | $caNew = CentralAuthUser::getInstanceByName( $this->params['to'] ); |
167 | $caNew->quickInvalidateCache(); |
168 | } |
169 | |
170 | /** |
171 | * @param string $status |
172 | */ |
173 | protected function updateStatus( $status ) { |
174 | $this->renameuserStatus->updateStatus( WikiMap::getCurrentWikiId(), $status ); |
175 | } |
176 | |
177 | /** |
178 | * @param bool $status See Job::addTeardownCallback |
179 | */ |
180 | protected function scheduleNextWiki( $status ) { |
181 | if ( $status === false ) { |
182 | // This will lock the user out of their account until a sysadmin intervenes. |
183 | $this->updateStatus( 'failed' ); |
184 | // Bail out just in case the error would affect all renames and continuing would |
185 | // just put all wikis of the user in failure state. Running the rename for this |
186 | // wiki again (e.g. with fixStuckGlobalRename.php) will resume the job chain. |
187 | return; |
188 | } |
189 | |
190 | $job = new static( $this->getTitle(), $this->getParams() ); |
191 | $nextWiki = null; |
192 | $statuses = $this->renameuserStatus->getStatuses( IDBAccessObject::READ_LATEST ); |
193 | foreach ( $statuses as $wiki => $status ) { |
194 | if ( $status === 'queued' && $wiki !== WikiMap::getCurrentWikiId() ) { |
195 | $nextWiki = $wiki; |
196 | break; |
197 | } |
198 | } |
199 | if ( $nextWiki ) { |
200 | MediaWikiServices::getInstance()->getJobQueueGroupFactory()->makeJobQueueGroup( $nextWiki )->push( $job ); |
201 | } |
202 | } |
203 | } |