Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 459 |
|
0.00% |
0 / 23 |
CRAP | |
0.00% |
0 / 1 |
SpecialGlobalRenameQueue | |
0.00% |
0 / 459 |
|
0.00% |
0 / 23 |
5700 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
30 | |||
commonPreamble | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getAssociatedNavigationLinks | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getShortDescription | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
addSubtitleLinks | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
getCommonFormFieldsArray | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
2 | |||
outputFilterForm | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
handleOpenQueue | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
handleClosedQueue | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
2 | |||
handleProcessRequest | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
72 | |||
showUnkownRequest | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
doRedirectToOpenQueue | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
doViewRequest | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
42 | |||
doShowProcessForm | |
0.00% |
0 / 153 |
|
0.00% |
0 / 1 |
272 | |||
onProcessSubmit | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
doResolveRequest | |
0.00% |
0 / 88 |
|
0.00% |
0 / 1 |
210 | |||
logPromotionRename | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getRemoteUserMailAddress | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
sendNotificationEmail | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSubpagesForPrefixSearch | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * @section LICENSE |
4 | * This program is free software; you can redistribute it and/or modify |
5 | * it under the terms of the GNU General Public License as published by |
6 | * the Free Software Foundation; either version 2 of the License, or |
7 | * (at your option) any later version. |
8 | * |
9 | * This program is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | * GNU General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU General Public License along |
15 | * with this program; if not, write to the Free Software Foundation, Inc., |
16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
17 | * http://www.gnu.org/copyleft/gpl.html |
18 | * |
19 | * @file |
20 | * @ingroup SpecialPage |
21 | */ |
22 | |
23 | namespace MediaWiki\Extension\CentralAuth\Special; |
24 | |
25 | use ExtensionRegistry; |
26 | use HTMLForm; |
27 | use LogEventsList; |
28 | use MailAddress; |
29 | use MediaWiki\Extension\CentralAuth\CentralAuthDatabaseManager; |
30 | use MediaWiki\Extension\CentralAuth\CentralAuthUIService; |
31 | use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameFactory; |
32 | use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameRequest; |
33 | use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameRequestStore; |
34 | use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameUserLogger; |
35 | use MediaWiki\Extension\CentralAuth\GlobalRename\LocalRenameJob\LocalRenameUserJob; |
36 | use MediaWiki\Extension\CentralAuth\User\CentralAuthAntiSpoofManager; |
37 | use MediaWiki\Extension\CentralAuth\User\CentralAuthUser; |
38 | use MediaWiki\Extension\TitleBlacklist\TitleBlacklist; |
39 | use MediaWiki\Extension\TitleBlacklist\TitleBlacklistEntry; |
40 | use MediaWiki\JobQueue\JobQueueGroupFactory; |
41 | use MediaWiki\Logger\LoggerFactory; |
42 | use MediaWiki\SpecialPage\SpecialPage; |
43 | use MediaWiki\Status\Status; |
44 | use MediaWiki\Title\Title; |
45 | use MediaWiki\User\User; |
46 | use MediaWiki\User\UserNameUtils; |
47 | use MediaWiki\WikiMap\WikiMap; |
48 | use OOUI\MessageWidget; |
49 | use RuntimeException; |
50 | use UserMailer; |
51 | use Wikimedia\Rdbms\LBFactory; |
52 | use Xml; |
53 | |
54 | /** |
55 | * Process account rename requests made via [[Special:GlobalRenameRequest]]. |
56 | * |
57 | * @author Bryan Davis <bd808@wikimedia.org> |
58 | * @copyright © 2014 Bryan Davis and Wikimedia Foundation. |
59 | * @ingroup SpecialPage |
60 | */ |
61 | class SpecialGlobalRenameQueue extends SpecialPage { |
62 | |
63 | /** @var UserNameUtils */ |
64 | private $userNameUtils; |
65 | |
66 | /** @var LBFactory */ |
67 | private $lbFactory; |
68 | |
69 | /** @var CentralAuthDatabaseManager */ |
70 | private $databaseManager; |
71 | |
72 | /** @var CentralAuthUIService */ |
73 | private $uiService; |
74 | |
75 | /** @var GlobalRenameRequestStore */ |
76 | private $globalRenameRequestStore; |
77 | |
78 | /** @var JobQueueGroupFactory */ |
79 | private $jobQueueGroupFactory; |
80 | |
81 | private CentralAuthAntiSpoofManager $caAntiSpoofManager; |
82 | |
83 | private GlobalRenameFactory $globalRenameFactory; |
84 | |
85 | /** @var \Psr\Log\LoggerInterface */ |
86 | private $logger; |
87 | |
88 | public const PAGE_OPEN_QUEUE = 'open'; |
89 | public const PAGE_PROCESS_REQUEST = 'request'; |
90 | public const PAGE_CLOSED_QUEUE = 'closed'; |
91 | |
92 | private const ACTION_CANCEL = 'cancel'; |
93 | public const ACTION_VIEW = 'view'; |
94 | |
95 | public function __construct( |
96 | UserNameUtils $userNameUtils, |
97 | LBFactory $lbFactory, |
98 | CentralAuthDatabaseManager $databaseManager, |
99 | CentralAuthUIService $uiService, |
100 | GlobalRenameRequestStore $globalRenameRequestStore, |
101 | JobQueueGroupFactory $jobQueueGroupFactory, |
102 | CentralAuthAntiSpoofManager $caAntiSpoofManager, |
103 | GlobalRenameFactory $globalRenameFactory |
104 | ) { |
105 | parent::__construct( 'GlobalRenameQueue', 'centralauth-rename' ); |
106 | $this->userNameUtils = $userNameUtils; |
107 | $this->lbFactory = $lbFactory; |
108 | $this->databaseManager = $databaseManager; |
109 | $this->uiService = $uiService; |
110 | $this->globalRenameRequestStore = $globalRenameRequestStore; |
111 | $this->jobQueueGroupFactory = $jobQueueGroupFactory; |
112 | $this->caAntiSpoofManager = $caAntiSpoofManager; |
113 | $this->globalRenameFactory = $globalRenameFactory; |
114 | $this->logger = LoggerFactory::getInstance( 'CentralAuth' ); |
115 | } |
116 | |
117 | public function doesWrites() { |
118 | return true; |
119 | } |
120 | |
121 | /** |
122 | * @param string|null $par Subpage string if one was specified |
123 | */ |
124 | public function execute( $par ) { |
125 | $navigation = explode( '/', $par ); |
126 | $action = array_shift( $navigation ); |
127 | |
128 | $this->outputHeader(); |
129 | $this->addSubtitleLinks(); |
130 | |
131 | switch ( $action ) { |
132 | case self::PAGE_OPEN_QUEUE: |
133 | $this->handleOpenQueue(); |
134 | break; |
135 | |
136 | case self::PAGE_CLOSED_QUEUE: |
137 | $this->handleClosedQueue(); |
138 | break; |
139 | |
140 | case self::PAGE_PROCESS_REQUEST: |
141 | $this->handleProcessRequest( $navigation ); |
142 | break; |
143 | |
144 | default: |
145 | $this->doRedirectToOpenQueue(); |
146 | break; |
147 | } |
148 | } |
149 | |
150 | /** |
151 | * @param string $titleMessage Message name for page title |
152 | * @param array $titleParams Params for page title |
153 | */ |
154 | protected function commonPreamble( $titleMessage, $titleParams = [] ) { |
155 | $out = $this->getOutput(); |
156 | $this->setHeaders(); |
157 | $this->checkPermissions(); |
158 | $out->setPageTitleMsg( $this->msg( $titleMessage, $titleParams ) ); |
159 | } |
160 | |
161 | /** |
162 | * @inheritDoc |
163 | */ |
164 | public function getAssociatedNavigationLinks() { |
165 | return [ |
166 | $this->getPageTitle( self::PAGE_OPEN_QUEUE )->getPrefixedText(), |
167 | $this->getPageTitle( self::PAGE_CLOSED_QUEUE )->getPrefixedText(), |
168 | ]; |
169 | } |
170 | |
171 | /** |
172 | * @inheritDoc |
173 | */ |
174 | public function getShortDescription( string $path = '' ): string { |
175 | switch ( $path ) { |
176 | case $this->getPageTitle( self::PAGE_OPEN_QUEUE )->getText(): |
177 | return $this->msg( 'globalrenamequeue-nav-openqueue' )->text(); |
178 | case $this->getPageTitle( self::PAGE_CLOSED_QUEUE )->getText(): |
179 | return $this->msg( 'globalrenamequeue-nav-closedqueue' )->text(); |
180 | default: |
181 | return ''; |
182 | } |
183 | } |
184 | |
185 | private function addSubtitleLinks() { |
186 | if ( $this->getSkin()->supportsMenu( 'associated-pages' ) ) { |
187 | // Already shown by the skin |
188 | return; |
189 | } |
190 | $links = []; |
191 | foreach ( $this->getAssociatedNavigationLinks() as $titleText ) { |
192 | $title = Title::newFromText( $titleText ); |
193 | $links[] = $this->getLinkRenderer()->makeKnownLink( |
194 | $title, |
195 | $this->getShortDescription( $title->getText() ) |
196 | ); |
197 | } |
198 | $this->getOutput()->addSubtitle( $this->getLanguage()->pipeList( $links ) ); |
199 | } |
200 | |
201 | /** |
202 | * Get an array of fields for use by the HTMLForm shown above the pager. |
203 | * |
204 | * @return array[] |
205 | */ |
206 | private function getCommonFormFieldsArray() { |
207 | $lang = $this->getLanguage(); |
208 | return [ |
209 | 'username' => [ |
210 | 'type' => 'text', |
211 | 'name' => 'username', |
212 | 'label-message' => 'globalrenamequeue-form-username', |
213 | 'size' => 30, |
214 | ], |
215 | 'newname' => [ |
216 | 'type' => 'text', |
217 | 'name' => 'newname', |
218 | 'size' => 30, |
219 | 'label-message' => 'globalrenamequeue-form-newname', |
220 | ], |
221 | 'limit' => [ |
222 | 'type' => 'limitselect', |
223 | 'name' => 'limit', |
224 | 'label-message' => 'table_pager_limit_label', |
225 | 'options' => [ |
226 | $lang->formatNum( 25 ) => 25, |
227 | $lang->formatNum( 50 ) => 50, |
228 | $lang->formatNum( 75 ) => 75, |
229 | $lang->formatNum( 100 ) => 100, |
230 | ], |
231 | ], |
232 | ]; |
233 | } |
234 | |
235 | /** |
236 | * Initialize and output the HTMLForm used for filtering. |
237 | * |
238 | * @param array $formDescriptor |
239 | */ |
240 | private function outputFilterForm( array $formDescriptor ) { |
241 | $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() ); |
242 | $htmlForm |
243 | ->setMethod( 'get' ) |
244 | ->setWrapperLegendMsg( 'search' ) |
245 | ->prepareForm()->displayForm( false ); |
246 | } |
247 | |
248 | /** |
249 | * Handle requests to display the open request queue |
250 | */ |
251 | protected function handleOpenQueue() { |
252 | $this->commonPreamble( 'globalrenamequeue' ); |
253 | $this->outputFilterForm( $this->getCommonFormFieldsArray() ); |
254 | |
255 | $pager = new RenameQueueTablePager( |
256 | $this->getContext(), |
257 | $this->getLinkRenderer(), |
258 | $this->databaseManager, |
259 | $this->userNameUtils, |
260 | self::PAGE_OPEN_QUEUE |
261 | ); |
262 | $this->getOutput()->addParserOutputContent( $pager->getFullOutput() ); |
263 | } |
264 | |
265 | /** |
266 | * Handle requests to display the closed request queue |
267 | */ |
268 | protected function handleClosedQueue() { |
269 | $this->commonPreamble( 'globalrenamequeue' ); |
270 | $formDescriptor = array_merge( |
271 | $this->getCommonFormFieldsArray(), |
272 | [ |
273 | 'status' => [ |
274 | 'type' => 'select', |
275 | 'name' => 'status', |
276 | 'label-message' => 'globalrenamequeue-form-status', |
277 | 'options-messages' => [ |
278 | 'globalrenamequeue-form-status-all' => 'all', |
279 | 'globalrenamequeue-view-approved' => GlobalRenameRequest::APPROVED, |
280 | 'globalrenamequeue-view-rejected' => GlobalRenameRequest::REJECTED, |
281 | ], |
282 | 'default' => 'all', |
283 | ] |
284 | ] |
285 | ); |
286 | $this->outputFilterForm( $formDescriptor ); |
287 | |
288 | $pager = new RenameQueueTablePager( |
289 | $this->getContext(), |
290 | $this->getLinkRenderer(), |
291 | $this->databaseManager, |
292 | $this->userNameUtils, |
293 | self::PAGE_CLOSED_QUEUE |
294 | ); |
295 | $this->getOutput()->addParserOutputContent( $pager->getFullOutput() ); |
296 | } |
297 | |
298 | /** |
299 | * Handle requests related to processing a request. |
300 | * |
301 | * @param array $pathArgs Extra path arguments |
302 | */ |
303 | protected function handleProcessRequest( array $pathArgs ) { |
304 | if ( !$pathArgs ) { |
305 | $this->doRedirectToOpenQueue(); |
306 | return; |
307 | } |
308 | |
309 | $rqId = array_shift( $pathArgs ); |
310 | if ( !is_numeric( $rqId ) ) { |
311 | $this->showUnkownRequest(); |
312 | return; |
313 | } |
314 | $req = $this->globalRenameRequestStore->newFromId( $rqId ); |
315 | if ( !$req->exists() ) { |
316 | $this->showUnkownRequest(); |
317 | return; |
318 | } |
319 | |
320 | $action = array_shift( $pathArgs ); |
321 | if ( !$req->isPending() ) { |
322 | $action = self::ACTION_VIEW; |
323 | } |
324 | |
325 | switch ( $action ) { |
326 | case self::ACTION_CANCEL: |
327 | $this->doRedirectToOpenQueue(); |
328 | break; |
329 | case self::ACTION_VIEW: |
330 | $this->doViewRequest( $req ); |
331 | break; |
332 | default: |
333 | $this->doShowProcessForm( $req ); |
334 | break; |
335 | } |
336 | } |
337 | |
338 | private function showUnkownRequest() { |
339 | $this->commonPreamble( 'globalrenamequeue-request-unknown-title' ); |
340 | $this->getOutput()->addWikiMsg( |
341 | 'globalrenamequeue-request-unknown-body' |
342 | ); |
343 | } |
344 | |
345 | protected function doRedirectToOpenQueue() { |
346 | $this->getOutput()->redirect( |
347 | $this->getPageTitle( self::PAGE_OPEN_QUEUE )->getFullURL() |
348 | ); |
349 | } |
350 | |
351 | /** |
352 | * Display a request. |
353 | * |
354 | * @param GlobalRenameRequest $req |
355 | */ |
356 | protected function doViewRequest( GlobalRenameRequest $req ) { |
357 | $this->commonPreamble( 'globalrenamequeue-request-status-title', |
358 | [ $req->getName(), $req->getNewName() ] |
359 | ); |
360 | |
361 | $reason = $req->getReason() ?: $this->msg( |
362 | 'globalrenamequeue-request-reason-sul' |
363 | )->parseAsBlock(); |
364 | |
365 | $renamer = CentralAuthUser::newFromId( $req->getPerformer() ); |
366 | if ( $renamer === false ) { |
367 | throw new RuntimeException( |
368 | "The performer's global user id ({$req->getPerformer()}) " . |
369 | "does not exist in the database" |
370 | ); |
371 | } |
372 | $homewiki = $renamer->getHomeWiki(); |
373 | if ( $renamer->isAttached() || $homewiki === null ) { |
374 | $renamerLink = Title::makeTitleSafe( NS_USER, $renamer->getName() )->getFullURL(); |
375 | } else { |
376 | $renamerLink = WikiMap::getForeignURL( $homewiki, 'User:' . $renamer->getName() ); |
377 | } |
378 | |
379 | if ( strpos( $reason, "\n" ) !== false ) { |
380 | $reason = "<dl><dd>" . str_replace( "\n", "</dd><dd>", $reason ) . "</dd></dl>"; |
381 | } else { |
382 | $reason = ': ' . $reason; |
383 | } |
384 | |
385 | // Done as one big message so that admins can create a local |
386 | // translation to customize the output as they see fit. |
387 | // @TODO: Do that actually in here... this is not how we do interfaces in 2015. |
388 | $viewMsg = $this->msg( 'globalrenamequeue-view', |
389 | $req->getName(), |
390 | $req->getNewName(), |
391 | $reason, |
392 | $this->msg( 'globalrenamequeue-view-' . $req->getStatus() )->text(), |
393 | $this->getLanguage()->userTimeAndDate( |
394 | $req->getRequested(), $this->getUser() |
395 | ), |
396 | $this->getLanguage()->userTimeAndDate( |
397 | $req->getCompleted(), $this->getUser() |
398 | ), |
399 | $renamerLink, |
400 | $renamer->getName(), |
401 | $req->getComments() |
402 | )->parseAsBlock(); |
403 | $this->getOutput()->addHtml( '<div class="plainlinks">' . $viewMsg . '</div>' ); |
404 | } |
405 | |
406 | /** |
407 | * Display form for approving/denying request or process form submission. |
408 | * |
409 | * @param GlobalRenameRequest $req Pending request |
410 | */ |
411 | protected function doShowProcessForm( GlobalRenameRequest $req ) { |
412 | $this->commonPreamble( |
413 | 'globalrenamequeue-request-title', [ $req->getName() ] |
414 | ); |
415 | |
416 | $htmlForm = HTMLForm::factory( 'ooui', |
417 | [ |
418 | 'rid' => [ |
419 | 'default' => $req->getId(), |
420 | 'name' => 'rid', |
421 | 'type' => 'hidden', |
422 | ], |
423 | 'comments' => [ |
424 | 'default' => $this->getRequest()->getVal( 'comments' ), |
425 | 'id' => 'mw-renamequeue-comments', |
426 | 'label-message' => 'globalrenamequeue-request-comments-label', |
427 | 'name' => 'comments', |
428 | 'type' => 'textarea', |
429 | 'rows' => 5, |
430 | ], |
431 | ], |
432 | $this->getContext(), |
433 | 'globalrenamequeue' |
434 | ); |
435 | |
436 | // Show tools to approve only when user is not reviewing own request. |
437 | if ( $req->getName() !== $this->getUser()->getName() ) { |
438 | $htmlForm |
439 | ->addFields( [ |
440 | // The following fields need to have their names stay in |
441 | // sync with the expectations of GlobalRenameUser::rename() |
442 | 'reason' => [ |
443 | 'id' => 'mw-renamequeue-reason', |
444 | 'label-message' => 'globalrenamequeue-request-reason-label', |
445 | 'name' => 'reason', |
446 | 'type' => 'text', |
447 | ], |
448 | 'movepages' => [ |
449 | 'id' => 'mw-renamequeue-movepages', |
450 | 'name' => 'movepages', |
451 | 'label-message' => 'globalrenamequeue-request-movepages', |
452 | 'type' => 'check', |
453 | 'default' => 1, |
454 | ], |
455 | 'suppressredirects' => [ |
456 | 'id' => 'mw-renamequeue-suppressredirects', |
457 | 'name' => 'suppressredirects', |
458 | 'label-message' => 'globalrenamequeue-request-suppressredirects', |
459 | 'type' => 'check', |
460 | ], |
461 | ] ) |
462 | ->addButton( [ |
463 | 'name' => 'approve', |
464 | 'value' => $this->msg( 'globalrenamequeue-request-approve-text' )->text(), |
465 | 'id' => 'mw-renamequeue-approve', |
466 | 'flags' => [ 'primary', 'progressive' ], |
467 | 'framed' => true |
468 | ] ); |
469 | } |
470 | |
471 | $htmlForm |
472 | ->suppressDefaultSubmit() |
473 | ->addButton( [ |
474 | 'name' => 'deny', |
475 | 'value' => $this->msg( 'globalrenamequeue-request-deny-text' )->text(), |
476 | 'id' => 'mw-renamequeue-deny', |
477 | 'flags' => [ 'destructive' ], |
478 | 'framed' => true |
479 | ] ) |
480 | ->addButton( [ |
481 | 'name' => 'cancel', |
482 | 'value' => $this->msg( 'globalrenamequeue-request-cancel-text' )->text(), |
483 | 'id' => 'mw-renamequeue-cancel', |
484 | ] ) |
485 | ->setId( 'mw-globalrenamequeue-request' ); |
486 | |
487 | if ( $req->userIsGlobal() ) { |
488 | $globalUser = CentralAuthUser::getInstanceByName( $req->getName() ); |
489 | $homeWiki = $globalUser->getHomeWiki(); |
490 | $infoMsgKey = 'globalrenamequeue-request-userinfo-global'; |
491 | } else { |
492 | $homeWiki = $req->getWiki(); |
493 | $infoMsgKey = 'globalrenamequeue-request-userinfo-local'; |
494 | } |
495 | |
496 | if ( $homeWiki === null ) { |
497 | $homeLink = Title::makeTitleSafe( NS_USER, $req->getName() )->getFullURL(); |
498 | } else { |
499 | $homeLink = WikiMap::getForeignURL( $homeWiki, 'User:' . $req->getName() ); |
500 | } |
501 | |
502 | $headerMsg = $this->msg( 'globalrenamequeue-request-header', |
503 | $homeLink, |
504 | $req->getName(), |
505 | $req->getNewName() |
506 | ); |
507 | $htmlForm->addHeaderHtml( '<span class="plainlinks">' . $headerMsg->parseAsBlock() . |
508 | '</span>' ); |
509 | |
510 | $homeWikiWiki = $homeWiki ? WikiMap::getWiki( $homeWiki ) : null; |
511 | $infoMsg = $this->msg( $infoMsgKey, |
512 | $req->getName(), |
513 | // homeWikiWiki shouldn't ever be null except in |
514 | // a development/testing environment. |
515 | ( $homeWikiWiki ? $homeWikiWiki->getDisplayName() : $homeWiki ), |
516 | $req->getNewName() |
517 | ); |
518 | |
519 | if ( isset( $globalUser ) ) { |
520 | $infoMsg->numParams( $globalUser->getGlobalEditCount() ); |
521 | } |
522 | |
523 | $htmlForm->addHeaderHtml( $infoMsg->parseAsBlock() ); |
524 | |
525 | // Handle AntiSpoof integration |
526 | $spoofUser = $this->caAntiSpoofManager->getSpoofUser( $req->getNewName() ); |
527 | $conflicts = $this->uiService->processAntiSpoofConflicts( |
528 | $this->getContext(), |
529 | $req->getName(), |
530 | $spoofUser->getConflicts() |
531 | ); |
532 | $renamedUser = $this->caAntiSpoofManager->getOldRenamedUserName( $req->getNewName() ); |
533 | if ( $renamedUser !== null ) { |
534 | $conflicts[] = $renamedUser; |
535 | } |
536 | if ( $conflicts ) { |
537 | $htmlForm->addHeaderHtml( |
538 | $this->msg( |
539 | 'globalrenamequeue-request-antispoof-conflicts', |
540 | $this->getLanguage()->commaList( $conflicts ) |
541 | )->numParams( count( $conflicts ) )->parseAsBlock() |
542 | ); |
543 | } |
544 | |
545 | // Show a message if the new username matches the title blacklist. |
546 | if ( ExtensionRegistry::getInstance()->isLoaded( 'TitleBlacklist' ) ) { |
547 | $titleBlacklist = TitleBlacklist::singleton()->isBlacklisted( |
548 | Title::makeTitleSafe( NS_USER, $req->getNewName() ), |
549 | 'new-account' |
550 | ); |
551 | if ( $titleBlacklist instanceof TitleBlacklistEntry ) { |
552 | $htmlForm->addHeaderHtml( |
553 | $this->msg( 'globalrenamequeue-request-titleblacklist' ) |
554 | ->params( wfEscapeWikiText( $titleBlacklist->getRegex() ) )->parseAsBlock() |
555 | ); |
556 | } |
557 | } |
558 | |
559 | // Show a log entry of previous renames under the requesting user's username |
560 | $caTitle = SpecialPage::getTitleFor( 'CentralAuth', $req->getName() ); |
561 | $extract = ''; |
562 | $extractCount = LogEventsList::showLogExtract( $extract, 'gblrename', $caTitle, '', [ |
563 | 'showIfEmpty' => false, |
564 | ] ); |
565 | if ( $extractCount ) { |
566 | $htmlForm->addHeaderHtml( |
567 | Xml::fieldset( $this->msg( 'globalrenamequeue-request-previous-renames' ) |
568 | ->numParams( $extractCount ) |
569 | ->text(), $extract ) |
570 | ); |
571 | } |
572 | |
573 | $reason = $req->getReason() ?: $this->msg( |
574 | 'globalrenamequeue-request-reason-sul' |
575 | )->parseAsBlock(); |
576 | $htmlForm->addHeaderHtml( $this->msg( 'globalrenamequeue-request-reason', |
577 | "<dl><dd>" . str_replace( "\n", "</dd><dd>", $reason ) . "</dd></dl>" |
578 | )->parseAsBlock() ); |
579 | |
580 | // Show warning when reviewing own request |
581 | if ( $req->getName() === $this->getUser()->getName() ) { |
582 | $message = new MessageWidget( [ |
583 | 'label' => $this->msg( 'globalrenamerequest-self-warning' )->text(), |
584 | 'type' => 'warning', |
585 | 'inline' => true |
586 | ] ); |
587 | $htmlForm->addHeaderHtml( $message->toString() ); |
588 | } |
589 | |
590 | $htmlForm->setSubmitCallback( [ $this, 'onProcessSubmit' ] ); |
591 | |
592 | $out = $this->getOutput(); |
593 | $out->addModuleStyles( 'ext.centralauth.globalrenamequeue.styles' ); |
594 | $out->addModules( 'ext.centralauth.globalrenamequeue' ); |
595 | |
596 | $status = $htmlForm->show(); |
597 | if ( $status instanceof Status && $status->isOK() ) { |
598 | $this->getOutput()->redirect( |
599 | $this->getPageTitle( |
600 | self::PAGE_PROCESS_REQUEST . "/{$req->getId()}/{$status->value}" |
601 | )->getFullURL() |
602 | ); |
603 | } |
604 | } |
605 | |
606 | /** |
607 | * @param array $data |
608 | * @return Status |
609 | */ |
610 | public function onProcessSubmit( array $data ) { |
611 | $request = $this->getContext()->getRequest(); |
612 | $status = new Status; |
613 | if ( $request->getCheck( 'approve' ) ) { |
614 | $status = $this->doResolveRequest( true, $data ); |
615 | } elseif ( $request->getCheck( 'deny' ) ) { |
616 | $status = $this->doResolveRequest( false, $data ); |
617 | } else { |
618 | $status->setResult( true, 'cancel' ); |
619 | } |
620 | return $status; |
621 | } |
622 | |
623 | /** |
624 | * @param bool $approved |
625 | * @param array $data |
626 | * |
627 | * @return Status |
628 | */ |
629 | protected function doResolveRequest( $approved, $data ) { |
630 | $request = $this->globalRenameRequestStore->newFromId( $data['rid'] ); |
631 | $oldUser = User::newFromName( $request->getName() ); |
632 | |
633 | $newUser = User::newFromName( $request->getNewName(), 'creatable' ); |
634 | $status = new Status; |
635 | $session = $this->getContext()->exportSession(); |
636 | if ( $approved ) { |
637 | // Disallow self-renaming |
638 | if ( $request->getName() === $this->getUser()->getName() ) { |
639 | return Status::newFatal( 'globalrenamerequest-self-error' ); |
640 | } |
641 | |
642 | if ( $request->userIsGlobal() ) { |
643 | // Trigger a global rename job |
644 | |
645 | $status = $this->globalRenameFactory |
646 | ->newGlobalRenameUser( |
647 | $this->getUser(), |
648 | CentralAuthUser::getInstanceByName( $request->getName() ), |
649 | $request->getNewName() |
650 | ) |
651 | ->withSession( $session ) |
652 | ->rename( $data ); |
653 | } else { |
654 | // If the user is local-only: |
655 | // * rename the local user using LocalRenameUserJob |
656 | // * create a global user attached only to the local wiki |
657 | $job = new LocalRenameUserJob( |
658 | Title::newFromText( 'Global rename job' ), |
659 | [ |
660 | 'from' => $oldUser->getName(), |
661 | 'to' => $newUser->getName(), |
662 | 'renamer' => $this->getUser()->getName(), |
663 | 'movepages' => true, |
664 | 'suppressredirects' => true, |
665 | 'promotetoglobal' => true, |
666 | 'reason' => $data['reason'], |
667 | 'session' => $session, |
668 | ] |
669 | ); |
670 | $this->jobQueueGroupFactory->makeJobQueueGroup( $request->getWiki() )->push( $job ); |
671 | // Now log it |
672 | $this->logPromotionRename( |
673 | $oldUser->getName(), |
674 | $request->getWiki(), |
675 | $newUser->getName(), |
676 | $data['reason'] |
677 | ); |
678 | $status = Status::newGood(); |
679 | } |
680 | } |
681 | |
682 | if ( $status->isGood() ) { |
683 | $request->setStatus( |
684 | $approved ? GlobalRenameRequest::APPROVED : GlobalRenameRequest::REJECTED |
685 | ); |
686 | $request->setCompleted( wfTimestampNow() ); |
687 | $request->setPerformer( |
688 | CentralAuthUser::getInstance( $this->getUser() )->getId() |
689 | ); |
690 | $request->setComments( $data['comments'] ); |
691 | |
692 | if ( $this->globalRenameRequestStore->save( $request ) ) { |
693 | // Send email to the user about the change in status. |
694 | if ( $approved ) { |
695 | $subject = $this->msg( |
696 | 'globalrenamequeue-email-subject-approved' |
697 | )->inContentLanguage()->text(); |
698 | if ( $request->getComments() === '' ) { |
699 | $msgKey = 'globalrenamequeue-email-body-approved'; |
700 | } else { |
701 | $msgKey = 'globalrenamequeue-email-body-approved-with-note'; |
702 | } |
703 | $body = $this->msg( |
704 | $msgKey, |
705 | [ |
706 | $oldUser->getName(), |
707 | $newUser->getName(), |
708 | $request->getComments(), |
709 | ] |
710 | )->inContentLanguage()->text(); |
711 | } else { |
712 | $subject = $this->msg( |
713 | 'globalrenamequeue-email-subject-rejected' |
714 | )->inContentLanguage()->text(); |
715 | $body = $this->msg( |
716 | 'globalrenamequeue-email-body-rejected', |
717 | [ |
718 | $oldUser->getName(), |
719 | $newUser->getName(), |
720 | $request->getComments(), |
721 | ] |
722 | )->inContentLanguage()->text(); |
723 | } |
724 | |
725 | if ( $request->userIsGlobal() || $request->getWiki() === WikiMap::getCurrentWikiId() ) { |
726 | $notifyEmail = MailAddress::newFromUser( $oldUser ); |
727 | } else { |
728 | $notifyEmail = $this->getRemoteUserMailAddress( |
729 | $request->getWiki(), $request->getName() |
730 | ); |
731 | } |
732 | |
733 | if ( $notifyEmail !== null && $notifyEmail->address ) { |
734 | $type = $approved ? 'approval' : 'rejection'; |
735 | $this->logger->info( "Send $type email to User:{oldName}", [ |
736 | 'oldName' => $oldUser->getName(), |
737 | 'component' => 'GlobalRename', |
738 | ] ); |
739 | $this->sendNotificationEmail( $notifyEmail, $subject, $body ); |
740 | } |
741 | } else { |
742 | $status->fatal( 'globalrenamequeue-request-savefailed' ); |
743 | } |
744 | } |
745 | return $status; |
746 | } |
747 | |
748 | /** |
749 | * Log a promotion to global rename in the global rename log |
750 | * |
751 | * @param string $oldName |
752 | * @param string $wiki |
753 | * @param string $newName |
754 | * @param string $reason |
755 | */ |
756 | protected function logPromotionRename( $oldName, $wiki, $newName, $reason ) { |
757 | $logger = new GlobalRenameUserLogger( $this->getUser() ); |
758 | $logger->logPromotion( $oldName, $wiki, $newName, $reason ); |
759 | } |
760 | |
761 | /** |
762 | * Get a MailAddress for a user on a remote wiki |
763 | * |
764 | * @param string $wiki |
765 | * @param string $username |
766 | * @return MailAddress|null |
767 | */ |
768 | protected function getRemoteUserMailAddress( $wiki, $username ) { |
769 | $lb = $this->lbFactory->getMainLB( $wiki ); |
770 | $remoteDB = $lb->getConnection( DB_REPLICA, [], $wiki ); |
771 | $row = $remoteDB->newSelectQueryBuilder() |
772 | ->select( [ 'user_email', 'user_name', 'user_real_name' ] ) |
773 | ->from( 'user' ) |
774 | ->where( [ |
775 | 'user_name' => $this->userNameUtils->getCanonical( $username ), |
776 | ] ) |
777 | ->caller( __METHOD__ ) |
778 | ->fetchRow(); |
779 | if ( $row === false ) { |
780 | $address = null; |
781 | } else { |
782 | $address = new MailAddress( |
783 | $row->user_email, $row->user_name, $row->user_real_name |
784 | ); |
785 | } |
786 | return $address; |
787 | } |
788 | |
789 | /** |
790 | * Send an email notifying the user of the result of their request. |
791 | * |
792 | * @param MailAddress $to |
793 | * @param string $subject |
794 | * @param string $body |
795 | * @return Status |
796 | */ |
797 | protected function sendNotificationEmail( MailAddress $to, $subject, $body ) { |
798 | $from = new MailAddress( |
799 | $this->getConfig()->get( 'PasswordSender' ), |
800 | $this->msg( 'emailsender' )->inContentLanguage()->text() |
801 | ); |
802 | return UserMailer::send( $to, $from, $subject, $body ); |
803 | } |
804 | |
805 | /** @inheritDoc */ |
806 | protected function getGroupName() { |
807 | return 'users'; |
808 | } |
809 | |
810 | /** @inheritDoc */ |
811 | public function getSubpagesForPrefixSearch() { |
812 | return [ |
813 | self::PAGE_OPEN_QUEUE, |
814 | self::PAGE_PROCESS_REQUEST, |
815 | self::PAGE_CLOSED_QUEUE |
816 | ]; |
817 | } |
818 | } |