Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.41% covered (warning)
83.41%
538 / 645
36.36% covered (danger)
36.36%
8 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialBlock
83.54% covered (warning)
83.54%
538 / 644
36.36% covered (danger)
36.36%
8 / 22
264.52
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRestriction
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDescription
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 execute
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
6
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 checkExecutePermissions
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 requiresUnblock
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setParameter
82.61% covered (warning)
82.61%
38 / 46
0.00% covered (danger)
0.00%
0 / 1
19.70
 alterForm
27.78% covered (danger)
27.78%
10 / 36
0.00% covered (danger)
0.00%
0 / 1
25.46
 getDisplayFormat
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 getFormFields
96.88% covered (success)
96.88%
217 / 224
0.00% covered (danger)
0.00%
0 / 1
19
 validateTarget
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
8.01
 getExpiryLabel
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
3.07
 maybeAlterFormDefaults
81.82% covered (warning)
81.82%
54 / 66
0.00% covered (danger)
0.00%
0 / 1
30.06
 formatExpiryForHtml
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 preHtml
48.39% covered (danger)
48.39%
15 / 31
0.00% covered (danger)
0.00%
0 / 1
10.95
 postHtml
78.08% covered (warning)
78.08%
57 / 73
0.00% covered (danger)
0.00%
0 / 1
6.38
 getTargetInternal
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
5
 onSubmit
