Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
21.46% |
44 / 205 |
|
15.38% |
2 / 13 |
CRAP | |
0.00% |
0 / 1 |
SpecialGlobalBlock | |
21.46% |
44 / 205 |
|
15.38% |
2 / 13 |
1071.02 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
setParameter | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
7 | |||
loadExistingBlock | |
95.83% |
23 / 24 |
|
0.00% |
0 / 1 |
6 | |||
getFormFields | |
0.00% |
0 / 72 |
|
0.00% |
0 / 1 |
30 | |||
alterForm | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
postHtml | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
6 | |||
onSubmit | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
90 | |||
onSuccess | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
buildExpirySelector | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
42 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDisplayFormat | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\GlobalBlocking\Special; |
4 | |
5 | use HTMLForm; |
6 | use LogEventsList; |
7 | use MediaWiki\Block\BlockUserFactory; |
8 | use MediaWiki\Block\BlockUtils; |
9 | use MediaWiki\CommentStore\CommentStore; |
10 | use MediaWiki\Extension\GlobalBlocking\GlobalBlocking; |
11 | use MediaWiki\Extension\GlobalBlocking\Services\GlobalBlockingConnectionProvider; |
12 | use MediaWiki\Extension\GlobalBlocking\Services\GlobalBlockManager; |
13 | use MediaWiki\Html\Html; |
14 | use MediaWiki\SpecialPage\FormSpecialPage; |
15 | use MediaWiki\Status\Status; |
16 | use MediaWiki\Title\Title; |
17 | use MediaWiki\User\CentralId\CentralIdLookup; |
18 | use MediaWiki\User\UserIdentity; |
19 | use MediaWiki\User\UserNameUtils; |
20 | use Wikimedia\IPUtils; |
21 | |
22 | class 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 | } |