Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 600
0.00% covered (danger)
0.00%
0 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialGlobalRenameQueue
0.00% covered (danger)
0.00%
0 / 600
0.00% covered (danger)
0.00%
0 / 24
9900
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 11
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 / 37
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 / 64
0.00% covered (danger)
0.00%
0 / 1
132
 doShowProcessForm
0.00% covered (danger)
0.00%
0 / 176
0.00% covered (danger)
0.00%
0 / 1
462
 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 / 112
0.00% covered (danger)
0.00%
0 / 1
380
 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
 sendEmailForRejectionOfVanishRequest
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 1
90
 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 Exception;
26use LogEventsList;
27use MailAddress;
28use MediaWiki\Extension\CentralAuth\CentralAuthDatabaseManager;
29use MediaWiki\Extension\CentralAuth\CentralAuthUIService;
30use MediaWiki\Extension\CentralAuth\Config\CAMainConfigNames;
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\HTMLForm\HTMLForm;
41use MediaWiki\JobQueue\JobQueueGroupFactory;
42use MediaWiki\Logger\LoggerFactory;
43use MediaWiki\MainConfigNames;
44use MediaWiki\Registration\ExtensionRegistry;
45use MediaWiki\SpecialPage\SpecialPage;
46use MediaWiki\Status\Status;
47use MediaWiki\Title\Title;
48use MediaWiki\User\User;
49use MediaWiki\User\UserIdentityLookup;
50use MediaWiki\User\UserNameUtils;
51use MediaWiki\WikiMap\WikiMap;
52use MediaWiki\Xml\Xml;
53use OOUI\MessageWidget;
54use Psr\Log\LoggerInterface;
55use RuntimeException;
56use UserMailer;
57use 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 */
66class 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}