Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 459
0.00% covered (danger)
0.00%
0 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialGlobalRenameQueue
0.00% covered (danger)
0.00%
0 / 459
0.00% covered (danger)
0.00%
0 / 23
5700
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 commonPreamble
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getAssociatedNavigationLinks
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getShortDescription
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 addSubtitleLinks
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 getCommonFormFieldsArray
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
2
 outputFilterForm
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 handleOpenQueue
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 handleClosedQueue
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
2
 handleProcessRequest
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
72
 showUnkownRequest
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 doRedirectToOpenQueue
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 doViewRequest
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
42
 doShowProcessForm
0.00% covered (danger)
0.00%
0 / 153
0.00% covered (danger)
0.00%
0 / 1
272
 onProcessSubmit
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 doResolveRequest
0.00% covered (danger)
0.00%
0 / 88
0.00% covered (danger)
0.00%
0 / 1
210
 logPromotionRename
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getRemoteUserMailAddress
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 sendNotificationEmail
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSubpagesForPrefixSearch
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
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
23namespace MediaWiki\Extension\CentralAuth\Special;
24
25use ExtensionRegistry;
26use HTMLForm;
27use LogEventsList;
28use MailAddress;
29use MediaWiki\Extension\CentralAuth\CentralAuthDatabaseManager;
30use MediaWiki\Extension\CentralAuth\CentralAuthUIService;
31use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameFactory;
32use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameRequest;
33use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameRequestStore;
34use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameUserLogger;
35use MediaWiki\Extension\CentralAuth\GlobalRename\LocalRenameJob\LocalRenameUserJob;
36use MediaWiki\Extension\CentralAuth\User\CentralAuthAntiSpoofManager;
37use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
38use MediaWiki\Extension\TitleBlacklist\TitleBlacklist;
39use MediaWiki\Extension\TitleBlacklist\TitleBlacklistEntry;
40use MediaWiki\JobQueue\JobQueueGroupFactory;
41use MediaWiki\Logger\LoggerFactory;
42use MediaWiki\SpecialPage\SpecialPage;
43use MediaWiki\Status\Status;
44use MediaWiki\Title\Title;
45use MediaWiki\User\User;
46use MediaWiki\User\UserNameUtils;
47use MediaWiki\WikiMap\WikiMap;
48use OOUI\MessageWidget;
49use RuntimeException;
50use UserMailer;
51use Wikimedia\Rdbms\LBFactory;
52use 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 */
61class 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}