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