90.54% covered (success)
90.54%
67 / 74
0.00% covered (danger)
0.00%
0 / 1
33.92
 onSuccess
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 prefixSearchSubpages
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Specials;
8
9use MediaWiki\Block\AnonIpBlockTarget;
10use MediaWiki\Block\BlockActionInfo;
11use MediaWiki\Block\BlockPermissionCheckerFactory;
12use MediaWiki\Block\BlockTarget;
13use MediaWiki\Block\BlockTargetFactory;
14use MediaWiki\Block\BlockTargetWithIp;
15use MediaWiki\Block\BlockTargetWithUserPage;
16use MediaWiki\Block\BlockUserFactory;
17use MediaWiki\Block\DatabaseBlock;
18use MediaWiki\Block\DatabaseBlockStore;
19use MediaWiki\Block\MultiblocksException;
20use MediaWiki\Block\RangeBlockTarget;
21use MediaWiki\Block\Restriction\ActionRestriction;
22use MediaWiki\Block\Restriction\NamespaceRestriction;
23use MediaWiki\Block\Restriction\PageRestriction;
24use MediaWiki\Block\UserBlockTarget;
25use MediaWiki\CommentStore\CommentStore;
26use MediaWiki\Exception\ErrorPageError;
27use MediaWiki\Html\Html;
28use MediaWiki\HTMLForm\HTMLForm;
29use MediaWiki\Logging\LogEventsList;
30use MediaWiki\MainConfigNames;
31use MediaWiki\Message\Message;
32use MediaWiki\Request\WebRequest;
33use MediaWiki\SpecialPage\FormSpecialPage;
34use MediaWiki\SpecialPage\SpecialPage;
35use MediaWiki\Status\Status;
36use MediaWiki\Title\NamespaceInfo;
37use MediaWiki\Title\Title;
38use MediaWiki\Title\TitleFormatter;
39use MediaWiki\User\User;
40use MediaWiki\User\UserNamePrefixSearch;
41use MediaWiki\User\UserNameUtils;
42use MediaWiki\Watchlist\WatchlistManager;
43use OOUI\FieldLayout;
44use OOUI\HtmlSnippet;
45use OOUI\LabelWidget;
46use OOUI\Widget;
47use Wikimedia\HtmlArmor\HtmlArmor;
48use Wikimedia\Message\MessageSpecifier;
49use Wikimedia\Timestamp\TimestampFormat as TS;
50
51/**
52 * Allow users with 'block' user right to block IPs and user accounts from
53 * editing pages and other actions.
54 *
55 * @ingroup SpecialPage
56 */
57class SpecialBlock extends FormSpecialPage {
58
59    /** @var BlockTarget|null User to be blocked, as passed either by parameter
60     * (url?wpTarget=Foo) or as subpage (Special:Block/Foo)
61     */
62    protected $target;
63
64    /** @var BlockTarget|null The previous block target */
65    protected $previousTarget;
66
67    /** @var bool Whether the previous submission of the form asked for HideUser */
68    protected $requestedHideUser;
69
70    /** @var bool */
71    protected $alreadyBlocked;
72
73    /**
74     * @var MessageSpecifier[]
75     */
76    protected $preErrors = [];
77
78    /**
79     * @var array <mixed,mixed> An associative array used to pass vars to Codex form
80     */
81    protected array $codexFormData = [];
82
83    public function __construct(
84        private readonly BlockTargetFactory $blockTargetFactory,
85        private readonly BlockPermissionCheckerFactory $blockPermissionCheckerFactory,
86        private readonly BlockUserFactory $blockUserFactory,
87        private readonly DatabaseBlockStore $blockStore,
88        private readonly UserNameUtils $userNameUtils,
89        private readonly UserNamePrefixSearch $userNamePrefixSearch,
90        private readonly BlockActionInfo $blockActionInfo,
91        private readonly TitleFormatter $titleFormatter,
92        private readonly NamespaceInfo $namespaceInfo,
93        private readonly WatchlistManager $watchlistManager
94    ) {
95        parent::__construct( 'Block' );
96    }
97
98    /** @inheritDoc */
99    public function getRestriction(): string {
100        return 'block';
101    }
102
103    public function getDescription(): Message {
104        return $this->msg( $this->getConfig()->get( MainConfigNames::EnableMultiBlocks )
105            ? 'block-manage-blocks' : 'block' );
106    }
107
108    /**
109     * @inheritDoc
110     */
111    public function execute( $par ) {
112        parent::execute( $par );
113
114        $this->getOutput()->addJsConfigVars(
115            'wgAutoCreateTempUserEnabled',
116            $this->getConfig()->get( 'AutoCreateTempUser' )['enabled'],
117        );
118
119        if ( $this->getConfig()->get( MainConfigNames::UseCodexSpecialBlock )
120            || $this->getRequest()->getBool( 'usecodex' )
121        ) {
122            // Ensure wgUseCodexSpecialBlock is set when ?usecodex=1 is used.
123            $this->codexFormData[ 'wgUseCodexSpecialBlock' ] = true;
124            $this->codexFormData[ 'blockEnableMultiblocks' ] =
125                $this->getConfig()->get( MainConfigNames::EnableMultiBlocks ) ||
126                $this->getRequest()->getBool( 'multiblocks' );
127            $this->codexFormData[ 'blockTargetUser' ] =
128                $this->target ? $this->target->toString() : null;
129            $this->codexFormData[ 'blockId' ] =
130                $this->target ? $this->getRequest()->getInt( 'id' ) : null;
131            $authority = $this->getAuthority();
132            $this->codexFormData[ 'blockShowSuppressLog' ] = $authority->isAllowed( 'suppressionlog' );
133            $this->codexFormData[ 'blockCanDeleteLogEntry' ] = $authority->isAllowed( 'deletelogentry' );
134            $this->codexFormData[ 'blockCanEditInterface' ] = $authority->isAllowed( 'editinterface' );
135            $this->codexFormData[ 'blockCIDRLimit' ] = $this->getConfig()->get( MainConfigNames::BlockCIDRLimit );
136            $this->getOutput()->addJsConfigVars( $this->codexFormData );
137        }
138    }
139
140    /**
141     * @inheritDoc
142     */
143    public function doesWrites() {
144        return true;
145    }
146
147    /**
148     * Check that the user can unblock themselves if they are trying to do so
149     *
150     * @param User $user
151     * @throws ErrorPageError
152     */
153    protected function checkExecutePermissions( User $user ) {
154        parent::checkExecutePermissions( $user );
155        if ( $this->target ) {
156            // T17810: blocked admins should have limited access here
157            $status = $this->blockPermissionCheckerFactory
158                ->newChecker( $user )
159                ->checkBlockPermissions( $this->target );
160            if ( $status !== true ) {
161                throw new ErrorPageError( 'badaccess', $status );
162            }
163        }
164    }
165
166    /**
167     * We allow certain special cases where user is blocked
168     *
169     * @return bool
170     */
171    public function requiresUnblock() {
172        return false;
173    }
174
175    /**
176     * Handle some magic here
177     *
178     * @param string $par
179     */
180    protected function setParameter( $par ) {
181        // Extract variables from the request.  Try not to get into a situation where we
182        // need to extract *every* variable from the form just for processing here, but
183        // there are legitimate uses for some variables
184        $request = $this->getRequest();
185        $this->target = $this->getTargetInternal( $par, $request );
186        if ( $this->target instanceof BlockTargetWithUserPage ) {
187            // Set the 'relevant user' in the skin, so it displays links like Contributions,
188            // User logs, UserRights, etc.
189            $this->getSkin()->setRelevantUser( $this->target->getUserIdentity() );
190        }
191
192        $this->previousTarget = $this->blockTargetFactory
193            ->newFromString( $request->getVal( 'wpPreviousTarget' ) );
194        $this->requestedHideUser = $request->getBool( 'wpHideUser' );
195
196        if ( $this->getConfig()->get( MainConfigNames::UseCodexSpecialBlock ) || $request->getBool( 'usecodex' ) ) {
197            // Parse wpExpiry param
198            $givenExpiry = $request->getVal( 'wpExpiry', '' );
199            if ( wfIsInfinity( $givenExpiry ) ) {
200                $this->codexFormData[ 'blockExpiryPreset' ] = 'infinite';
201            } else {
202                $expiry = date_parse( $givenExpiry );
203                $this->codexFormData[ 'blockExpiryPreset' ] = isset( $expiry[ 'relative' ] ) ?
204                    // Relative expiry (e.g. '1 week')
205                    $givenExpiry :
206                    // Absolute expiry, formatted for <input type="datetime-local">
207                    $this->formatExpiryForHtml( $request->getVal( 'wpExpiry', '' ) );
208            }
209
210            $this->codexFormData[ 'blockTypePreset' ] =
211                $request->getRawVal( 'wpEditingRestriction' ) === 'partial' ?
212                'partial' :
213                'sitewide';
214
215            $reasonPreset = $request->getVal( 'wpReason' );
216            $reasonOtherPreset = $request->getVal( 'wpReason-other' );
217            if ( $reasonPreset && $reasonOtherPreset ) {
218                $this->codexFormData[ 'blockReasonPreset' ] = $reasonPreset .
219                    $this->msg( 'colon-separator' )->text() . $reasonOtherPreset;
220            } else {
221                $this->codexFormData[ 'blockReasonPreset' ] =
222                    $reasonPreset ?: $reasonOtherPreset ?: '';
223            }
224
225            $this->codexFormData[ 'blockRemovalReasonPreset' ] = $request->getVal( 'wpRemovalReason' );
226            $blockAdditionalDetailsPreset = $blockDetailsPreset = [];
227
228            // Default is to always block account creation.
229            if ( $request->getBool( 'wpCreateAccount', true ) ) {
230                $blockDetailsPreset[] = 'wpCreateAccount';
231            }
232
233            if ( $request->getBool( 'wpDisableEmail' ) ) {
234                $blockDetailsPreset[] = 'wpDisableEmail';
235            }
236
237            if ( $request->getBool( 'wpDisableUTEdit' ) ) {
238                $blockDetailsPreset[] = 'wpDisableUTEdit';
239            }
240
241            if ( $request->getRawVal( 'wpAutoBlock' ) !== '0' ) {
242                $blockAdditionalDetailsPreset[] = 'wpAutoBlock';
243            }
244
245            if ( $request->getBool( 'wpWatch' ) ) {
246                $blockAdditionalDetailsPreset[] = 'wpWatch';
247            }
248
249            if ( $request->getBool( 'wpHideUser' ) ) {
250                $blockAdditionalDetailsPreset[] = 'wpHideUser';
251            }
252
253            if ( $request->getBool( 'wpHardBlock' ) ) {
254                $blockAdditionalDetailsPreset[] = 'wpHardBlock';
255            }
256
257            $this->codexFormData[ 'blockDetailsPreset' ] = $blockDetailsPreset;
258            $this->codexFormData[ 'blockAdditionalDetailsPreset' ] = $blockAdditionalDetailsPreset;
259            $this->codexFormData[ 'blockPageRestrictions' ] = $request->getVal( 'wpPageRestrictions' );
260            $this->codexFormData[ 'blockNamespaceRestrictions' ] = $request->getVal( 'wpNamespaceRestrictions' );
261        }
262    }
263
264    /**
265     * Customizes the HTMLForm a bit
266     */
267    protected function alterForm( HTMLForm $form ) {
268        $form->setHeaderHtml( '' );
269        $form->setSubmitDestructive();
270        $form->setId( 'mw-block-form' );
271
272        $msg = $this->alreadyBlocked ? 'ipb-change-block' : 'ipbsubmit';
273        $form->setSubmitTextMsg( $msg );
274
275        $useCodex = $this->getConfig()->get( MainConfigNames::UseCodexSpecialBlock )
276            || $this->getRequest()->getBool( 'usecodex' );
277
278        $this->addHelpLink( $useCodex ? 'Help:Manage blocks' : 'Help:Blocking users' );
279
280        // Don't need to do anything if the form has been posted, or if there were no pre-errors.
281        if ( $this->getRequest()->wasPosted() || !$this->preErrors ) {
282            return;
283        }
284
285        if ( $useCodex ) {
286            $this->codexFormData[ 'blockPreErrors' ] = array_map( function ( $errMsg ) {
287                return $this->msg( $errMsg )->parse();
288            }, $this->preErrors );
289
290            // Mimic Codex error messages later generated by SpecialBlock.vue
291            $form->addHeaderHtml(
292                Html::rawElement(
293                    'div',
294                    [ 'class' => 'mw-block-messages' ],
295                    array_reduce( $this->preErrors, function ( $carry, $errMsg ) {
296                        return $carry . Html::errorBox(
297                                $this->msg( $errMsg )->parse(),
298                                '',
299                                'cdx-message--inline'
300                            );
301                    }, '' )
302                )
303            );
304        } else {
305            // Mimic error messages normally generated by the form
306            $form->addHeaderHtml( (string)new FieldLayout(
307                new Widget( [] ),
308                [
309                    'align' => 'top',
310                    'errors' => array_map( function ( $errMsg ) {
311                        return new HtmlSnippet( $this->msg( $errMsg )->parse() );
312                    }, $this->preErrors ),
313                ]
314            ) );
315        }
316    }
317
318    /**
319     * @inheritDoc
320     */
321    protected function getDisplayFormat() {
322        return $this->getConfig()->get( MainConfigNames::UseCodexSpecialBlock ) ||
323            $this->getRequest()->getBool( 'usecodex' ) ? 'codex' : 'ooui';
324    }
325
326    /**
327     * Get the HTMLForm descriptor array for the block form
328     * @return array
329     */
330    protected function getFormFields() {
331        $conf = $this->getConfig();
332        $blockAllowsUTEdit = $conf->get( MainConfigNames::BlockAllowsUTEdit );
333        $useCodex = $conf->get( MainConfigNames::UseCodexSpecialBlock ) || $this->getRequest()->getBool( 'usecodex' );
334
335        if ( !$useCodex ) {
336            $this->getOutput()->enableOOUI();
337        }
338
339        $user = $this->getUser();
340
341        $suggestedDurations = $this->getLanguage()->getBlockDurations();
342
343        $a = [];
344
345        $a['Target'] = [
346            'type' => 'user',
347            'ipallowed' => true,
348            'iprange' => true,
349            'id' => 'mw-bi-target',
350            'size' => '45',
351            'autofocus' => true,
352            'required' => true,
353            'placeholder' => $this->msg( 'block-target-placeholder' )->text(),
354            'validation-callback' => function ( $value, $alldata, $form ) {
355                $status = $this->blockTargetFactory->newFromString( $value )->validateForCreation();
356                if ( !$status->isOK() ) {
357                    $errors = $status->getMessages();
358                    return $form->msg( $errors[0] );
359                }
360                return true;
361            },
362            'section' => 'target',
363        ];
364
365        $editingRestrictionOptions = $useCodex ?
366            // If we're using Codex, use the option-descriptions feature, which is only supported by Codex
367            [
368                'options-messages' => [
369                    'ipb-sitewide' => 'sitewide',
370                    'ipb-partial' => 'partial'
371                ],
372                'option-descriptions-messages' => [
373                    'sitewide' => 'ipb-sitewide-help',
374                    'partial' => 'ipb-partial-help'
375                ],
376                'option-descriptions-messages-parse' => true,
377            ] :
378            // Otherwise, if we're using OOUI, add the options' descriptions as part of their labels
379            [
380                'options' => [
381                    $this->msg( 'ipb-sitewide' )->escaped() .
382                        new LabelWidget( [
383                            'classes' => [ 'oo-ui-inline-help' ],
384                            'label' => new HtmlSnippet( $this->msg( 'ipb-sitewide-help' )->parse() ),
385                        ] ) => 'sitewide',
386                    $this->msg( 'ipb-partial' )->escaped() .
387                        new LabelWidget( [
388                            'classes' => [ 'oo-ui-inline-help' ],
389                            'label' => new HtmlSnippet( $this->msg( 'ipb-partial-help' )->parse() ),
390                        ] ) => 'partial',
391                ]
392            ];
393
394        $a['EditingRestriction'] = [
395            'type' => 'radio',
396            'cssclass' => 'mw-block-editing-restriction',
397            'default' => 'sitewide',
398            'section' => 'actions',
399        ] + $editingRestrictionOptions;
400
401        $a['PageRestrictions'] = [
402            'type' => 'titlesmultiselect',
403            'label' => $this->msg( 'ipb-pages-label' )->text(),
404            'exists' => true,
405            'max' => 10,
406            'cssclass' => 'mw-htmlform-checkradio-indent mw-block-partial-restriction',
407            'default' => '',
408            'showMissing' => false,
409            'creatable' => true,
410            'input' => [
411                'autocomplete' => false
412            ],
413            'section' => 'actions',
414        ];
415
416        $a['NamespaceRestrictions'] = [
417            'type' => 'namespacesmultiselect',
418            'label' => $this->msg( 'ipb-namespaces-label' )->text(),
419            'exists' => true,
420            'cssclass' => 'mw-htmlform-checkradio-indent mw-block-partial-restriction',
421            'default' => '',
422            'input' => [
423                'autocomplete' => false
424            ],
425            'section' => 'actions',
426        ];
427
428        $blockActions = $this->blockActionInfo->getAllBlockActions();
429        $optionMessages = array_combine(
430            array_map( static function ( $action ) {
431                return "ipb-action-$action";
432            }, array_keys( $blockActions ) ),
433            $blockActions
434        );
435
436        $this->codexFormData[ 'partialBlockActionOptions'] = $optionMessages;
437
438        $a['ActionRestrictions'] = [
439            'type' => 'multiselect',
440            'cssclass' => 'mw-htmlform-checkradio-indent mw-block-partial-restriction mw-block-action-restriction',
441            'options-messages' => $optionMessages,
442            'section' => 'actions',
443        ];
444
445        $a['CreateAccount'] = [
446            'type' => 'check',
447            'cssclass' => 'mw-block-restriction',
448            'label-message' => 'ipbcreateaccount',
449            'default' => true,
450            'section' => 'details',
451        ];
452
453        if ( $this->blockPermissionCheckerFactory
454            ->newChecker( $user )
455            ->checkEmailPermissions()
456        ) {
457            $a['DisableEmail'] = [
458                'type' => 'check',
459                'cssclass' => 'mw-block-restriction',
460                'label-message' => 'ipbemailban',
461                'section' => 'details',
462            ];
463
464            $this->codexFormData[ 'blockDisableEmailVisible'] = true;
465        }
466
467        if ( $blockAllowsUTEdit ) {
468            $a['DisableUTEdit'] = [
469                'type' => 'check',
470                'cssclass' => 'mw-block-restriction',
471                'label-message' => 'ipb-disableusertalk',
472                'default' => false,
473                'section' => 'details',
474            ];
475
476            $this->codexFormData[ 'blockDisableUTEditVisible'] = true;
477        }
478
479        $defaultExpiry = $this->msg( 'ipb-default-expiry' )->inContentLanguage();
480        $indefiniteExpiry = 'indefinite';
481        $indefiniteExpiryLabel = $this->msg( 'block-expiry-indefinite' )->text();
482        if ( $this->target instanceof BlockTargetWithIp ) {
483            $defaultExpiryIP = $this->msg( 'ipb-default-expiry-ip' )->inContentLanguage();
484            if ( !$defaultExpiryIP->isDisabled() ) {
485                $defaultExpiry = $defaultExpiryIP;
486            }
487        } elseif (
488            $this->target instanceof UserBlockTarget &&
489            $this->userNameUtils->isTemp( $this->target->getUserIdentity()->getName() )
490        ) {
491            $defaultExpiryTemporaryAccount = $this->msg( 'ipb-default-expiry-temporary-account' )
492                ->inContentLanguage();
493            if ( !$defaultExpiryTemporaryAccount->isDisabled() ) {
494                $defaultExpiry = $defaultExpiryTemporaryAccount;
495            }
496
497            $indefiniteExpiryTemporaryAccount = $this->msg( 'ipb-indefinite-expiry-temporary-account' )
498                ->inContentLanguage();
499            if ( !$indefiniteExpiryTemporaryAccount->isDisabled() ) {
500                $indefiniteExpiry = $indefiniteExpiryTemporaryAccount->text();
501                $indefiniteExpiryLabel = $this->getExpiryLabel( $indefiniteExpiry, $suggestedDurations );
502            }
503        }
504
505        $a['Expiry'] = [
506            'type' => 'expiry',
507            'required' => true,
508            'options' => $suggestedDurations,
509            'default' => $defaultExpiry->text(),
510            'section' => 'expiry',
511        ];
512        $this->codexFormData[ 'blockExpiryOptions' ] = $suggestedDurations;
513        $this->codexFormData[ 'blockExpiryDefault' ] = $defaultExpiry->text();
514        $this->codexFormData[ 'blockIndefiniteExpiry' ] = $indefiniteExpiry;
515        $this->codexFormData[ 'blockIndefiniteExpiryLabel' ] = $indefiniteExpiryLabel;
516
517        $a['Reason'] = [
518            'type' => 'selectandother',
519            // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
520            // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
521            // Unicode codepoints.
522            'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
523            'maxlength-unit' => 'codepoints',
524            'options-message' => 'ipbreason-dropdown',
525            'section' => 'reason',
526            'help-message' => 'block-reason-help',
527        ];
528
529        if ( $useCodex ) {
530            $blockReasonOptions = Html::listDropdownOptionsCodex(
531                Html::listDropdownOptions( $this->msg( 'ipbreason-dropdown' )->inContentLanguage()->plain(),
532                    [ 'other' => $this->msg( 'htmlform-selectorother-other' )->text() ]
533            ) );
534            $this->codexFormData[ 'blockReasonOptions' ] = $blockReasonOptions;
535            $indefBlockReasonOptions = Html::listDropdownOptionsCodex(
536                Html::listDropdownOptions( $this->msg( 'ipbreason-indef-dropdown' )->inContentLanguage()->plain(),
537                    [ 'other' => $this->msg( 'htmlform-selectorother-other' )->text() ]
538                ) );
539            $this->codexFormData[ 'indefBlockReasonOptions' ] = $indefBlockReasonOptions;
540            $this->codexFormData[ 'blockReasonMaxLength' ] = CommentStore::COMMENT_CHARACTER_LIMIT;
541        }
542
543        $a['AutoBlock'] = [
544            'type' => 'check',
545            'label-message' => [
546                'ipbenableautoblock',
547                Message::durationParam( $conf->get( MainConfigNames::AutoblockExpiry ) )
548            ],
549            'default' => true,
550            'section' => 'options',
551        ];
552        $this->codexFormData['blockAutoblockExpiry'] = $this->getLanguage()
553            ->formatDuration( $conf->get( MainConfigNames::AutoblockExpiry ) );
554
555        // Allow some users to hide name from block log, blocklist and listusers
556        if ( $this->getAuthority()->isAllowed( 'hideuser' ) ) {
557            $a['HideUser'] = [
558                'type' => 'check',
559                'label-message' => 'ipbhidename',
560                'cssclass' => 'mw-block-hideuser',
561                'section' => 'options',
562            ];
563
564            $this->codexFormData['blockHideUser'] = true;
565        }
566
567        // Watchlist their user page? (Only if user is logged in)
568        if ( $user->isRegistered() ) {
569            $a['Watch'] = [
570                'type' => 'check',
571                'label-message' => 'ipbwatchuser',
572                'section' => 'options',
573            ];
574        }
575
576        $a['HardBlock'] = [
577            'type' => 'check',
578            'label-message' => 'ipb-hardblock',
579            'default' => false,
580            'section' => 'options',
581        ];
582
583        // This is basically a copy of the Target field, but the user can't change it, so we
584        // can see if the warnings we maybe showed to the user before still apply
585        $a['PreviousTarget'] = [
586            'type' => 'hidden',
587            'default' => false,
588        ];
589
590        // We'll turn this into a checkbox if we need to
591        $a['Confirm'] = [
592            'type' => 'hidden',
593            'default' => '',
594            'label-message' => 'ipb-confirm',
595            'cssclass' => 'mw-block-confirm',
596        ];
597
598        $this->validateTarget();
599
600        // (T382496) Only load the modified defaults from a previous
601        // block if multiblocks are not enabled
602        if ( !$this->getConfig()->get( MainConfigNames::EnableMultiBlocks )
603            || $this->getRequest()->getBool( 'multiblocks' )
604        ) {
605            $this->maybeAlterFormDefaults( $a );
606        }
607
608        // Allow extensions to add more fields
609        $this->getHookRunner()->onSpecialBlockModifyFormFields( $this, $a );
610
611        if ( $useCodex ) {
612            $default = (string)$this->target;
613            $a['Target']['default'] = $default;
614            $a['Target']['disabled'] = true;
615            // Remove all fields except Target for Codex. (T377529)
616            // This is a temporary measure until Codex PHP is available.
617            $a = array_intersect_key( $a, [ 'Target' => true ] );
618        }
619
620        return $a;
621    }
622
623    /**
624     * Validate the target, setting preErrors if necessary.
625     *
626     * @param WebRequest|null $request For testing purposes.
627     */
628    private function validateTarget( ?WebRequest $request = null ): void {
629        $request ??= $this->getRequest();
630        if ( !$this->target ) {
631            if ( $request->getVal( 'id' ) ) {
632                $this->preErrors[] = $this->msg( 'block-invalid-id' );
633            }
634            return;
635        }
636
637        $status = $this->target->validateForCreation();
638        $this->codexFormData[ 'blockTargetExists' ] = true;
639
640        if ( !$status->isOK() ) {
641            $errors = $status->getMessages( 'error' );
642            $this->preErrors = array_merge( $this->preErrors, $errors );
643
644            // Remove top-level errors that are later handled per-field in Codex.
645            if ( $this->getConfig()->get( MainConfigNames::UseCodexSpecialBlock ) || $request->getBool( 'usecodex' ) ) {
646                $this->preErrors = array_filter( $this->preErrors, function ( $error ) {
647                    if ( $error->getKey() === 'nosuchusershort' || $error->getKey() === 'ip_range_toolarge' ) {
648                        // Avoids us having to re-query the API to validate the user.
649                        $this->codexFormData[ 'blockTargetExists' ] = false;
650                        return false;
651                    }
652                    return true;
653                } );
654            }
655        }
656    }
657
658    /**
659     * Get a localized label for a block expiry value.
660     *
661     * @param string $expiry
662     * @param array<string,string> $suggestedDurations
663     * @return string
664     */
665    private function getExpiryLabel( string $expiry, array $suggestedDurations ): string {
666        $label = array_search( $expiry, $suggestedDurations, true );
667        if ( $label !== false ) {
668            return $label;
669        }
670
671        $expiryTimestamp = strtotime( '+' . $expiry, 0 );
672        if ( $expiryTimestamp !== false ) {
673            return $this->getLanguage()->formatDurationBetweenTimestamps(
674                0,
675                $expiryTimestamp
676            );
677        }
678
679        return $expiry;
680    }
681
682    /**
683     * If the user has already been blocked with similar settings, load that block
684     * and change the defaults for the form fields to match the existing settings.
685     * @param array &$fields HTMLForm descriptor array
686     */
687    protected function maybeAlterFormDefaults( &$fields ) {
688        // This will be overwritten by request data
689        $fields['Target']['default'] = (string)$this->target;
690
691        // This won't be
692        $fields['PreviousTarget']['default'] = (string)$this->target;
693
694        $block = $this->blockStore->newFromTarget(
695            $this->target, null, false, DatabaseBlockStore::AUTO_NONE );
696
697        // Populate fields if there is a block that is not an autoblock; if it is a range
698        // block, only populate the fields if the range is the same as $this->target
699        if ( $block instanceof DatabaseBlock
700            && ( !( $this->target instanceof RangeBlockTarget )
701                || $block->isBlocking( $this->target ) )
702        ) {
703            $fields['HardBlock']['default'] = $block->isHardblock();
704            $fields['CreateAccount']['default'] = $block->isCreateAccountBlocked();
705            $fields['AutoBlock']['default'] = $block->isAutoblocking();
706
707            if ( isset( $fields['DisableEmail'] ) ) {
708                $fields['DisableEmail']['default'] = $block->isEmailBlocked();
709            }
710
711            if ( isset( $fields['HideUser'] ) ) {
712                $fields['HideUser']['default'] = $block->getHideName();
713            }
714
715            if ( isset( $fields['DisableUTEdit'] ) ) {
716                $fields['DisableUTEdit']['default'] = !$block->isUsertalkEditAllowed();
717            }
718
719            // If the block was hidden, don't show the reason unless this user can see the block: T37839
720            if ( !$block->getHideBlock() || $this->getAuthority()->isAllowed( 'hideuser' ) ) {
721                $fields['Reason']['default'] = $block->getReasonComment()->text;
722            } else {
723                $fields['Reason']['default'] = '';
724            }
725
726            if ( $this->getRequest()->wasPosted() ) {
727                // Ok, so we got a POST submission asking us to reblock a user.  So show the
728                // confirm checkbox; the user will only see it if they haven't previously
729                $fields['Confirm']['type'] = 'check';
730            } else {
731                // We got a target, but it wasn't a POST request, so the user must have gone
732                // to a link like [[Special:Block/User]].  We don't need to show the checkbox
733                // as long as they go ahead and block *that* user
734                $fields['Confirm']['default'] = 1;
735            }
736
737            if ( $block->getExpiry() == 'infinity' ) {
738                $fields['Expiry']['default'] = $this->codexFormData[ 'blockExpiryDefault' ] = 'infinite';
739            } else {
740                $fields['Expiry']['default'] = wfTimestamp( TS::RFC2822, $block->getExpiry() );
741
742                // Don't overwrite if expiry was specified in the URL
743                if ( !isset( $this->codexFormData[ 'blockExpiryPreset' ] ) ) {
744                    $this->codexFormData[ 'blockExpiryPreset' ] = $this->formatExpiryForHtml( $block->getExpiry() );
745                }
746            }
747
748            if ( !$block->isSitewide() ) {
749                $fields['EditingRestriction']['default'] =
750                    $this->codexFormData[ 'blockTypePreset' ] = 'partial';
751
752                $pageRestrictions = [];
753                $namespaceRestrictions = [];
754                foreach ( $block->getRestrictions() as $restriction ) {
755                    if ( $restriction instanceof PageRestriction && $restriction->getTitle() ) {
756                        $pageRestrictions[] = $restriction->getTitle()->getPrefixedText();
757                    } elseif ( $restriction instanceof NamespaceRestriction &&
758                        $this->namespaceInfo->exists( $restriction->getValue() )
759                    ) {
760                        $namespaceRestrictions[] = $restriction->getValue();
761                    }
762                }
763
764                // Sort the restrictions so they are in alphabetical order.
765                sort( $pageRestrictions );
766                $fields['PageRestrictions']['default'] =
767                    $this->codexFormData[ 'blockPageRestrictions' ] = implode( "\n", $pageRestrictions );
768                sort( $namespaceRestrictions );
769                $fields['NamespaceRestrictions']['default'] =
770                    $this->codexFormData[ 'blockNamespaceRestrictions' ] = implode( "\n", $namespaceRestrictions );
771
772                $actionRestrictions = [];
773                foreach ( $block->getRestrictions() as $restriction ) {
774                    if ( $restriction instanceof ActionRestriction ) {
775                        $actionRestrictions[] = $restriction->getValue();
776                    }
777                }
778                $fields['ActionRestrictions']['default'] = $actionRestrictions;
779            }
780
781            $this->alreadyBlocked = true;
782            $this->codexFormData[ 'blockAlreadyBlocked' ] = $this->alreadyBlocked;
783            $this->preErrors[] = $this->msg(
784                'ipb-needreblock',
785                '<bdi>' . wfEscapeWikiText( $block->getTargetName() ) . '</bdi>'
786            );
787        }
788
789        if ( $this->alreadyBlocked || $this->getRequest()->wasPosted()
790            || $this->getRequest()->getCheck( 'wpCreateAccount' )
791        ) {
792            $this->getOutput()->addJsConfigVars( 'wgCreateAccountDirty', true );
793        }
794
795        // We always need confirmation to do HideUser
796        if ( $this->requestedHideUser && $this->getAuthority()->isAllowed( 'hideuser' ) ) {
797            $fields['Confirm']['type'] = 'check';
798            unset( $fields['Confirm']['default'] );
799            $this->preErrors[] = $this->msg( 'ipb-confirmhideuser', 'ipb-confirmaction' );
800        }
801
802        // Or if the user is trying to block themselves
803        if ( (string)$this->target === $this->getUser()->getName() ) {
804            $fields['Confirm']['type'] = 'check';
805            unset( $fields['Confirm']['default'] );
806            $this->preErrors[] = $this->msg( 'ipb-blockingself', 'ipb-confirmaction' );
807        }
808    }
809
810    /**
811     * Format a date string for use by <input type="datetime-local">
812     *
813     * @param string $expiry
814     * @return string Formatted as YYYY-MM-DDTHH:mm
815     */
816    private function formatExpiryForHtml( string $expiry ): string {
817        if ( preg_match( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/', $expiry ) === 1 ) {
818            // YYYY-MM-DDTHH:mm which is accepted by <input type="datetime-local">, but not by MediaWiki.
819            return substr( $expiry, 0, 16 );
820        } elseif ( $expiry === '' ) {
821            // No expiry specified
822            return '';
823        }
824        return substr( wfTimestamp( TS::ISO_8601, $expiry ), 0, 16 );
825    }
826
827    /**
828     * Add header elements like block log entries, etc.
829     * @return string
830     */
831    protected function preHtml() {
832        $this->getOutput()->addModuleStyles( [ 'mediawiki.special' ] );
833        if ( $this->getConfig()->get( MainConfigNames::UseCodexSpecialBlock )
834            || $this->getRequest()->getBool( 'usecodex' )
835        ) {
836            $this->getOutput()->addModules( [ 'mediawiki.special.block.codex' ] );
837            $this->getOutput()->addElement( 'noscript', [],
838                $this->msg( 'block-javascript-required' )->text()
839            );
840        } else {
841            $this->getOutput()->addModules( [ 'mediawiki.special.block' ] );
842            $this->getOutput()->addBodyClasses( 'mw-special-Block--legacy' );
843        }
844
845        $blockCIDRLimit = $this->getConfig()->get( MainConfigNames::BlockCIDRLimit );
846        $text = $this->msg( 'blockiptext', $blockCIDRLimit['IPv4'], $blockCIDRLimit['IPv6'] )->parse();
847
848        $otherBlockMessages = [];
849        if ( $this->target !== null ) {
850            // Get other blocks, i.e. from GlobalBlocking or TorBlock extension
851            $this->getHookRunner()->onOtherBlockLogLink(
852                $otherBlockMessages, $this->target->toString() );
853
854            if ( count( $otherBlockMessages ) ) {
855                $s = Html::rawElement(
856                    'h2',
857                    [],
858                    $this->msg( 'ipb-otherblocks-header', count( $otherBlockMessages ) )->parse()
859                ) . "\n";
860
861                $list = '';
862
863                foreach ( $otherBlockMessages as $link ) {
864                    $list .= Html::rawElement( 'li', [], $link ) . "\n";
865                }
866
867                $s .= Html::rawElement(
868                    'ul',
869                    [ 'class' => 'mw-blockip-alreadyblocked' ],
870                    $list
871                ) . "\n";
872
873                $text .= $s;
874            }
875        }
876
877        return $text;
878    }
879
880    /**
881     * Add footer elements to the form
882     * @return string
883     */
884    protected function postHtml() {
885        $links = [];
886
887        $this->getOutput()->addModuleStyles( 'mediawiki.special' );
888
889        $linkRenderer = $this->getLinkRenderer();
890        // Link to the user's contributions, if applicable
891        if ( $this->target instanceof BlockTargetWithUserPage ) {
892            $contribsPage = SpecialPage::getTitleFor( 'Contributions', (string)$this->target );
893            $links[] = $linkRenderer->makeLink(
894                $contribsPage,
895                $this->msg( 'ipb-blocklist-contribs', (string)$this->target )->text()
896            );
897        }
898
899        // Link to unblock the specified user, or to a blank unblock form
900        if ( $this->target instanceof BlockTargetWithUserPage ) {
901            $message = $this->msg(
902                'ipb-unblock-addr',
903                wfEscapeWikiText( (string)$this->target )
904            )->parse();
905            $list = SpecialPage::getTitleFor( 'Unblock', (string)$this->target );
906        } else {
907            $message = $this->msg( 'ipb-unblock' )->parse();
908            $list = SpecialPage::getTitleFor( 'Unblock' );
909        }
910        $links[] = $linkRenderer->makeKnownLink(
911            $list,
912            new HtmlArmor( $message )
913        );
914
915        // Link to the block list
916        $links[] = $linkRenderer->makeKnownLink(
917            SpecialPage::getTitleFor( 'BlockList' ),
918            $this->msg( 'ipb-blocklist' )->text()
919        );
920
921        // Link to edit the block dropdown reasons, if applicable
922        if ( $this->getAuthority()->isAllowed( 'editinterface' ) ) {
923            $links[] = $linkRenderer->makeKnownLink(
924                $this->msg( 'ipbreason-dropdown' )->inContentLanguage()->getTitle(),
925                $this->msg( 'ipb-edit-dropdown' )->text(),
926                [],
927                [ 'action' => 'edit' ]
928            );
929        }
930
931        $text = Html::rawElement(
932            'p',
933            [ 'class' => 'mw-ipb-conveniencelinks' ],
934            $this->getLanguage()->pipeList( $links )
935        );
936
937        if ( $this->target ) {
938            $userPage = $this->target->getLogPage();
939            // Get relevant extracts from the block and suppression logs, if possible
940            $out = '';
941
942            LogEventsList::showLogExtract(
943                $out,
944                'block',
945                $userPage,
946                '',
947                [
948                    'lim' => 10,
949                    'msgKey' => [
950                        'blocklog-showlog',
951                        $this->titleFormatter->getText( $userPage ),
952                    ],
953                    'showIfEmpty' => false
954                ]
955            );
956            $text .= $out;
957
958            // Add suppression block entries if allowed
959            if ( $this->getAuthority()->isAllowed( 'suppressionlog' ) ) {
960                LogEventsList::showLogExtract(
961                    $out,
962                    'suppress',
963                    $userPage,
964                    '',
965                    [
966                        'lim' => 10,
967                        'conds' => [ 'log_action' => [ 'block', 'reblock', 'unblock' ] ],
968                        'msgKey' => [
969                            'blocklog-showsuppresslog',
970                            $this->titleFormatter->getText( $userPage ),
971                        ],
972                        'showIfEmpty' => false
973                    ]
974                );
975
976                $text .= $out;
977            }
978        }
979
980        return $text;
981    }
982
983    /**
984     * Get the target and type, given the request and the subpage parameter.
985     * Several parameters are handled for backwards compatibility. A block ID
986     * is prioritized, followed by 'wpTarget' since it matches the HTML form.
987     *
988     * @param string|null $par Subpage parameter passed to setup, or data value from
989     *  the HTMLForm
990     * @param WebRequest $request Try and get data from a request too
991     * @return BlockTarget|null
992     */
993    private function getTargetInternal( ?string $par, WebRequest $request ) {
994        // Passing in a block ID gets priority.
995        $blockId = $request->getInt( 'id', 0 );
996        if ( $blockId > 0 ) {
997            $block = $this->blockStore->newFromId( $blockId );
998            if ( $block ) {
999                return $block->getRedactedTarget();
1000            }
1001        }
1002
1003        $possibleTargets = [
1004            $request->getVal( 'wpTarget', null ),
1005            $par,
1006            $request->getVal( 'ip', null ),
1007            // B/C @since 1.18
1008            $request->getVal( 'wpBlockAddress', null ),
1009        ];
1010        foreach ( $possibleTargets as $possibleTarget ) {
1011            $target = $this->blockTargetFactory
1012                ->newFromString( $possibleTarget );
1013            // If type is not null then target is valid
1014            if ( $target !== null ) {
1015                break;
1016            }
1017        }
1018        return $target;
1019    }
1020
1021    /**
1022     * Process the form on POST submission.
1023     * @param array $data
1024     * @param HTMLForm|null $form
1025     * @return bool|string|array|Status As documented for HTMLForm::trySubmit.
1026     */
1027    public function onSubmit( array $data, ?HTMLForm $form = null ) {
1028        if ( $this->getConfig()->get( MainConfigNames::UseCodexSpecialBlock )
1029            || $this->getRequest()->getBool( 'usecodex' )
1030        ) {
1031            // Treat as no submission for the JS-only Codex form.
1032            // This happens if the form is submitted before any JS is loaded.
1033            return false;
1034        }
1035
1036        $isPartialBlock = isset( $data['EditingRestriction'] ) &&
1037            $data['EditingRestriction'] === 'partial';
1038
1039        // This might have been a hidden field or a checkbox, so interesting data
1040        // can come from it
1041        $data['Confirm'] = !in_array( $data['Confirm'], [ '', '0', null, false ], true );
1042
1043        // If the user has done the form 'properly', they won't even have been given the
1044        // option to suppress-block unless they have the 'hideuser' permission
1045        if ( !isset( $data['HideUser'] ) ) {
1046            $data['HideUser'] = false;
1047        }
1048
1049        /** @var User $target */
1050        $target = $this->blockTargetFactory->newFromString( $data['Target'] );
1051        if ( $target instanceof UserBlockTarget ) {
1052            // Give admins a heads-up before they go and block themselves.  Much messier
1053            // to do this for IPs, but it's pretty unlikely they'd ever get the 'block'
1054            // permission anyway, although the code does allow for it.
1055            // Note: Important to use $target instead of $data['Target']
1056            // since both $data['PreviousTarget'] and $target are normalized
1057            // but $data['Target'] gets overridden by (non-normalized) request variable
1058            // from previous request.
1059            if ( $target->toString() === $this->getUser()->getName() &&
1060                ( $data['PreviousTarget'] !== $target->toString() || !$data['Confirm'] )
1061            ) {
1062                return [ 'ipb-blockingself', 'ipb-confirmaction' ];
1063            }
1064
1065            if ( $data['HideUser'] && !$data['Confirm'] ) {
1066                return [ 'ipb-confirmhideuser', 'ipb-confirmaction' ];
1067            }
1068        } elseif ( !( $target instanceof AnonIpBlockTarget || $target instanceof RangeBlockTarget ) ) {
1069            // This should have been caught in the form field validation
1070            return [ 'badipaddress' ];
1071        }
1072
1073        // Reason, to be passed to the block object. For default values of reason, see
1074        // HTMLSelectAndOtherField::getDefault
1075        $blockReason = $data['Reason'][0] ?? '';
1076
1077        $pageRestrictions = [];
1078        $namespaceRestrictions = [];
1079        $actionRestrictions = [];
1080        if ( $isPartialBlock ) {
1081            if ( isset( $data['PageRestrictions'] ) && $data['PageRestrictions'] !== '' ) {
1082                $titles = explode( "\n", $data['PageRestrictions'] );
1083                foreach ( $titles as $title ) {
1084                    $pageRestrictions[] = PageRestriction::newFromTitle( $title );
1085                }
1086            }
1087            if ( isset( $data['NamespaceRestrictions'] ) && $data['NamespaceRestrictions'] !== '' ) {
1088                $namespaceRestrictions = array_map( static function ( $id ) {
1089                    return new NamespaceRestriction( 0, (int)$id );
1090                }, explode( "\n", $data['NamespaceRestrictions'] ) );
1091            }
1092            if ( isset( $data['ActionRestrictions'] ) && $data['ActionRestrictions'] !== '' ) {
1093                $actionRestrictions = array_map( static function ( $id ) {
1094                    return new ActionRestriction( 0, $id );
1095                }, $data['ActionRestrictions'] );
1096            }
1097        }
1098        $restrictions = array_merge( $pageRestrictions, $namespaceRestrictions, $actionRestrictions );
1099
1100        if ( !isset( $data['Tags'] ) ) {
1101            $data['Tags'] = [];
1102        }
1103
1104        $blockOptions = [
1105            'isCreateAccountBlocked' => $data['CreateAccount'],
1106            'isHardBlock' => $data['HardBlock'],
1107            'isAutoblocking' => $data['AutoBlock'],
1108            'isHideUser' => $data['HideUser'],
1109            'isPartial' => $isPartialBlock,
1110        ];
1111
1112        if ( isset( $data['DisableUTEdit'] ) ) {
1113            $blockOptions['isUserTalkEditBlocked'] = $data['DisableUTEdit'];
1114        }
1115        if ( isset( $data['DisableEmail'] ) ) {
1116            $blockOptions['isEmailBlocked'] = $data['DisableEmail'];
1117        }
1118
1119        $blockUser = $this->blockUserFactory->newBlockUser(
1120            $target,
1121            $this->getAuthority(),
1122            $data['Expiry'],
1123            $blockReason,
1124            $blockOptions,
1125            $restrictions,
1126            $data['Tags']
1127        );
1128
1129        // Indicates whether the user is confirming the block and is aware of
1130        // the conflict (did not change the block target in the meantime)
1131        $blockNotConfirmed = !$data['Confirm'] || ( array_key_exists( 'PreviousTarget', $data )
1132            && $data['PreviousTarget'] !== $target->toString() );
1133
1134        // Special case for API - T34434
1135        $reblockNotAllowed = ( array_key_exists( 'Reblock', $data ) && !$data['Reblock'] );
1136
1137        $doReblock = !$blockNotConfirmed && !$reblockNotAllowed;
1138
1139        try {
1140            $status = $blockUser->placeBlock( $doReblock );
1141        } catch ( MultiblocksException ) {
1142            $status = Status::newFatal( 'block-reblock-multi-legacy' );
1143        }
1144
1145        if ( !$status->isOK() ) {
1146            return $status;
1147        }
1148
1149        if (
1150            // Can't watch a range block
1151            $target instanceof BlockTargetWithUserPage
1152
1153            // Technically a wiki can be configured to allow anonymous users to place blocks,
1154            // in which case the 'Watch' field isn't included in the form shown, and we should
1155            // not try to access it.
1156            && array_key_exists( 'Watch', $data )
1157            && $data['Watch']
1158        ) {
1159            $this->watchlistManager->addWatchIgnoringRights(
1160                $this->getUser(),
1161                Title::newFromPageReference( $target->getUserPage() )
1162            );
1163        }
1164
1165        return true;
1166    }
1167
1168    /**
1169     * Do something exciting on successful processing of the form, most likely to show a
1170     * confirmation message
1171     */
1172    public function onSuccess() {
1173        $out = $this->getOutput();
1174        $out->setPageTitleMsg( $this->msg( 'blockipsuccesssub' ) );
1175        $out->addWikiMsg( 'blockipsuccesstext', wfEscapeWikiText( (string)$this->target ) );
1176    }
1177
1178    /**
1179     * Return an array of subpages beginning with $search that this special page will accept.
1180     *
1181     * @param string $search Prefix to search for
1182     * @param int $limit Maximum number of results to return (usually 10)
1183     * @param int $offset Number of results to skip (usually 0)
1184     * @return string[] Matching subpages
1185     */
1186    public function prefixSearchSubpages( $search, $limit, $offset ) {
1187        $search = $this->userNameUtils->getCanonical( $search );
1188        if ( !$search ) {
1189            // No prefix suggestion for invalid user
1190            return [];
1191        }
1192        // Autocomplete subpage as user list - public to allow caching
1193        return $this->userNamePrefixSearch
1194            ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
1195    }
1196
1197    /**
1198     * @inheritDoc
1199     */
1200    protected function getGroupName() {
1201        return 'users';
1202    }
1203}
1204
1205/** @deprecated class alias since 1.41 */
1206class_alias( SpecialBlock::class, 'SpecialBlock' );