Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
4.35% |
5 / 115 |
|
16.67% |
1 / 6 |
CRAP | |
0.00% |
0 / 1 |
LocalRenameUserJob | |
4.35% |
5 / 115 |
|
16.67% |
1 / 6 |
485.96 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
doRun | |
0.00% |
0 / 49 |
|
0.00% |
0 / 1 |
90 | |||
promoteToGlobal | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
movePages | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
12 | |||
done | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
escapeReplacement | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\CentralAuth\GlobalRename\LocalRenameJob; |
4 | |
5 | use LogicException; |
6 | use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameRequest; |
7 | use MediaWiki\Extension\CentralAuth\User\CentralAuthUser; |
8 | use MediaWiki\MediaWikiServices; |
9 | use MediaWiki\RenameUser\RenameuserSQL; |
10 | use MediaWiki\Title\Title; |
11 | use MediaWiki\User\User; |
12 | use MediaWiki\WikiMap\WikiMap; |
13 | use Wikimedia\Rdbms\IExpression; |
14 | use Wikimedia\Rdbms\LikeValue; |
15 | |
16 | /** |
17 | * Job class to rename a user locally |
18 | * This is intended to be run on each wiki individually |
19 | */ |
20 | class LocalRenameUserJob extends LocalRenameJob { |
21 | |
22 | /** |
23 | * @param Title $title |
24 | * @param array $params An associative array of options: |
25 | * from - old username |
26 | * to - new username |
27 | * force - try to do the rename even if the old username is invalid |
28 | * renamer - whom the renaming should be attributed in logs |
29 | * reason - reason to use in the rename log |
30 | * movepages - move user / user talk pages and their subpages |
31 | * suppressredirects - when moving pages, suppress redirects |
32 | * reattach - after rename, attach the local account. When used, should be set to |
33 | * [ wiki ID => [ 'attachedMethod' => method, 'attachedTimestamp' => timestamp ]. |
34 | * See CentralAuthUser::queryAttached. (default: false) |
35 | * promotetoglobal - globalize the new user account (default: false) |
36 | * session - session data from RequestContext::exportSession, for checkuser data |
37 | * ignorestatus - ignore update status, run the job even if it seems like another job |
38 | * is already working on it |
39 | */ |
40 | public function __construct( $title, $params ) { |
41 | $this->command = 'LocalRenameUserJob'; |
42 | |
43 | // For back-compat |
44 | if ( !isset( $params['promotetoglobal'] ) ) { |
45 | $params['promotetoglobal'] = false; |
46 | } |
47 | if ( !isset( $params['reason'] ) ) { |
48 | $params['reason'] = ''; |
49 | } |
50 | if ( !isset( $params['reattach'] ) ) { |
51 | $params['reattach'] = false; |
52 | } |
53 | if ( !isset( $params['type'] ) ) { |
54 | $params['type'] = GlobalRenameRequest::RENAME; |
55 | } |
56 | |
57 | parent::__construct( $title, $params ); |
58 | } |
59 | |
60 | /** @inheritDoc */ |
61 | public function doRun( $fnameTrxOwner ) { |
62 | $from = $this->params['from']; |
63 | $to = $this->params['to']; |
64 | |
65 | $this->updateStatus( 'inprogress' ); |
66 | $services = MediaWikiServices::getInstance(); |
67 | // Make the status update visible to all other transactions immediately |
68 | $factory = $services->getDBLoadBalancerFactory(); |
69 | $factory->commitPrimaryChanges( $fnameTrxOwner ); |
70 | |
71 | if ( isset( $this->params['force'] ) && $this->params['force'] ) { |
72 | // If we're dealing with an invalid username, load the data ourselves to avoid |
73 | // any normalization at all done by User or Title. |
74 | $userQuery = User::getQueryInfo(); |
75 | $row = $services->getConnectionProvider()->getPrimaryDatabase()->newSelectQueryBuilder() |
76 | ->tables( $userQuery['tables'] ) |
77 | ->select( $userQuery['fields'] ) |
78 | ->where( [ 'user_name' => $from ] ) |
79 | ->joinConds( $userQuery['joins'] ) |
80 | ->caller( __METHOD__ ) |
81 | ->fetchRow(); |
82 | $oldUser = User::newFromRow( $row ); |
83 | } else { |
84 | $oldUser = User::newFromName( $from ); |
85 | } |
86 | |
87 | $rename = new RenameuserSQL( |
88 | $from, |
89 | $to, |
90 | $oldUser->getId(), |
91 | $this->getRenameUser(), |
92 | [ |
93 | 'checkIfUserExists' => false, |
94 | 'debugPrefix' => 'GlobalRename', |
95 | 'reason' => $this->params['reason'], |
96 | ] |
97 | ); |
98 | if ( !$rename->rename() ) { |
99 | // This should never happen! |
100 | // If it does happen, the user will be locked out of their account |
101 | // until a sysadmin intervenes... |
102 | throw new LogicException( 'RenameuserSQL::rename returned false.' ); |
103 | } |
104 | |
105 | if ( $this->params['type'] === GlobalRenameRequest::VANISH ) { |
106 | // $renamedUser should always be truthy, otherwise the exception |
107 | // above would be thrown |
108 | $renamedUser = $services->getUserFactory()->newFromName( $to ); |
109 | if ( $renamedUser ) { |
110 | User::newSystemUser( $renamedUser->getName(), [ 'steal' => true ] ); |
111 | } |
112 | } |
113 | |
114 | if ( $this->params['reattach'] ) { |
115 | $caUser = CentralAuthUser::getInstanceByName( $this->params['to'] ); |
116 | $wikiId = WikiMap::getCurrentWikiId(); |
117 | $details = $this->params['reattach'][$wikiId]; |
118 | $caUser->attach( |
119 | $wikiId, |
120 | $details['attachedMethod'], |
121 | false, |
122 | $details['attachedTimestamp'] |
123 | ); |
124 | } |
125 | |
126 | if ( $this->params['movepages'] ) { |
127 | $this->movePages( $oldUser ); |
128 | } |
129 | |
130 | if ( $this->params['promotetoglobal'] ) { |
131 | $this->promoteToGlobal(); |
132 | } |
133 | |
134 | $this->done(); |
135 | } |
136 | |
137 | private function promoteToGlobal() { |
138 | $newName = $this->params['to']; |
139 | $caUser = CentralAuthUser::getPrimaryInstanceByName( $newName ); |
140 | $status = $caUser->promoteToGlobal( WikiMap::getCurrentWikiId() ); |
141 | if ( !$status->isOK() ) { |
142 | if ( $status->hasMessage( 'promote-not-on-wiki' ) ) { |
143 | // Eh, what? |
144 | throw new LogicException( "Tried to promote '$newName' to a global account except it " . |
145 | "doesn't exist locally" ); |
146 | } elseif ( $status->hasMessage( 'promote-already-exists' ) ) { |
147 | // Even more wtf. |
148 | throw new LogicException( "Tried to prommote '$newName' to a global account except it " . |
149 | "already exists" ); |
150 | } |
151 | } |
152 | |
153 | $caUser->quickInvalidateCache(); |
154 | } |
155 | |
156 | /** |
157 | * Queue up jobs to move pages |
158 | * @param User $oldUser |
159 | */ |
160 | public function movePages( User $oldUser ) { |
161 | $from = $this->params['from']; |
162 | $to = $this->params['to']; |
163 | |
164 | $fromDBkey = $oldUser->getUserPage()->getDBkey(); |
165 | $toDBkey = Title::makeTitleSafe( NS_USER, $to )->getDBkey(); |
166 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
167 | |
168 | $rows = $dbr->newSelectQueryBuilder() |
169 | ->select( [ 'page_namespace', 'page_title' ] ) |
170 | ->from( 'page' ) |
171 | ->where( [ |
172 | 'page_namespace' => [ NS_USER, NS_USER_TALK ], |
173 | $dbr->expr( 'page_title', IExpression::LIKE, new LikeValue( $fromDBkey . '/', $dbr->anyString() ) ) |
174 | ->or( 'page_title', '=', $fromDBkey ) |
175 | ] ) |
176 | ->caller( __METHOD__ ) |
177 | ->fetchResultSet(); |
178 | |
179 | $jobParams = [ |
180 | 'to' => $to, |
181 | 'from' => $from, |
182 | 'renamer' => $this->getRenameUser()->getName(), |
183 | 'suppressredirects' => $this->params['suppressredirects'], |
184 | ]; |
185 | if ( isset( $this->params['session'] ) ) { |
186 | $jobParams['session'] = $this->params['session']; |
187 | } |
188 | $jobs = []; |
189 | |
190 | $toReplace = static::escapeReplacement( $toDBkey ); |
191 | foreach ( $rows as $row ) { |
192 | $oldPage = Title::newFromRow( $row ); |
193 | $newPage = Title::makeTitleSafe( $row->page_namespace, |
194 | preg_replace( '!^[^/]+!', $toReplace, $row->page_title ) ); |
195 | $jobs[] = new LocalPageMoveJob( |
196 | Title::newFromText( 'LocalRenameUserJob' ), |
197 | $jobParams + [ |
198 | 'old' => [ $oldPage->getNamespace(), $oldPage->getDBkey() ], |
199 | 'new' => [ $newPage->getNamespace(), $newPage->getDBkey() ], |
200 | ] |
201 | ); |
202 | } |
203 | |
204 | MediaWikiServices::getInstance()->getJobQueueGroup()->push( $jobs ); |
205 | } |
206 | |
207 | protected function done() { |
208 | parent::done(); |
209 | $caOld = CentralAuthUser::getInstanceByName( $this->params['from'] ); |
210 | $caOld->quickInvalidateCache(); |
211 | } |
212 | |
213 | /** |
214 | * Escape a string to be used as a replacement by preg_replace so that |
215 | * anything in it that looks like a backreference is treated as a literal |
216 | * substitution. |
217 | * |
218 | * @param string $str String to escape |
219 | * @return string |
220 | */ |
221 | protected static function escapeReplacement( $str ) { |
222 | // T188171: escape any occurrence of '$n' or '\n' in the replacement |
223 | // string passed to preg_replace so that it will not be treated as |
224 | // a backreference. |
225 | return preg_replace( |
226 | // find $n, ${n}, and \n |
227 | '/[$\\\\]{?\d+}?/', |
228 | // prepend with a literal '\\' |
229 | '\\\\${0}', |
230 | $str |
231 | ); |
232 | } |
233 | } |