Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 600 |
|
0.00% |
0 / 24 |
CRAP | |
0.00% |
0 / 1 |
SpecialGlobalRenameQueue | |
0.00% |
0 / 600 |
|
0.00% |
0 / 24 |
9900 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 11 |
|
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 / 37 |
|
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 / 64 |
|
0.00% |
0 / 1 |
132 | |||
doShowProcessForm | |
0.00% |
0 / 176 |
|
0.00% |
0 / 1 |
462 | |||
onProcessSubmit | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
doResolveRequest | |
0.00% |
0 / 112 |
|
0.00% |
0 / 1 |
380 | |||
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 | |||
sendEmailForRejectionOfVanishRequest | |
0.00% |
0 / 53 |
|
0.00% |
0 / 1 |
90 | |||
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 Exception; |
26 | use LogEventsList; |
27 | use MailAddress; |
28 | use MediaWiki\Extension\CentralAuth\CentralAuthDatabaseManager; |
29 | use MediaWiki\Extension\CentralAuth\CentralAuthUIService; |
30 | use MediaWiki\Extension\CentralAuth\Config\CAMainConfigNames; |
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\HTMLForm\HTMLForm; |
41 | use MediaWiki\JobQueue\JobQueueGroupFactory; |
42 | use MediaWiki\Logger\LoggerFactory; |
43 | use MediaWiki\MainConfigNames; |
44 | use MediaWiki\Registration\ExtensionRegistry; |
45 | use MediaWiki\SpecialPage\SpecialPage; |
46 | use MediaWiki\Status\Status; |
47 | use MediaWiki\Title\Title; |
48 | use MediaWiki\User\User; |
49 | use MediaWiki\User\UserIdentityLookup; |
50 | use MediaWiki\User\UserNameUtils; |
51 | use MediaWiki\WikiMap\WikiMap; |
52 | use MediaWiki\Xml\Xml; |
53 | use OOUI\MessageWidget; |
54 | use Psr\Log\LoggerInterface; |
55 | use RuntimeException; |
56 | use UserMailer; |
57 | use Wikimedia\Rdbms\LBFactory; |
58 | |
59 | /** |
60 | * Process account rename requests made via [[Special:GlobalRenameRequest]]. |
61 | * |
62 | * @author Bryan Davis <bd808@wikimedia.org> |
63 | * @copyright © 2014 Bryan Davis and Wikimedia Foundation. |
64 | * @ingroup SpecialPage |
65 | */ |
66 | class SpecialGlobalRenameQueue extends SpecialPage { |
67 | |
68 | private UserNameUtils $userNameUtils; |
69 | private LBFactory $lbFactory; |
70 | private CentralAuthDatabaseManager $databaseManager; |
71 | private CentralAuthUIService $uiService; |
72 | private GlobalRenameRequestStore $globalRenameRequestStore; |
73 | private JobQueueGroupFactory $jobQueueGroupFactory; |
74 | private CentralAuthAntiSpoofManager $caAntiSpoofManager; |
75 | private GlobalRenameFactory $globalRenameFactory; |
76 | private UserIdentityLookup $userIdentityLookup; |
77 | private LoggerInterface $logger; |
78 | |
79 | public const PAGE_OPEN_QUEUE = 'open'; |
80 | public const PAGE_PROCESS_REQUEST = 'request'; |
81 | public const PAGE_CLOSED_QUEUE = 'closed'; |
82 | |
83 | private const ACTION_CANCEL = 'cancel'; |
84 | public const ACTION_VIEW = 'view'; |
85 | |
86 | public function __construct( |
87 | UserNameUtils $userNameUtils, |
88 | LBFactory $lbFactory, |
89 | CentralAuthDatabaseManager $databaseManager, |
90 | CentralAuthUIService $uiService, |
91 | GlobalRenameRequestStore $globalRenameRequestStore, |
92 | JobQueueGroupFactory $jobQueueGroupFactory, |
93 | CentralAuthAntiSpoofManager $caAntiSpoofManager, |
94 | GlobalRenameFactory $globalRenameFactory, |
95 | UserIdentityLookup $userIdentityLookup |
96 | ) { |
97 | parent::__construct( 'GlobalRenameQueue', 'centralauth-rename' ); |
98 | $this->userNameUtils = $userNameUtils; |
99 | $this->lbFactory = $lbFactory; |
100 | $this->databaseManager = $databaseManager; |
101 | $this->uiService = $uiService; |
102 | $this->globalRenameRequestStore = $globalRenameRequestStore; |
103 | $this->jobQueueGroupFactory = $jobQueueGroupFactory; |
104 | $this->caAntiSpoofManager = $caAntiSpoofManager; |
105 | $this->globalRenameFactory = $globalRenameFactory; |
106 | $this->userIdentityLookup = $userIdentityLookup; |
107 | $this->logger = LoggerFactory::getInstance( 'CentralAuth' ); |
108 | } |
109 | |
110 | public function doesWrites() { |
111 | return true; |
112 | } |
113 | |
114 | /** |
115 | * @param string|null $par Subpage string if one was specified |
116 | */ |
117 | public function execute( $par ) { |
118 | $navigation = explode( '/', $par ); |
119 | $action = array_shift( $navigation ); |
120 | |
121 | $this->outputHeader(); |
122 | $this->addSubtitleLinks(); |
123 | |
124 | switch ( $action ) { |
125 | case self::PAGE_OPEN_QUEUE: |
126 | $this->handleOpenQueue(); |
127 | break; |
128 | |
129 | case self::PAGE_CLOSED_QUEUE: |
130 | $this->handleClosedQueue(); |
131 | break; |
132 | |
133 | case self::PAGE_PROCESS_REQUEST: |
134 | $this->handleProcessRequest( $navigation ); |
135 | break; |
136 | |
137 | default: |
138 | $this->doRedirectToOpenQueue(); |
139 | break; |
140 | } |
141 | } |
142 | |
143 | /** |
144 | * @param string $titleMessage Message name for page title |
145 | * @param array $titleParams Params for page title |
146 | */ |
147 | protected function commonPreamble( $titleMessage, $titleParams = [] ) { |
148 | $out = $this->getOutput(); |
149 | $this->setHeaders(); |
150 | $this->checkPermissions(); |
151 | $out->setPageTitleMsg( $this->msg( $titleMessage, $titleParams ) ); |
152 | } |
153 | |
154 | /** |
155 | * @inheritDoc |
156 | */ |
157 | public function getAssociatedNavigationLinks() { |
158 | return [ |
159 | $this->getPageTitle( self::PAGE_OPEN_QUEUE )->getPrefixedText(), |
160 | $this->getPageTitle( self::PAGE_CLOSED_QUEUE )->getPrefixedText(), |
161 | ]; |
162 | } |
163 | |
164 | /** |
165 | * @inheritDoc |
166 | */ |
167 | public function getShortDescription( string $path = '' ): string { |
168 | switch ( $path ) { |
169 | case $this->getPageTitle( self::PAGE_OPEN_QUEUE )->getText(): |
170 | return $this->msg( 'globalrenamequeue-nav-openqueue' )->text(); |
171 | case $this->getPageTitle( self::PAGE_CLOSED_QUEUE )->getText(): |
172 | return $this->msg( 'globalrenamequeue-nav-closedqueue' )->text(); |
173 | default: |
174 | return ''; |
175 | } |
176 | } |
177 | |
178 | private function addSubtitleLinks() { |
179 | if ( $this->getSkin()->supportsMenu( 'associated-pages' ) ) { |
180 | // Already shown by the skin |
181 | return; |
182 | } |
183 | $links = []; |
184 | foreach ( $this->getAssociatedNavigationLinks() as $titleText ) { |
185 | $title = Title::newFromText( $titleText ); |
186 | $links[] = $this->getLinkRenderer()->makeKnownLink( |
187 | $title, |
188 | $this->getShortDescription( $title->getText() ) |
189 | ); |
190 | } |
191 | $this->getOutput()->addSubtitle( $this->getLanguage()->pipeList( $links ) ); |
192 | } |
193 | |
194 | /** |
195 | * Get an array of fields for use by the HTMLForm shown above the pager. |
196 | * |
197 | * @return array[] |
198 | */ |
199 | private function getCommonFormFieldsArray() { |
200 | $lang = $this->getLanguage(); |
201 | return [ |
202 | 'username' => [ |
203 | 'type' => 'text', |
204 | 'name' => 'username', |
205 | 'label-message' => 'globalrenamequeue-form-username', |
206 | 'size' => 30, |
207 | ], |
208 | 'newname' => [ |
209 | 'type' => 'text', |
210 | 'name' => 'newname', |
211 | 'size' => 30, |
212 | 'label-message' => 'globalrenamequeue-form-newname', |
213 | ], |
214 | 'limit' => [ |
215 | 'type' => 'limitselect', |
216 | 'name' => 'limit', |
217 | 'label-message' => 'table_pager_limit_label', |
218 | 'options' => [ |
219 | $lang->formatNum( 25 ) => 25, |
220 | $lang->formatNum( 50 ) => 50, |
221 | $lang->formatNum( 75 ) => 75, |
222 | $lang->formatNum( 100 ) => 100, |
223 | ], |
224 | ], |
225 | 'type' => [ |
226 | 'type' => 'select', |
227 | 'name' => 'type', |
228 | 'label-message' => 'globalrenamequeue-form-type', |
229 | 'options-messages' => [ |
230 | 'globalrenamequeue-form-type-all' => 'all', |
231 | 'globalrenamequeue-form-type-rename' => GlobalRenameRequest::RENAME, |
232 | 'globalrenamequeue-form-type-vanish' => GlobalRenameRequest::VANISH, |
233 | ], |
234 | 'default' => 'all', |
235 | ], |
236 | ]; |
237 | } |
238 | |
239 | /** |
240 | * Initialize and output the HTMLForm used for filtering. |
241 | * |
242 | * @param array $formDescriptor |
243 | */ |
244 | private function outputFilterForm( array $formDescriptor ) { |
245 | $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() ); |
246 | $htmlForm |
247 | ->setMethod( 'get' ) |
248 | ->setWrapperLegendMsg( 'search' ) |
249 | ->prepareForm()->displayForm( false ); |
250 | } |
251 | |
252 | /** |
253 | * Handle requests to display the open request queue |
254 | */ |
255 | protected function handleOpenQueue() { |
256 | $this->commonPreamble( 'globalrenamequeue' ); |
257 | $this->outputFilterForm( $this->getCommonFormFieldsArray() ); |
258 | |
259 | $pager = new RenameQueueTablePager( |
260 | $this->getContext(), |
261 | $this->getLinkRenderer(), |
262 | $this->databaseManager, |
263 | $this->userNameUtils, |
264 | self::PAGE_OPEN_QUEUE |
265 | ); |
266 | $this->getOutput()->addParserOutputContent( $pager->getFullOutput() ); |
267 | } |
268 | |
269 | /** |
270 | * Handle requests to display the closed request queue |
271 | */ |
272 | protected function handleClosedQueue() { |
273 | $this->commonPreamble( 'globalrenamequeue' ); |
274 | $formDescriptor = array_merge( |
275 | $this->getCommonFormFieldsArray(), |
276 | [ |
277 | 'status' => [ |
278 | 'type' => 'select', |
279 | 'name' => 'status', |
280 | 'label-message' => 'globalrenamequeue-form-status', |
281 | 'options-messages' => [ |
282 | 'globalrenamequeue-form-status-all' => 'all', |
283 | 'globalrenamequeue-view-approved' => GlobalRenameRequest::APPROVED, |
284 | 'globalrenamequeue-view-rejected' => GlobalRenameRequest::REJECTED, |
285 | ], |
286 | 'default' => 'all', |
287 | ] |
288 | ] |
289 | ); |
290 | $this->outputFilterForm( $formDescriptor ); |
291 | |
292 | $pager = new RenameQueueTablePager( |
293 | $this->getContext(), |
294 | $this->getLinkRenderer(), |
295 | $this->databaseManager, |
296 | $this->userNameUtils, |
297 | self::PAGE_CLOSED_QUEUE |
298 | ); |
299 | $this->getOutput()->addParserOutputContent( $pager->getFullOutput() ); |
300 | } |
301 | |
302 | /** |
303 | * Handle requests related to processing a request. |
304 | * |
305 | * @param array $pathArgs Extra path arguments |
306 | */ |
307 | protected function handleProcessRequest( array $pathArgs ) { |
308 | if ( !$pathArgs ) { |
309 | $this->doRedirectToOpenQueue(); |
310 | return; |
311 | } |
312 | |
313 | $rqId = array_shift( $pathArgs ); |
314 | if ( !is_numeric( $rqId ) ) { |
315 | $this->showUnkownRequest(); |
316 | return; |
317 | } |
318 | $req = $this->globalRenameRequestStore->newFromId( $rqId ); |
319 | if ( !$req->exists() ) { |
320 | $this->showUnkownRequest(); |
321 | return; |
322 | } |
323 | |
324 | $action = array_shift( $pathArgs ); |
325 | if ( !$req->isPending() ) { |
326 | $action = self::ACTION_VIEW; |
327 | } |
328 | |
329 | switch ( $action ) { |
330 | case self::ACTION_CANCEL: |
331 | $this->doRedirectToOpenQueue(); |
332 | break; |
333 | case self::ACTION_VIEW: |
334 | $this->doViewRequest( $req ); |
335 | break; |
336 | default: |
337 | $this->doShowProcessForm( $req ); |
338 | break; |
339 | } |
340 | } |
341 | |
342 | private function showUnkownRequest() { |
343 | $this->commonPreamble( 'globalrenamequeue-request-unknown-title' ); |
344 | $this->getOutput()->addWikiMsg( |
345 | 'globalrenamequeue-request-unknown-body' |
346 | ); |
347 | } |
348 | |
349 | protected function doRedirectToOpenQueue() { |
350 | $this->getOutput()->redirect( |
351 | $this->getPageTitle( self::PAGE_OPEN_QUEUE )->getFullURL() |
352 | ); |
353 | } |
354 | |
355 | /** |
356 | * Display a request. |
357 | * |
358 | * @param GlobalRenameRequest $req |
359 | */ |
360 | protected function doViewRequest( GlobalRenameRequest $req ) { |
361 | $isVanishRequest = $req->getType() === GlobalRenameRequest::VANISH; |
362 | |
363 | if ( $isVanishRequest ) { |
364 | $this->commonPreamble( |
365 | 'globalrenamequeue-request-vanish-status-title', |
366 | [ $req->getName() ] |
367 | ); |
368 | } else { |
369 | $this->commonPreamble( |
370 | 'globalrenamequeue-request-status-title', |
371 | [ $req->getName(), $req->getNewName() ] |
372 | ); |
373 | } |
374 | |
375 | $reason = $req->getReason() ?: ''; |
376 | |
377 | $renamer = CentralAuthUser::newFromId( $req->getPerformer() ); |
378 | if ( $renamer === false ) { |
379 | throw new RuntimeException( |
380 | "The performer's global user id ({$req->getPerformer()}) " . |
381 | "does not exist in the database" |
382 | ); |
383 | } |
384 | $homewiki = $renamer->getHomeWiki(); |
385 | if ( $renamer->isAttached() || $homewiki === null ) { |
386 | $renamerLink = Title::makeTitleSafe( NS_USER, $renamer->getName() )->getFullURL(); |
387 | } else { |
388 | $renamerLink = WikiMap::getForeignURL( $homewiki, 'User:' . $renamer->getName() ); |
389 | } |
390 | |
391 | if ( strpos( $reason, "\n" ) !== false ) { |
392 | $reason = "<dl><dd>" . str_replace( "\n", "</dd><dd>", $reason ) . "</dd></dl>"; |
393 | } else { |
394 | $reason = ': ' . $reason; |
395 | } |
396 | |
397 | $status = $req->getStatus(); |
398 | $causerName = $status === GlobalRenameRequest::APPROVED |
399 | ? $req->getNewName() |
400 | : $req->getName(); |
401 | |
402 | $causer = CentralAuthUser::getInstanceByName( $causerName ); |
403 | $attachedWikis = $causer->exists() && $causer->isAttached() |
404 | ? implode( ', ', array_keys( $causer->queryAttachedBasic() ) ) |
405 | : ''; |
406 | |
407 | // Done as one big message so that admins can create a local |
408 | // translation to customize the output as they see fit. |
409 | // @TODO: Do that actually in here... this is not how we do interfaces in 2015. |
410 | if ( $isVanishRequest ) { |
411 | $viewMsg = $this->msg( 'globalrenamequeue-vanish-view', |
412 | $req->getName(), |
413 | $reason, |
414 | $this->msg( 'globalrenamequeue-view-' . $status )->text(), |
415 | $this->getLanguage()->userTimeAndDate( |
416 | $req->getRequested(), $this->getUser() |
417 | ), |
418 | $this->getLanguage()->userTimeAndDate( |
419 | $req->getCompleted(), $this->getUser() |
420 | ), |
421 | $attachedWikis, |
422 | $renamerLink, |
423 | $renamer->getName(), |
424 | $req->getComments() |
425 | )->parseAsBlock(); |
426 | } else { |
427 | $viewMsg = $this->msg( 'globalrenamequeue-view', |
428 | $req->getName(), |
429 | $req->getNewName(), |
430 | $reason, |
431 | $this->msg( 'globalrenamequeue-view-' . $status )->text(), |
432 | $this->getLanguage()->userTimeAndDate( |
433 | $req->getRequested(), $this->getUser() |
434 | ), |
435 | $this->getLanguage()->userTimeAndDate( |
436 | $req->getCompleted(), $this->getUser() |
437 | ), |
438 | $renamerLink, |
439 | $renamer->getName(), |
440 | $req->getComments() |
441 | )->parseAsBlock(); |
442 | } |
443 | |
444 | $this->getOutput()->addHtml( '<div class="plainlinks">' . $viewMsg . '</div>' ); |
445 | } |
446 | |
447 | /** |
448 | * Display form for approving/denying request or process form submission. |
449 | * |
450 | * @param GlobalRenameRequest $req Pending request |
451 | */ |
452 | protected function doShowProcessForm( GlobalRenameRequest $req ) { |
453 | $isVanishRequest = $req->getType() === GlobalRenameRequest::VANISH; |
454 | |
455 | // Set up message keys according to request type (rename/vanish) |
456 | if ( $isVanishRequest ) { |
457 | $commonPreambleMsg = 'globalrenamequeue-request-vanish-title'; |
458 | $approveButtonMsg = 'globalrenamequeue-request-vanish-approve-text'; |
459 | $denyButtonMsg = 'globalrenamequeue-request-vanish-deny-text'; |
460 | $globalUserInfoMsg = 'globalrenamequeue-request-vanish-userinfo'; |
461 | $headerMsgKey = 'globalrenamequeue-request-vanish-header'; |
462 | $reasonMsg = 'globalrenamequeue-request-vanish-reason'; |
463 | $approveConfirmation = 'mw-renamequeue-approve-vanish'; |
464 | } else { |
465 | $commonPreambleMsg = 'globalrenamequeue-request-title'; |
466 | $approveButtonMsg = 'globalrenamequeue-request-approve-text'; |
467 | $denyButtonMsg = 'globalrenamequeue-request-deny-text'; |
468 | $globalUserInfoMsg = 'globalrenamequeue-request-userinfo-global'; |
469 | $headerMsgKey = 'globalrenamequeue-request-header'; |
470 | $reasonMsg = 'globalrenamequeue-request-reason'; |
471 | $approveConfirmation = 'mw-renamequeue-approve'; |
472 | } |
473 | |
474 | $this->commonPreamble( $commonPreambleMsg, [ $req->getName() ] ); |
475 | |
476 | $htmlForm = HTMLForm::factory( 'ooui', |
477 | [ |
478 | 'rid' => [ |
479 | 'default' => $req->getId(), |
480 | 'name' => 'rid', |
481 | 'type' => 'hidden', |
482 | ], |
483 | 'comments' => [ |
484 | 'default' => $this->getRequest()->getVal( 'comments' ), |
485 | 'id' => 'mw-renamequeue-comments', |
486 | 'label-message' => 'globalrenamequeue-request-comments-label', |
487 | 'name' => 'comments', |
488 | 'type' => 'textarea', |
489 | 'rows' => 5, |
490 | ], |
491 | ], |
492 | $this->getContext(), |
493 | 'globalrenamequeue' |
494 | ); |
495 | |
496 | // Show tools to approve only when user is not reviewing own request. |
497 | if ( $req->getName() !== $this->getUser()->getName() ) { |
498 | $htmlForm |
499 | ->addFields( [ |
500 | // The following fields need to have their names stay in |
501 | // sync with the expectations of GlobalRenameUser::rename() |
502 | 'reason' => [ |
503 | 'id' => 'mw-renamequeue-reason', |
504 | 'label-message' => 'globalrenamequeue-request-reason-label', |
505 | 'name' => 'reason', |
506 | 'type' => 'text', |
507 | ], |
508 | 'movepages' => [ |
509 | 'id' => 'mw-renamequeue-movepages', |
510 | 'name' => 'movepages', |
511 | 'label-message' => 'globalrenamequeue-request-movepages', |
512 | 'type' => 'check', |
513 | 'default' => 1, |
514 | ], |
515 | 'suppressredirects' => [ |
516 | 'id' => 'mw-renamequeue-suppressredirects', |
517 | 'name' => 'suppressredirects', |
518 | 'label-message' => 'globalrenamequeue-request-suppressredirects', |
519 | 'type' => 'check', |
520 | 'default' => $isVanishRequest ? 1 : 0, |
521 | 'disabled' => $isVanishRequest ? 1 : 0, |
522 | ], |
523 | ] ) |
524 | ->addButton( [ |
525 | 'name' => 'approve', |
526 | 'value' => $this->msg( $approveButtonMsg )->text(), |
527 | 'id' => $approveConfirmation, |
528 | 'flags' => [ 'primary', 'progressive' ], |
529 | 'framed' => true |
530 | ] ); |
531 | } |
532 | |
533 | $htmlForm |
534 | ->suppressDefaultSubmit() |
535 | ->addButton( [ |
536 | 'name' => 'deny', |
537 | 'value' => $this->msg( $denyButtonMsg )->text(), |
538 | 'id' => 'mw-renamequeue-deny', |
539 | 'flags' => [ 'destructive' ], |
540 | 'framed' => true |
541 | ] ) |
542 | ->addButton( [ |
543 | 'name' => 'cancel', |
544 | 'value' => $this->msg( 'globalrenamequeue-request-cancel-text' )->text(), |
545 | 'id' => 'mw-renamequeue-cancel', |
546 | ] ) |
547 | ->setId( 'mw-globalrenamequeue-request' ); |
548 | |
549 | if ( $req->userIsGlobal() ) { |
550 | $globalUser = CentralAuthUser::getInstanceByName( $req->getName() ); |
551 | $homeWiki = $globalUser->getHomeWiki(); |
552 | $infoMsgKey = $globalUserInfoMsg; |
553 | } else { |
554 | $homeWiki = $req->getWiki(); |
555 | $infoMsgKey = 'globalrenamequeue-request-userinfo-local'; |
556 | } |
557 | |
558 | if ( $homeWiki === null ) { |
559 | $homeLink = Title::makeTitleSafe( NS_USER, $req->getName() )->getFullURL(); |
560 | } else { |
561 | $homeLink = WikiMap::getForeignURL( $homeWiki, 'User:' . $req->getName() ); |
562 | } |
563 | |
564 | $headerMsg = $this->msg( |
565 | $headerMsgKey, |
566 | $homeLink, |
567 | $req->getName(), |
568 | $req->getNewName() |
569 | ); |
570 | $htmlForm->addHeaderHtml( '<span class="plainlinks">' . $headerMsg->parseAsBlock() . |
571 | '</span>' ); |
572 | |
573 | $homeWikiWiki = $homeWiki ? WikiMap::getWiki( $homeWiki ) : null; |
574 | $infoMsgArgs = [ |
575 | $infoMsgKey, |
576 | $req->getName(), |
577 | // homeWikiWiki shouldn't ever be null except in |
578 | // a development/testing environment. |
579 | ( $homeWikiWiki ? $homeWikiWiki->getDisplayName() : $homeWiki ), |
580 | ]; |
581 | // Rename requests need the new username into the info message |
582 | if ( !$isVanishRequest ) { |
583 | $infoMsgArgs[] = $req->getNewName(); |
584 | } |
585 | $infoMsg = $this->msg( ...$infoMsgArgs ); |
586 | |
587 | if ( isset( $globalUser ) ) { |
588 | $infoMsg->numParams( $globalUser->getGlobalEditCount() ); |
589 | $infoMsg->params( $this->msg( |
590 | $globalUser->isBlocked() ? |
591 | 'globalrenamequeue-request-vanish-user-blocked' : |
592 | 'globalrenamequeue-request-vanish-user-not-blocked' |
593 | ) ); |
594 | } |
595 | |
596 | $htmlForm->addHeaderHtml( $infoMsg->parseAsBlock() ); |
597 | |
598 | // Handle AntiSpoof integration |
599 | $spoofUser = $this->caAntiSpoofManager->getSpoofUser( $req->getNewName() ); |
600 | $conflicts = $this->uiService->processAntiSpoofConflicts( |
601 | $this->getContext(), |
602 | $req->getName(), |
603 | $spoofUser->getConflicts() |
604 | ); |
605 | $renamedUser = $this->caAntiSpoofManager->getOldRenamedUserName( $req->getNewName() ); |
606 | if ( $renamedUser !== null ) { |
607 | $conflicts[] = $renamedUser; |
608 | } |
609 | if ( $conflicts ) { |
610 | $htmlForm->addHeaderHtml( |
611 | $this->msg( |
612 | 'globalrenamequeue-request-antispoof-conflicts', |
613 | $this->getLanguage()->commaList( $conflicts ) |
614 | )->numParams( count( $conflicts ) )->parseAsBlock() |
615 | ); |
616 | } |
617 | |
618 | // Show a message if the new username matches the title blacklist. |
619 | if ( ExtensionRegistry::getInstance()->isLoaded( 'TitleBlacklist' ) ) { |
620 | $titleBlacklist = TitleBlacklist::singleton()->isBlacklisted( |
621 | Title::makeTitleSafe( NS_USER, $req->getNewName() ), |
622 | 'new-account' |
623 | ); |
624 | if ( $titleBlacklist instanceof TitleBlacklistEntry ) { |
625 | $htmlForm->addHeaderHtml( |
626 | $this->msg( 'globalrenamequeue-request-titleblacklist' ) |
627 | ->params( wfEscapeWikiText( $titleBlacklist->getRegex() ) )->parseAsBlock() |
628 | ); |
629 | } |
630 | } |
631 | |
632 | // Show a log entry of previous renames under the requesting user's username |
633 | $caTitle = SpecialPage::getTitleFor( 'CentralAuth', $req->getName() ); |
634 | $extract = ''; |
635 | $extractCount = LogEventsList::showLogExtract( $extract, 'gblrename', $caTitle, '', [ |
636 | 'showIfEmpty' => false, |
637 | ] ); |
638 | if ( $extractCount ) { |
639 | $htmlForm->addHeaderHtml( |
640 | Xml::fieldset( $this->msg( 'globalrenamequeue-request-previous-renames' ) |
641 | ->numParams( $extractCount ) |
642 | ->text(), $extract ) |
643 | ); |
644 | } |
645 | |
646 | $reason = $req->getReason() ?: ''; |
647 | |
648 | $htmlForm->addHeaderHtml( $this->msg( $reasonMsg, |
649 | "<dl><dd>" . str_replace( "\n", "</dd><dd>", $reason ) . "</dd></dl>" |
650 | )->parseAsBlock() ); |
651 | |
652 | // Show warning when reviewing own request |
653 | if ( $req->getName() === $this->getUser()->getName() ) { |
654 | $message = new MessageWidget( [ |
655 | 'label' => $this->msg( 'globalrenamerequest-self-warning' )->text(), |
656 | 'type' => 'warning', |
657 | 'inline' => true |
658 | ] ); |
659 | $htmlForm->addHeaderHtml( $message->toString() ); |
660 | } |
661 | |
662 | $htmlForm->setSubmitCallback( [ $this, 'onProcessSubmit' ] ); |
663 | |
664 | $out = $this->getOutput(); |
665 | $out->addModuleStyles( 'ext.centralauth.globalrenamequeue.styles' ); |
666 | $out->addModules( 'ext.centralauth.globalrenamequeue' ); |
667 | |
668 | $status = $htmlForm->show(); |
669 | if ( $status instanceof Status && $status->isOK() ) { |
670 | $this->getOutput()->redirect( |
671 | $this->getPageTitle( |
672 | self::PAGE_PROCESS_REQUEST . "/{$req->getId()}/{$status->value}" |
673 | )->getFullURL() |
674 | ); |
675 | } |
676 | } |
677 | |
678 | /** |
679 | * @param array $data |
680 | * @return Status |
681 | */ |
682 | public function onProcessSubmit( array $data ) { |
683 | $request = $this->getContext()->getRequest(); |
684 | $status = new Status; |
685 | if ( $request->getCheck( 'approve' ) ) { |
686 | $status = $this->doResolveRequest( true, $data ); |
687 | } elseif ( $request->getCheck( 'deny' ) ) { |
688 | $status = $this->doResolveRequest( false, $data ); |
689 | } else { |
690 | $status->setResult( true, 'cancel' ); |
691 | } |
692 | return $status; |
693 | } |
694 | |
695 | /** |
696 | * @param bool $approved |
697 | * @param array $data |
698 | * |
699 | * @return Status |
700 | */ |
701 | protected function doResolveRequest( $approved, $data ) { |
702 | $request = $this->globalRenameRequestStore->newFromId( $data['rid'] ); |
703 | $oldUser = User::newFromName( $request->getName() ); |
704 | |
705 | $newUser = User::newFromName( $request->getNewName(), 'creatable' ); |
706 | $status = new Status; |
707 | $session = $this->getContext()->exportSession(); |
708 | if ( $approved ) { |
709 | // Disallow self-renaming |
710 | if ( $request->getName() === $this->getUser()->getName() ) { |
711 | return Status::newFatal( 'globalrenamerequest-self-error' ); |
712 | } |
713 | |
714 | if ( $request->userIsGlobal() ) { |
715 | $data['type'] = $request->getType(); |
716 | |
717 | $globalRenameUser = $this->globalRenameFactory |
718 | ->newGlobalRenameUser( |
719 | $this->getUser(), |
720 | CentralAuthUser::getInstanceByName( $request->getName() ), |
721 | $request->getNewName() |
722 | ) |
723 | ->withSession( $session ); |
724 | |
725 | // Credit the vanish performer with account locking logs for |
726 | // vanish requests. Renamers cannot normally perform locks and |
727 | // thus should not be associated with them. |
728 | if ( $request->getType() === GlobalRenameRequest::VANISH ) { |
729 | $vanishPerformerName = $this->getConfig() |
730 | ->get( CAMainConfigNames::CentralAuthAutomaticVanishPerformer ); |
731 | |
732 | if ( $vanishPerformerName !== null ) { |
733 | $localVanishPerformer = $this->userIdentityLookup |
734 | ->getUserIdentityByName( $vanishPerformerName ); |
735 | |
736 | if ( $localVanishPerformer !== null ) { |
737 | $globalRenameUser = $globalRenameUser->withLockPerformingUser( $localVanishPerformer ); |
738 | } |
739 | } |
740 | } |
741 | |
742 | // Trigger a global rename job |
743 | $status = $globalRenameUser->rename( $data ); |
744 | } else { |
745 | // If the user is local-only: |
746 | // * rename the local user using LocalRenameUserJob |
747 | // * create a global user attached only to the local wiki |
748 | $job = new LocalRenameUserJob( |
749 | Title::newFromText( 'Global rename job' ), |
750 | [ |
751 | 'from' => $oldUser->getName(), |
752 | 'to' => $newUser->getName(), |
753 | 'renamer' => $this->getUser()->getName(), |
754 | 'movepages' => true, |
755 | 'suppressredirects' => true, |
756 | 'promotetoglobal' => true, |
757 | 'reason' => $data['reason'], |
758 | 'session' => $session, |
759 | 'type' => $request->getType(), |
760 | ] |
761 | ); |
762 | $this->jobQueueGroupFactory->makeJobQueueGroup( $request->getWiki() )->push( $job ); |
763 | // Now log it |
764 | $this->logPromotionRename( |
765 | $oldUser->getName(), |
766 | $request->getWiki(), |
767 | $newUser->getName(), |
768 | $data['reason'] |
769 | ); |
770 | $status = Status::newGood(); |
771 | } |
772 | } |
773 | |
774 | if ( $status->isGood() ) { |
775 | $request->setStatus( |
776 | $approved ? GlobalRenameRequest::APPROVED : GlobalRenameRequest::REJECTED |
777 | ); |
778 | $request->setCompleted( wfTimestampNow() ); |
779 | $request->setPerformer( |
780 | CentralAuthUser::getInstance( $this->getUser() )->getId() |
781 | ); |
782 | $request->setComments( $data['comments'] ); |
783 | |
784 | if ( $this->globalRenameRequestStore->save( $request ) ) { |
785 | if ( $request->getType() === GlobalRenameRequest::VANISH ) { |
786 | $emailSubjectApprovedMsg = 'globalrenamequeue-vanish-email-subject-approved'; |
787 | $emailBodyApprovedMsg = 'globalrenamequeue-vanish-email-body-approved'; |
788 | $emailBodyApprovedWithNoteMsg = 'globalrenamequeue-vanish-email-body-approved-with-note'; |
789 | $emailSubjectRejectedMsg = 'globalrenamequeue-vanish-email-subject-rejected'; |
790 | $emailBodyRejectedMsg = 'globalrenamequeue-vanish-email-body-rejected'; |
791 | $emailBodyMsgArgs = [ |
792 | $oldUser->getName(), |
793 | $request->getComments() |
794 | ]; |
795 | } else { |
796 | $emailSubjectApprovedMsg = 'globalrenamequeue-email-subject-approved'; |
797 | $emailBodyApprovedMsg = 'globalrenamequeue-email-body-approved'; |
798 | $emailBodyApprovedWithNoteMsg = 'globalrenamequeue-email-body-approved-with-note'; |
799 | $emailSubjectRejectedMsg = 'globalrenamequeue-email-subject-rejected'; |
800 | $emailBodyRejectedMsg = 'globalrenamequeue-email-body-rejected'; |
801 | $emailBodyMsgArgs = [ |
802 | $oldUser->getName(), |
803 | $newUser->getName(), |
804 | $request->getComments(), |
805 | ]; |
806 | } |
807 | |
808 | // Send email to the user about the change in status. |
809 | if ( $approved ) { |
810 | $subject = $this->msg( |
811 | $emailSubjectApprovedMsg |
812 | )->inContentLanguage()->text(); |
813 | if ( $request->getComments() === '' ) { |
814 | $msgKey = $emailBodyApprovedMsg; |
815 | } else { |
816 | $msgKey = $emailBodyApprovedWithNoteMsg; |
817 | } |
818 | $body = $this->msg( |
819 | $msgKey, $emailBodyMsgArgs |
820 | )->inContentLanguage()->text(); |
821 | } else { |
822 | $recipientConfig = $this->getConfig() |
823 | ->get( CAMainConfigNames::CentralAuthRejectVanishUserNotification ); |
824 | if ( $recipientConfig ) { |
825 | $status = $this->sendEmailForRejectionOfVanishRequest( $request, $recipientConfig ); |
826 | } |
827 | |
828 | $subject = $this->msg( |
829 | $emailSubjectRejectedMsg |
830 | )->inContentLanguage()->text(); |
831 | $body = $this->msg( |
832 | $emailBodyRejectedMsg, $emailBodyMsgArgs |
833 | )->inContentLanguage()->text(); |
834 | } |
835 | |
836 | if ( $request->userIsGlobal() || $request->getWiki() === WikiMap::getCurrentWikiId() ) { |
837 | $notifyEmail = MailAddress::newFromUser( $oldUser ); |
838 | } else { |
839 | $notifyEmail = $this->getRemoteUserMailAddress( |
840 | $request->getWiki(), $request->getName() |
841 | ); |
842 | } |
843 | |
844 | if ( $notifyEmail !== null && $notifyEmail->address ) { |
845 | $type = $approved ? 'approval' : 'rejection'; |
846 | $this->logger->info( "Send $type email to User:{oldName}", [ |
847 | 'oldName' => $oldUser->getName(), |
848 | 'component' => 'GlobalRename', |
849 | ] ); |
850 | $this->sendNotificationEmail( $notifyEmail, $subject, $body ); |
851 | } |
852 | } else { |
853 | $status->fatal( 'globalrenamequeue-request-savefailed' ); |
854 | } |
855 | } |
856 | return $status; |
857 | } |
858 | |
859 | /** |
860 | * Log a promotion to global rename in the global rename log |
861 | * |
862 | * @param string $oldName |
863 | * @param string $wiki |
864 | * @param string $newName |
865 | * @param string $reason |
866 | */ |
867 | protected function logPromotionRename( $oldName, $wiki, $newName, $reason ) { |
868 | $logger = new GlobalRenameUserLogger( $this->getUser() ); |
869 | $logger->logPromotion( $oldName, $wiki, $newName, $reason ); |
870 | } |
871 | |
872 | /** |
873 | * Get a MailAddress for a user on a remote wiki |
874 | * |
875 | * @param string $wiki |
876 | * @param string $username |
877 | * @return MailAddress|null |
878 | */ |
879 | protected function getRemoteUserMailAddress( $wiki, $username ) { |
880 | $lb = $this->lbFactory->getMainLB( $wiki ); |
881 | $remoteDB = $lb->getConnection( DB_REPLICA, [], $wiki ); |
882 | $row = $remoteDB->newSelectQueryBuilder() |
883 | ->select( [ 'user_email', 'user_name', 'user_real_name' ] ) |
884 | ->from( 'user' ) |
885 | ->where( [ |
886 | 'user_name' => $this->userNameUtils->getCanonical( $username ), |
887 | ] ) |
888 | ->caller( __METHOD__ ) |
889 | ->fetchRow(); |
890 | if ( $row === false ) { |
891 | $address = null; |
892 | } else { |
893 | $address = new MailAddress( |
894 | $row->user_email, $row->user_name, $row->user_real_name |
895 | ); |
896 | } |
897 | return $address; |
898 | } |
899 | |
900 | /** |
901 | * Send an email notifying the user of the result of their request. |
902 | * |
903 | * @param MailAddress $to |
904 | * @param string $subject |
905 | * @param string $body |
906 | * @return Status |
907 | */ |
908 | protected function sendNotificationEmail( MailAddress $to, $subject, $body ) { |
909 | $from = new MailAddress( |
910 | $this->getConfig()->get( MainConfigNames::PasswordSender ), |
911 | $this->msg( 'emailsender' )->inContentLanguage()->text() |
912 | ); |
913 | return UserMailer::send( $to, $from, $subject, $body ); |
914 | } |
915 | |
916 | /** |
917 | * Send an email on account vanishing rejection to the provided recipient. |
918 | * |
919 | * @param GlobalRenameRequest $request |
920 | * @param string $recipientUserName |
921 | * @return Status |
922 | */ |
923 | protected function sendEmailForRejectionOfVanishRequest( GlobalRenameRequest $request, $recipientUserName ) { |
924 | // Email to legal is only sent when it's a vanish request |
925 | if ( $request->getType() !== GlobalRenameRequest::VANISH ) { |
926 | return Status::newGood(); |
927 | } |
928 | |
929 | if ( $request->userIsGlobal() ) { |
930 | $globalUser = CentralAuthUser::getInstanceByName( $request->getName() ); |
931 | $homeWiki = $globalUser->getHomeWiki(); |
932 | $globalEditCount = $globalUser->getGlobalEditCount(); |
933 | $isBlocked = $globalUser->isBlocked() ? |
934 | 'globalrenamequeue-request-vanish-user-blocked' : |
935 | 'globalrenamequeue-request-vanish-user-not-blocked'; |
936 | } else { |
937 | $homeWiki = $request->getWiki(); |
938 | $globalEditCount = ''; |
939 | $isBlocked = 'globalrenamequeue-request-vanish-user-not-blocked'; |
940 | } |
941 | // This should never be null except in dev/testing environment. |
942 | $homeWikiWiki = $homeWiki ? WikiMap::getWiki( $homeWiki ) : null; |
943 | $rejector = CentralAuthUser::newFromId( $request->getPerformer() ); |
944 | $rejectorName = $rejector ? $rejector->getName() : $request->getPerformer(); |
945 | |
946 | $subject = $this->msg( |
947 | 'globalvanishrequest-rejected-subject-notification', |
948 | $request->getName(), |
949 | )->inContentLanguage()->text(); |
950 | $isBlockedMsg = $this->msg( $isBlocked )->inContentLanguage()->text(); |
951 | $bodyMessage = $this->msg( |
952 | 'globalvanishrequest-rejected-body-notification', |
953 | $request->getName(), |
954 | ( $homeWikiWiki ? $homeWikiWiki->getDisplayName() : $homeWiki ), |
955 | $globalEditCount, |
956 | $isBlockedMsg, |
957 | $request->getReason(), |
958 | $rejectorName, |
959 | $request->getComments(), |
960 | $this->getLanguage()->userTimeAndDate( |
961 | $request->getRequested(), $this->getUser() |
962 | ), |
963 | $this->getLanguage()->userTimeAndDate( |
964 | $request->getCompleted(), $this->getUser() |
965 | ), |
966 | )->inContentLanguage()->text(); |
967 | |
968 | try { |
969 | $contactRecipientUser = User::newFromName( $recipientUserName ); |
970 | $contactRecipientAddress = MailAddress::newFromUser( $contactRecipientUser ); |
971 | $contactSenderAddress = new MailAddress( |
972 | $this->getConfig()->get( MainConfigNames::PasswordSender ), |
973 | $this->msg( 'emailsender' )->inContentLanguage()->text() |
974 | ); |
975 | |
976 | $userMailerStatus = UserMailer::send( |
977 | $contactRecipientAddress, |
978 | $contactSenderAddress, |
979 | $subject, |
980 | $bodyMessage, |
981 | ); |
982 | if ( $userMailerStatus->isGood() ) { |
983 | return Status::newGood(); |
984 | } else { |
985 | return Status::newFatal( 'globalvanishrequest-rejected-notification-error' ); |
986 | } |
987 | } catch ( Exception $e ) { |
988 | return Status::newFatal( 'globalvanishrequest-rejected-notification-error' ); |
989 | } |
990 | } |
991 | |
992 | /** @inheritDoc */ |
993 | protected function getGroupName() { |
994 | return 'users'; |
995 | } |
996 | |
997 | /** @inheritDoc */ |
998 | public function getSubpagesForPrefixSearch() { |
999 | return [ |
1000 | self::PAGE_OPEN_QUEUE, |
1001 | self::PAGE_PROCESS_REQUEST, |
1002 | self::PAGE_CLOSED_QUEUE |
1003 | ]; |
1004 | } |
1005 | } |