Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
21.46% covered (danger)
21.46%
44 / 205
15.38% covered (danger)
15.38%
2 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialGlobalBlock
21.46% covered (danger)
21.46%
44 / 205
15.38% covered (danger)
15.38%
2 / 13
1071.02
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 setParameter
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
7
 loadExistingBlock
95.83% covered (success)
95.83%
23 / 24
0.00% covered (danger)
0.00%
0 / 1
6
 getFormFields
0.00% covered (danger)
0.00%
0 / 72
0.00% covered (danger)
0.00%
0 / 1
30
 alterForm
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 postHtml
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 onSubmit
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
90
 onSuccess
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 buildExpirySelector
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDisplayFormat
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\GlobalBlocking\Special;
4
5use HTMLForm;
6use LogEventsList;
7use MediaWiki\Block\BlockUserFactory;
8use MediaWiki\Block\BlockUtils;
9use MediaWiki\CommentStore\CommentStore;
10use MediaWiki\Extension\GlobalBlocking\GlobalBlocking;
11use MediaWiki\Extension\GlobalBlocking\Services\GlobalBlockingConnectionProvider;
12use MediaWiki\Extension\GlobalBlocking\Services\GlobalBlockManager;
13use MediaWiki\Html\Html;
14use MediaWiki\SpecialPage\FormSpecialPage;
15use MediaWiki\Status\Status;
16use MediaWiki\Title\Title;
17use MediaWiki\User\CentralId\CentralIdLookup;
18use MediaWiki\User\UserIdentity;
19use MediaWiki\User\UserNameUtils;
20use Wikimedia\IPUtils;
21
22class SpecialGlobalBlock extends FormSpecialPage {
23    /**
24     * @see SpecialGlobalBlock::setParameter()
25     * @var string|null
26     */
27    protected ?string $target;
28
29    /**
30     * @var bool Whether there is an existing block on the target
31     */
32    private bool $modifyForm = false;
33
34    private BlockUserFactory $blockUserFactory;
35    private BlockUtils $blockUtils;
36    private GlobalBlockingConnectionProvider $globalBlockingConnectionProvider;
37    private GlobalBlockManager $globalBlockManager;
38    private CentralIdLookup $centralIdLookup;
39    private UserNameUtils $userNameUtils;
40
41    /**
42     * @param BlockUserFactory $blockUserFactory
43     * @param BlockUtils $blockUtils
44     * @param GlobalBlockingConnectionProvider $globalBlockingConnectionProvider
45     * @param GlobalBlockManager $globalBlockManager
46     * @param CentralIdLookup $centralIdLookup
47     * @param UserNameUtils $userNameUtils
48     */
49    public function __construct(
50        BlockUserFactory $blockUserFactory,
51        BlockUtils $blockUtils,
52        GlobalBlockingConnectionProvider $globalBlockingConnectionProvider,
53        GlobalBlockManager $globalBlockManager,
54        CentralIdLookup $centralIdLookup,
55        UserNameUtils $userNameUtils
56    ) {
57        parent::__construct( 'GlobalBlock', 'globalblock' );
58        $this->blockUserFactory = $blockUserFactory;
59        $this->blockUtils = $blockUtils;
60        $this->globalBlockingConnectionProvider = $globalBlockingConnectionProvider;
61        $this->globalBlockManager = $globalBlockManager;
62        $this->centralIdLookup = $centralIdLookup;
63        $this->userNameUtils = $userNameUtils;
64    }
65
66    public function doesWrites() {
67        return true;
68    }
69
70    public function execute( $par ) {
71        parent::execute( $par );
72        $this->addHelpLink( 'Extension:GlobalBlocking' );
73        $out = $this->getOutput();
74        $out->setPageTitleMsg( $this->msg( 'globalblocking-block' ) );
75        $out->setSubtitle( GlobalBlocking::buildSubtitleLinks( $this ) );
76    }
77
78    /**
79     * Set subpage parameter or 'wpAddress' as $this->address
80     * @param string $par
81     */
82    protected function setParameter( $par ) {
83        if ( $par && !$this->getRequest()->wasPosted() ) {
84            // GET request to Special:GlobalBlock/target where 'target' can be an IP, range, or username.
85            $target = $par;
86        } else {
87            $target = trim( $this->getRequest()->getText( 'wpAddress' ) );
88        }
89
90        if ( IPUtils::isValidRange( $target ) ) {
91            $this->target = IPUtils::sanitizeRange( $target );
92        } elseif ( IPUtils::isIPAddress( $target ) ) {
93            $this->target = IPUtils::sanitizeIP( $target );
94        } else {
95            $normalisedTarget = $this->userNameUtils->getCanonical( $target );
96            if ( $normalisedTarget ) {
97                $this->target = $normalisedTarget;
98            } else {
99                // Allow invalid targets to be set, so that the user can be shown an error message.
100                $this->target = $target;
101            }
102        }
103
104        [ $targetForSkin ] = $this->blockUtils->parseBlockTarget( $target );
105
106        if ( $targetForSkin instanceof UserIdentity ) {
107            $this->getSkin()->setRelevantUser( $targetForSkin );
108        }
109    }
110
111    /**
112     * If there is an existing block for the target specified, change
113     * the form to a modify form and load that block's settings so that
114     * we can show it in the default form fields.
115     *
116     * @return array
117     */
118    protected function loadExistingBlock(): array {
119        $blockOptions = [];
120        if ( $this->target ) {
121            $dbr = $this->globalBlockingConnectionProvider->getReplicaGlobalBlockingDatabase();
122            $queryBuilder = $dbr->newSelectQueryBuilder()
123                ->select( [ 'gb_anon_only', 'gb_reason', 'gb_expiry' ] )
124                ->from( 'globalblocks' );
125            if ( IPUtils::isIPAddress( $this->target ) ) {
126                $queryBuilder->where( [ 'gb_address' => $this->target ] );
127            } else {
128                $centralId = $this->centralIdLookup->centralIdFromName( $this->target );
129                if ( !$centralId ) {
130                    return [];
131                }
132                $queryBuilder->where( [ 'gb_target_central_id' => $centralId ] );
133            }
134            $block = $queryBuilder
135                ->andWhere( [ $dbr->expr( 'gb_expiry', '>', $dbr->timestamp() ) ] )
136                ->caller( __METHOD__ )
137                ->fetchRow();
138            if ( $block ) {
139                $this->modifyForm = true;
140
141                $blockOptions['anononly'] = $block->gb_anon_only;
142                $blockOptions['reason'] = $block->gb_reason;
143                $blockOptions['expiry'] = ( $block->gb_expiry === 'infinity' )
144                    ? 'indefinite'
145                    : wfTimestamp( TS_ISO_8601, $block->gb_expiry );
146            }
147        }
148
149        return $blockOptions;
150    }
151
152    /**
153     * @return array
154     */
155    protected function getFormFields() {
156        $getExpiry = self::buildExpirySelector();
157        $fields = [
158            'Address' => [
159                'type' => 'text',
160                'label-message' => 'globalblocking-ipaddress',
161                'id' => 'mw-globalblock-address',
162                'required' => true,
163                'autofocus' => true,
164                'default' => $this->target,
165            ],
166            'Expiry' => [
167                'type' => count( $getExpiry ) ? 'selectorother' : 'text',
168                'label-message' => 'globalblocking-block-expiry',
169                'id' => 'mw-globalblocking-block-expiry-selector',
170                'required' => true,
171                'options' => $getExpiry,
172                'other' => $this->msg( 'globalblocking-block-expiry-selector-other' )->text(),
173            ],
174            'Reason' => [
175                'type' => 'selectandother',
176                'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
177                'label-message' => 'globalblocking-block-reason',
178                'id' => 'mw-globalblock-reason',
179                'options-message' => 'globalblocking-block-reason-dropdown',
180            ],
181            'AnonOnly' => [
182                'type' => 'check',
183                'label-message' => 'globalblocking-ipbanononly',
184                'id' => 'mw-globalblock-anon-only',
185            ],
186            'Modify' => [
187                'type' => 'hidden',
188                'default' => '',
189            ],
190            'Previous' => [
191                'type' => 'hidden',
192                'default' => $this->target,
193            ],
194        ];
195
196        // Modify form defaults if there is an existing block
197        $blockOptions = $this->loadExistingBlock();
198        if ( $this->modifyForm ) {
199            $fields['Expiry']['default'] = $blockOptions['expiry'];
200            $fields['Reason']['default'] = $blockOptions['reason'];
201            $fields['AnonOnly']['default'] = $blockOptions['anononly'];
202            if ( $this->getRequest()->getVal( 'Previous' ) !== $this->target ) {
203                // Let the user know about it and re-submit to modify
204                $fields['Modify']['default'] = 1;
205            }
206        }
207
208        if ( $this->getUser()->isAllowed( 'block' ) ) {
209            $fields['AlsoLocal'] = [
210                'type' => 'check',
211                'label-message' => 'globalblocking-also-local',
212                'id' => 'mw-globalblock-local',
213            ];
214            $fields['AlsoLocalTalk'] = [
215                'type' => 'check',
216                'label-message' => 'globalblocking-also-local-talk',
217                'id' => 'mw-globalblock-local-talk',
218                'hide-if' => [ '!==', 'AlsoLocal', '1' ],
219            ];
220            $fields['AlsoLocalEmail'] = [
221                'type' => 'check',
222                'label-message' => 'globalblocking-also-local-email',
223                'id' => 'mw-globalblock-local-email',
224                'hide-if' => [ '!==', 'AlsoLocal', '1' ],
225            ];
226
227            $fields['AlsoLocalSoft'] = [
228                'type' => 'check',
229                'label-message' => 'globalblocking-also-local-soft',
230                'id' => 'mw-globalblock-local-soft',
231                'hide-if' => [ '!==', 'AlsoLocal', '1' ],
232                'default' => true,
233            ];
234        }
235
236        return $fields;
237    }
238
239    /**
240     * @param HTMLForm $form
241     */
242    protected function alterForm( HTMLForm $form ) {
243        $form->addPreHtml( $this->msg( 'globalblocking-block-intro' )->parseAsBlock() );
244
245        if ( $this->modifyForm && !$this->getRequest()->wasPosted() ) {
246            // For GET requests with target field prefilled, tell the user that it's already blocked
247            // (For POST requests, this will be shown to the user as an actual error in HTMLForm)
248            $msg = $this->msg( 'globalblocking-block-alreadyblocked', $this->target )->parseAsBlock();
249            $form->addHeaderHtml( Html::rawElement( 'div', [ 'class' => 'error' ], $msg ) );
250        }
251
252        $submitMsg = $this->modifyForm ? 'globalblocking-modify-submit' : 'globalblocking-block-submit';
253        $form->setSubmitTextMsg( $submitMsg );
254        $form->setSubmitDestructive();
255        $form->setWrapperLegendMsg( 'globalblocking-block-legend' );
256    }
257
258    /**
259     * Show log of previous global blocks below the form
260     * @return string
261     */
262    protected function postHtml() {
263        $out = '';
264        $title = Title::makeTitleSafe( NS_USER, $this->target );
265        if ( $title ) {
266            LogEventsList::showLogExtract(
267                $out,
268                'gblblock',
269                $title->getPrefixedText(),
270                '',
271                [
272                    'lim' => 10,
273                    'msgKey' => 'globalblocking-showlog',
274                    'showIfEmpty' => false
275                ]
276            );
277        }
278        return $out;
279    }
280
281    /** @inheritDoc */
282    public function onSubmit( array $data ) {
283        $options = [];
284        $performer = $this->getUser();
285
286        if ( $data['AnonOnly'] ) {
287            $options[] = 'anon-only';
288        }
289
290        if ( $this->modifyForm && $data['Modify']
291            // Make sure that the block being modified is for the intended target
292            // (i.e., not from a previous submission)
293            && $data['Previous'] === $data['Address']
294        ) {
295            $options[] = 'modify';
296        }
297
298        // This handles validation too...
299        $globalBlockStatus = $this->globalBlockManager->block(
300            $this->target, // $this->target is sanitized; $data['Address'] isn't
301            $data['Reason'][0],
302            $data['Expiry'],
303            $performer,
304            $options
305        );
306
307        if ( !$globalBlockStatus->isOK() ) {
308            // Show the error message(s) to the user if an error occurred.
309            return Status::wrap( $globalBlockStatus );
310        }
311
312        // Add a local block if the user asked for that
313        if ( $performer->isAllowed( 'block' ) && $data['AlsoLocal'] ) {
314            $localBlockStatus = $this->blockUserFactory->newBlockUser(
315                $this->target,
316                $performer,
317                $data['Expiry'],
318                $data['Reason'][0],
319                [
320                    'isCreateAccountBlocked' => true,
321                    'isEmailBlocked' => $data['AlsoLocalEmail'],
322                    'isUserTalkEditBlocked' => $data['AlsoLocalTalk'],
323                    'isHardBlock' => !$data['AlsoLocalSoft'],
324                    'isAutoblocking' => true,
325                ]
326            )->placeBlock( $data['Modify'] );
327
328            if ( !$localBlockStatus->isOK() ) {
329                $this->getOutput()->addWikiMsg( 'globalblocking-local-failed' );
330            }
331        }
332
333        return Status::newGood();
334    }
335
336    public function onSuccess() {
337        $successMsg = $this->modifyForm ?
338            'globalblocking-modify-success' : 'globalblocking-block-success';
339        // The username must be escaped here, as it's user input and could contain wikitext.
340        $this->getOutput()->addHTML(
341            $this->msg( $successMsg )->plaintextParams( $this->target )->parseAsBlock()
342        );
343
344        $link = $this->getLinkRenderer()->makeKnownLink(
345            $this->getPageTitle(),
346            $this->msg( 'globalblocking-add-block' )->text()
347        );
348        $this->getOutput()->addHTML( $link );
349    }
350
351    /**
352     * Get an array of suggested block durations. Retrieved from
353     * 'globalblocking-expiry-options' and if it's disabled (default),
354     * retrieve it from SpecialBlock's 'ipboptions' message.
355     *
356     * @return array Expiry options, empty if messages are disabled.
357     * @see SpecialBlock::getSuggestedDurations()
358     */
359    protected function buildExpirySelector() {
360        $msg = $this->msg( 'globalblocking-expiry-options' )->inContentLanguage();
361        if ( $msg->isDisabled() ) {
362            $msg = $this->msg( 'ipboptions' )->inContentLanguage();
363            if ( $msg->isDisabled() ) {
364                // Do not assume that 'ipboptions' exists forever.
365                $msg = false;
366            }
367        }
368
369        $options = [];
370        if ( $msg !== false ) {
371            $msg = $msg->text();
372            foreach ( explode( ',', $msg ) as $option ) {
373                if ( strpos( $option, ':' ) === false ) {
374                    $option = "$option:$option";
375                }
376
377                [ $show, $value ] = explode( ':', $option );
378                $options[$show] = $value;
379            }
380        }
381        return $options;
382    }
383
384    protected function getGroupName() {
385        return 'users';
386    }
387
388    protected function getDisplayFormat() {
389        return 'ooui';
390    }
391}