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