Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
350 / 350 |
|
100.00% |
9 / 9 |
CRAP | |
100.00% |
1 / 1 |
SpecialNuke | |
100.00% |
350 / 350 |
|
100.00% |
9 / 9 |
66 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
doesWrites | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
execute | |
100.00% |
32 / 32 |
|
100.00% |
1 / 1 |
12 | |||
promptForm | |
100.00% |
43 / 43 |
|
100.00% |
1 / 1 |
1 | |||
listForm | |
100.00% |
105 / 105 |
|
100.00% |
1 / 1 |
13 | |||
getNewPages | |
100.00% |
86 / 86 |
|
100.00% |
1 / 1 |
20 | |||
doDelete | |
100.00% |
56 / 56 |
|
100.00% |
1 / 1 |
10 | |||
prefixSearchSubpages | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getGroupName | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
getDeleteReason | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
getNukeHookRunner | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Nuke; |
4 | |
5 | use DeletePageJob; |
6 | use JobQueueGroup; |
7 | use Language; |
8 | use MediaWiki\CommentStore\CommentStore; |
9 | use MediaWiki\Extension\Nuke\Hooks\NukeHookRunner; |
10 | use MediaWiki\Html\Html; |
11 | use MediaWiki\Html\ListToggle; |
12 | use MediaWiki\HTMLForm\HTMLForm; |
13 | use MediaWiki\Page\File\FileDeleteForm; |
14 | use MediaWiki\Permissions\PermissionManager; |
15 | use MediaWiki\Request\WebRequest; |
16 | use MediaWiki\SpecialPage\SpecialPage; |
17 | use MediaWiki\Title\NamespaceInfo; |
18 | use MediaWiki\Title\Title; |
19 | use MediaWiki\User\UserFactory; |
20 | use MediaWiki\User\UserNamePrefixSearch; |
21 | use MediaWiki\User\UserNameUtils; |
22 | use OOUI\DropdownInputWidget; |
23 | use OOUI\FieldLayout; |
24 | use OOUI\TextInputWidget; |
25 | use PermissionsError; |
26 | use RepoGroup; |
27 | use UserBlockedError; |
28 | use Wikimedia\Rdbms\IConnectionProvider; |
29 | use Wikimedia\Rdbms\IExpression; |
30 | use Wikimedia\Rdbms\LikeMatch; |
31 | use Wikimedia\Rdbms\LikeValue; |
32 | use Wikimedia\Rdbms\SelectQueryBuilder; |
33 | use Xml; |
34 | |
35 | class SpecialNuke extends SpecialPage { |
36 | |
37 | /** @var NukeHookRunner|null */ |
38 | private $hookRunner; |
39 | |
40 | private JobQueueGroup $jobQueueGroup; |
41 | private IConnectionProvider $dbProvider; |
42 | private PermissionManager $permissionManager; |
43 | private RepoGroup $repoGroup; |
44 | private UserFactory $userFactory; |
45 | private UserNamePrefixSearch $userNamePrefixSearch; |
46 | private UserNameUtils $userNameUtils; |
47 | private NamespaceInfo $namespaceInfo; |
48 | private Language $contentLanguage; |
49 | |
50 | public function __construct( |
51 | JobQueueGroup $jobQueueGroup, |
52 | IConnectionProvider $dbProvider, |
53 | PermissionManager $permissionManager, |
54 | RepoGroup $repoGroup, |
55 | UserFactory $userFactory, |
56 | UserNamePrefixSearch $userNamePrefixSearch, |
57 | UserNameUtils $userNameUtils, |
58 | NamespaceInfo $namespaceInfo, |
59 | Language $contentLanguage |
60 | ) { |
61 | parent::__construct( 'Nuke', 'nuke' ); |
62 | $this->jobQueueGroup = $jobQueueGroup; |
63 | $this->dbProvider = $dbProvider; |
64 | $this->permissionManager = $permissionManager; |
65 | $this->repoGroup = $repoGroup; |
66 | $this->userFactory = $userFactory; |
67 | $this->userNamePrefixSearch = $userNamePrefixSearch; |
68 | $this->userNameUtils = $userNameUtils; |
69 | $this->namespaceInfo = $namespaceInfo; |
70 | $this->contentLanguage = $contentLanguage; |
71 | } |
72 | |
73 | /** |
74 | * @inheritDoc |
75 | * @codeCoverageIgnore |
76 | */ |
77 | public function doesWrites() { |
78 | return true; |
79 | } |
80 | |
81 | /** |
82 | * @param null|string $par |
83 | */ |
84 | public function execute( $par ) { |
85 | $this->setHeaders(); |
86 | $this->checkPermissions(); |
87 | $this->checkReadOnly(); |
88 | $this->outputHeader(); |
89 | $this->addHelpLink( 'Help:Extension:Nuke' ); |
90 | |
91 | $currentUser = $this->getUser(); |
92 | $block = $currentUser->getBlock(); |
93 | |
94 | // appliesToRight is presently a no-op, since there is no handling for `delete`, |
95 | // and so will return `null`. `true` will be returned if the block actively |
96 | // applies to `delete`, and both `null` and `true` should result in an error |
97 | if ( $block && ( $block->isSitewide() || |
98 | ( $block->appliesToRight( 'delete' ) !== false ) ) |
99 | ) { |
100 | throw new UserBlockedError( $block ); |
101 | } |
102 | |
103 | $req = $this->getRequest(); |
104 | $target = trim( $req->getText( 'target', $par ?? '' ) ); |
105 | |
106 | // Normalise name |
107 | if ( $target !== '' ) { |
108 | $user = $this->userFactory->newFromName( $target ); |
109 | if ( $user ) { |
110 | $target = $user->getName(); |
111 | } |
112 | } |
113 | |
114 | $reason = $this->getDeleteReason( $this->getRequest(), $target ); |
115 | |
116 | $limit = $req->getInt( 'limit', 500 ); |
117 | $namespace = $req->getIntOrNull( 'namespace' ); |
118 | |
119 | if ( $req->wasPosted() |
120 | && $currentUser->matchEditToken( $req->getVal( 'wpEditToken' ) ) |
121 | ) { |
122 | if ( $req->getRawVal( 'action' ) === 'delete' ) { |
123 | $pages = $req->getArray( 'pages' ); |
124 | |
125 | if ( $pages ) { |
126 | $this->doDelete( $pages, $reason ); |
127 | return; |
128 | } |
129 | } elseif ( $req->getRawVal( 'action' ) === 'submit' ) { |
130 | $this->listForm( $target, $reason, $limit, $namespace ); |
131 | } else { |
132 | $this->promptForm(); |
133 | } |
134 | } elseif ( $target === '' ) { |
135 | $this->promptForm(); |
136 | } else { |
137 | $this->listForm( $target, $reason, $limit, $namespace ); |
138 | } |
139 | } |
140 | |
141 | /** |
142 | * Prompt for a username or IP address. |
143 | * |
144 | * @param string $userName |
145 | */ |
146 | protected function promptForm( string $userName = '' ): void { |
147 | $out = $this->getOutput(); |
148 | |
149 | $out->addWikiMsg( 'nuke-tools' ); |
150 | |
151 | $formDescriptor = [ |
152 | 'nuke-target' => [ |
153 | 'id' => 'nuke-target', |
154 | 'default' => $userName, |
155 | 'label' => $this->msg( 'nuke-userorip' )->text(), |
156 | 'type' => 'user', |
157 | 'name' => 'target', |
158 | 'autofocus' => true |
159 | ], |
160 | 'nuke-pattern' => [ |
161 | 'id' => 'nuke-pattern', |
162 | 'label' => $this->msg( 'nuke-pattern' )->text(), |
163 | 'maxLength' => 40, |
164 | 'type' => 'text', |
165 | 'name' => 'pattern' |
166 | ], |
167 | 'namespace' => [ |
168 | 'id' => 'nuke-namespace', |
169 | 'type' => 'namespaceselect', |
170 | 'label' => $this->msg( 'nuke-namespace' )->text(), |
171 | 'all' => 'all', |
172 | 'name' => 'namespace' |
173 | ], |
174 | 'limit' => [ |
175 | 'id' => 'nuke-limit', |
176 | 'maxLength' => 7, |
177 | 'default' => 500, |
178 | 'label' => $this->msg( 'nuke-maxpages' )->text(), |
179 | 'type' => 'int', |
180 | 'name' => 'limit' |
181 | ] |
182 | ]; |
183 | |
184 | HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() ) |
185 | ->setName( 'massdelete' ) |
186 | ->setFormIdentifier( 'massdelete' ) |
187 | ->setWrapperLegendMsg( 'nuke' ) |
188 | ->setSubmitTextMsg( 'nuke-submit-user' ) |
189 | ->setSubmitName( 'nuke-submit-user' ) |
190 | ->setAction( $this->getPageTitle()->getLocalURL( 'action=submit' ) ) |
191 | ->prepareForm() |
192 | ->displayForm( false ); |
193 | } |
194 | |
195 | /** |
196 | * Display list of pages to delete. |
197 | * |
198 | * @param string $username |
199 | * @param string $reason |
200 | * @param int $limit |
201 | * @param int|null $namespace |
202 | */ |
203 | protected function listForm( $username, $reason, $limit, $namespace = null ): void { |
204 | $out = $this->getOutput(); |
205 | |
206 | $pages = $this->getNewPages( $username, $limit, $namespace ); |
207 | |
208 | if ( !$pages ) { |
209 | if ( $username === '' ) { |
210 | $out->addWikiMsg( 'nuke-nopages-global' ); |
211 | } else { |
212 | $out->addWikiMsg( 'nuke-nopages', $username ); |
213 | } |
214 | |
215 | $this->promptForm( $username ); |
216 | return; |
217 | } |
218 | |
219 | $out->addModules( 'ext.nuke.confirm' ); |
220 | $out->addModuleStyles( [ 'ext.nuke.styles', 'mediawiki.interface.helpers.styles' ] ); |
221 | |
222 | if ( $username === '' ) { |
223 | $out->addWikiMsg( 'nuke-list-multiple' ); |
224 | } else { |
225 | $out->addWikiMsg( 'nuke-list', $username ); |
226 | } |
227 | |
228 | $nuke = $this->getPageTitle(); |
229 | |
230 | $options = Xml::listDropdownOptions( |
231 | $this->msg( 'deletereason-dropdown' )->inContentLanguage()->text(), |
232 | [ 'other' => $this->msg( 'deletereasonotherlist' )->inContentLanguage()->text() ] |
233 | ); |
234 | |
235 | $dropdown = new FieldLayout( |
236 | new DropdownInputWidget( [ |
237 | 'name' => 'wpDeleteReasonList', |
238 | 'inputId' => 'wpDeleteReasonList', |
239 | 'tabIndex' => 1, |
240 | 'infusable' => true, |
241 | 'value' => '', |
242 | 'options' => Xml::listDropdownOptionsOoui( $options ), |
243 | ] ), |
244 | [ |
245 | 'label' => $this->msg( 'deletecomment' )->text(), |
246 | 'align' => 'top', |
247 | ] |
248 | ); |
249 | $reasonField = new FieldLayout( |
250 | new TextInputWidget( [ |
251 | 'name' => 'wpReason', |
252 | 'inputId' => 'wpReason', |
253 | 'tabIndex' => 2, |
254 | 'maxLength' => CommentStore::COMMENT_CHARACTER_LIMIT, |
255 | 'infusable' => true, |
256 | 'value' => $reason, |
257 | 'autofocus' => true, |
258 | ] ), |
259 | [ |
260 | 'label' => $this->msg( 'deleteotherreason' )->text(), |
261 | 'align' => 'top', |
262 | ] |
263 | ); |
264 | |
265 | $out->enableOOUI(); |
266 | $out->addHTML( |
267 | Html::openElement( 'form', [ |
268 | 'action' => $nuke->getLocalURL( 'action=delete' ), |
269 | 'method' => 'post', |
270 | 'name' => 'nukelist' ] |
271 | ) . |
272 | Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ) . |
273 | $dropdown . |
274 | $reasonField . |
275 | // Select: All, None, Invert |
276 | ( new ListToggle( $this->getOutput() ) )->getHTML() . |
277 | '<ul>' |
278 | ); |
279 | |
280 | $wordSeparator = $this->msg( 'word-separator' )->escaped(); |
281 | $commaSeparator = $this->msg( 'comma-separator' )->escaped(); |
282 | $pipeSeparator = $this->msg( 'pipe-separator' )->escaped(); |
283 | |
284 | $linkRenderer = $this->getLinkRenderer(); |
285 | $localRepo = $this->repoGroup->getLocalRepo(); |
286 | foreach ( $pages as [ $title, $userName ] ) { |
287 | /** |
288 | * @var $title Title |
289 | */ |
290 | |
291 | $image = $title->inNamespace( NS_FILE ) ? $localRepo->newFile( $title ) : false; |
292 | $thumb = $image && $image->exists() ? |
293 | $image->transform( [ 'width' => 120, 'height' => 120 ], 0 ) : |
294 | false; |
295 | |
296 | $userNameText = $userName ? |
297 | ' <span class="mw-changeslist-separator"></span> ' . $this->msg( 'nuke-editby', $userName )->parse() : |
298 | ''; |
299 | $changesLink = $linkRenderer->makeKnownLink( |
300 | $title, |
301 | $this->msg( 'nuke-viewchanges' )->text(), |
302 | [], |
303 | [ 'action' => 'history' ] |
304 | ); |
305 | |
306 | $talkPageText = $this->namespaceInfo->isTalk( $title->getNamespace() ) ? |
307 | '' : |
308 | $linkRenderer->makeLink( |
309 | $this->namespaceInfo->getTalkPage( $title ), |
310 | $this->msg( 'sp-contributions-talk' )->text(), |
311 | [], |
312 | [], |
313 | ) . $wordSeparator . $pipeSeparator; |
314 | |
315 | $query = $title->isRedirect() ? [ 'redirect' => 'no' ] : []; |
316 | $attributes = $title->isRedirect() ? [ 'class' => 'ext-nuke-italicize' ] : []; |
317 | $out->addHTML( '<li>' . |
318 | Html::check( |
319 | 'pages[]', |
320 | true, |
321 | [ 'value' => $title->getPrefixedDBkey() ] |
322 | ) . "\u{00A0}" . |
323 | ( $thumb ? $thumb->toHtml( [ 'desc-link' => true ] ) : '' ) . |
324 | $linkRenderer->makeKnownLink( $title, null, $attributes, $query ) . $wordSeparator . |
325 | $this->msg( 'parentheses' )->rawParams( $talkPageText . $changesLink )->escaped() . $wordSeparator . |
326 | "<span class='ext-nuke-italicize'>" . $userNameText . "</span>" . |
327 | "</li>\n" ); |
328 | } |
329 | |
330 | $out->addHTML( |
331 | "</ul>\n" . |
332 | Html::submitButton( $this->msg( 'nuke-submit-delete' )->text() ) . |
333 | '</form>' |
334 | ); |
335 | } |
336 | |
337 | /** |
338 | * Gets a list of new pages by the specified user or everyone when none is specified. |
339 | * |
340 | * @param string $username |
341 | * @param int $limit |
342 | * @param int|null $namespace |
343 | * |
344 | * @return array{0:Title,1:string|false}[] |
345 | */ |
346 | protected function getNewPages( $username, $limit, $namespace = null ): array { |
347 | $dbr = $this->dbProvider->getReplicaDatabase(); |
348 | $queryBuilder = $dbr->newSelectQueryBuilder() |
349 | ->select( [ 'page_title', 'page_namespace' ] ) |
350 | ->from( 'recentchanges' ) |
351 | ->join( 'actor', null, 'actor_id=rc_actor' ) |
352 | ->join( 'page', null, 'page_id=rc_cur_id' ) |
353 | ->where( |
354 | $dbr->expr( 'rc_source', '=', 'mw.new' )->orExpr( |
355 | $dbr->expr( 'rc_log_type', '=', 'upload' ) |
356 | ->and( 'rc_log_action', '=', 'upload' ) |
357 | ) |
358 | ) |
359 | ->orderBy( 'rc_timestamp', SelectQueryBuilder::SORT_DESC ) |
360 | ->limit( $limit ); |
361 | |
362 | if ( $username === '' ) { |
363 | $queryBuilder->field( 'actor_name', 'rc_user_text' ); |
364 | } else { |
365 | $queryBuilder->andWhere( [ 'actor_name' => $username ] ); |
366 | } |
367 | |
368 | if ( $namespace !== null ) { |
369 | $queryBuilder->andWhere( [ 'page_namespace' => $namespace ] ); |
370 | } |
371 | |
372 | $pattern = $this->getRequest()->getText( 'pattern' ); |
373 | if ( $pattern !== null && trim( $pattern ) !== '' ) { |
374 | $addedWhere = false; |
375 | |
376 | $pattern = trim( $pattern ); |
377 | $pattern = preg_replace( '/ +/', '`_', $pattern ); |
378 | $pattern = preg_replace( '/\\\\([%_])/', '`$1', $pattern ); |
379 | |
380 | if ( $namespace !== null ) { |
381 | // Custom namespace requested |
382 | // If that namespace capitalizes titles, capitalize the first character |
383 | // to match the DB title. |
384 | $pattern = $this->namespaceInfo->isCapitalized( $namespace ) ? |
385 | $this->contentLanguage->ucfirst( $pattern ) : $pattern; |
386 | } else { |
387 | // All namespaces requested |
388 | |
389 | $overriddenNamespaces = []; |
390 | $capitalLinks = $this->getConfig()->get( 'CapitalLinks' ); |
391 | $capitalLinkOverrides = $this->getConfig()->get( 'CapitalLinkOverrides' ); |
392 | // If there are any capital-overridden namespaces, keep track of them. "overridden" |
393 | // here means the namespace-specific value is not equal to $wgCapitalLinks. |
394 | foreach ( $capitalLinkOverrides as $k => $v ) { |
395 | if ( $v !== $capitalLinks ) { |
396 | $overriddenNamespaces[] = $k; |
397 | } |
398 | } |
399 | |
400 | if ( count( $overriddenNamespaces ) ) { |
401 | // If there are overridden namespaces, they have to be converted |
402 | // on a case-by-case basis. |
403 | |
404 | $validNamespaces = $this->namespaceInfo->getValidNamespaces(); |
405 | $nonOverriddenNamespaces = []; |
406 | foreach ( $validNamespaces as $ns ) { |
407 | if ( !in_array( $ns, $overriddenNamespaces ) ) { |
408 | // Put all namespaces that aren't overridden in $nonOverriddenNamespaces |
409 | $nonOverriddenNamespaces[] = $ns; |
410 | } |
411 | } |
412 | |
413 | $patternSpecific = $this->namespaceInfo->isCapitalized( $overriddenNamespaces[0] ) ? |
414 | $this->contentLanguage->ucfirst( $pattern ) : $pattern; |
415 | $orConditions = [ |
416 | $dbr->expr( |
417 | 'page_title', IExpression::LIKE, new LikeValue( |
418 | new LikeMatch( $patternSpecific ) |
419 | ) |
420 | )->and( |
421 | // IN condition |
422 | 'page_namespace', '=', $overriddenNamespaces |
423 | ) |
424 | ]; |
425 | if ( count( $nonOverriddenNamespaces ) ) { |
426 | $patternStandard = $this->namespaceInfo->isCapitalized( $nonOverriddenNamespaces[0] ) ? |
427 | $this->contentLanguage->ucfirst( $pattern ) : $pattern; |
428 | $orConditions[] = $dbr->expr( |
429 | 'page_title', IExpression::LIKE, new LikeValue( |
430 | new LikeMatch( $patternStandard ) |
431 | ) |
432 | )->and( |
433 | // IN condition, with the non-overridden namespaces. |
434 | // If the default is case-sensitive namespaces, $pattern's first |
435 | // character is turned lowercase. Otherwise, it is turned uppercase. |
436 | 'page_namespace', '=', $nonOverriddenNamespaces |
437 | ); |
438 | } |
439 | $queryBuilder->andWhere( $dbr->orExpr( $orConditions ) ); |
440 | $addedWhere = true; |
441 | } else { |
442 | // No overridden namespaces; just convert all titles. |
443 | $pattern = $this->namespaceInfo->isCapitalized( NS_MAIN ) ? |
444 | $this->contentLanguage->ucfirst( $pattern ) : $pattern; |
445 | } |
446 | } |
447 | |
448 | if ( !$addedWhere ) { |
449 | $queryBuilder->andWhere( |
450 | $dbr->expr( |
451 | 'page_title', |
452 | IExpression::LIKE, |
453 | new LikeValue( |
454 | new LikeMatch( $pattern ) |
455 | ) |
456 | ) |
457 | ); |
458 | } |
459 | } |
460 | |
461 | $result = $queryBuilder->caller( __METHOD__ )->fetchResultSet(); |
462 | /** @var array{0:Title,1:string|false}[] $pages */ |
463 | $pages = []; |
464 | foreach ( $result as $row ) { |
465 | $pages[] = [ |
466 | Title::makeTitle( $row->page_namespace, $row->page_title ), |
467 | $username === '' ? $row->rc_user_text : false |
468 | ]; |
469 | } |
470 | |
471 | // Allows other extensions to provide pages to be nuked that don't use |
472 | // the recentchanges table the way mediawiki-core does |
473 | $this->getNukeHookRunner()->onNukeGetNewPages( $username, $pattern, $namespace, $limit, $pages ); |
474 | |
475 | // Re-enforcing the limit *after* the hook because other extensions |
476 | // may add and/or remove pages. We need to make sure we don't end up |
477 | // with more pages than $limit. |
478 | if ( count( $pages ) > $limit ) { |
479 | $pages = array_slice( $pages, 0, $limit ); |
480 | } |
481 | |
482 | return $pages; |
483 | } |
484 | |
485 | /** |
486 | * Does the actual deletion of the pages. |
487 | * |
488 | * @param array $pages The pages to delete |
489 | * @param string $reason |
490 | * @throws PermissionsError |
491 | */ |
492 | protected function doDelete( array $pages, $reason ): void { |
493 | $res = []; |
494 | $jobs = []; |
495 | $user = $this->getUser(); |
496 | |
497 | $localRepo = $this->repoGroup->getLocalRepo(); |
498 | foreach ( $pages as $page ) { |
499 | $title = Title::newFromText( $page ); |
500 | |
501 | $deletionResult = false; |
502 | if ( !$this->getNukeHookRunner()->onNukeDeletePage( $title, $reason, $deletionResult ) ) { |
503 | $res[] = $this->msg( |
504 | $deletionResult ? 'nuke-deleted' : 'nuke-not-deleted', |
505 | wfEscapeWikiText( $title->getPrefixedText() ) |
506 | )->parse(); |
507 | continue; |
508 | } |
509 | |
510 | $permission_errors = $this->permissionManager->getPermissionErrors( 'delete', $user, $title ); |
511 | |
512 | if ( $permission_errors !== [] ) { |
513 | throw new PermissionsError( 'delete', $permission_errors ); |
514 | } |
515 | |
516 | $file = $title->getNamespace() === NS_FILE ? $localRepo->newFile( $title ) : false; |
517 | if ( $file ) { |
518 | // Must be passed by reference |
519 | $oldimage = null; |
520 | $status = FileDeleteForm::doDelete( |
521 | $title, |
522 | $file, |
523 | $oldimage, |
524 | $reason, |
525 | false, |
526 | $user |
527 | ); |
528 | } else { |
529 | $job = new DeletePageJob( [ |
530 | 'namespace' => $title->getNamespace(), |
531 | 'title' => $title->getDBKey(), |
532 | 'reason' => $reason, |
533 | 'userId' => $user->getId(), |
534 | 'wikiPageId' => $title->getId(), |
535 | 'suppress' => false, |
536 | 'tags' => '[]', |
537 | 'logsubtype' => 'delete', |
538 | ] ); |
539 | $jobs[] = $job; |
540 | $status = 'job'; |
541 | } |
542 | |
543 | if ( $status === 'job' ) { |
544 | $res[] = $this->msg( |
545 | 'nuke-deletion-queued', |
546 | wfEscapeWikiText( $title->getPrefixedText() ) |
547 | )->parse(); |
548 | } else { |
549 | $res[] = $this->msg( |
550 | $status->isOK() ? 'nuke-deleted' : 'nuke-not-deleted', |
551 | wfEscapeWikiText( $title->getPrefixedText() ) |
552 | )->parse(); |
553 | } |
554 | } |
555 | |
556 | if ( $jobs ) { |
557 | $this->jobQueueGroup->push( $jobs ); |
558 | } |
559 | |
560 | $this->getOutput()->addHTML( |
561 | "<ul>\n<li>" . |
562 | implode( "</li>\n<li>", $res ) . |
563 | "</li>\n</ul>\n" |
564 | ); |
565 | $this->getOutput()->addWikiMsg( 'nuke-delete-more' ); |
566 | } |
567 | |
568 | /** |
569 | * Return an array of subpages beginning with $search that this special page will accept. |
570 | * |
571 | * @param string $search Prefix to search for |
572 | * @param int $limit Maximum number of results to return (usually 10) |
573 | * @param int $offset Number of results to skip (usually 0) |
574 | * @return string[] Matching subpages |
575 | */ |
576 | public function prefixSearchSubpages( $search, $limit, $offset ) { |
577 | $search = $this->userNameUtils->getCanonical( $search ); |
578 | if ( !$search ) { |
579 | // No prefix suggestion for invalid user |
580 | return []; |
581 | } |
582 | |
583 | // Autocomplete subpage as user list - public to allow caching |
584 | return $this->userNamePrefixSearch |
585 | ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset ); |
586 | } |
587 | |
588 | /** |
589 | * Group Special:Nuke with pagetools |
590 | * |
591 | * @codeCoverageIgnore |
592 | * @return string |
593 | */ |
594 | protected function getGroupName() { |
595 | return 'pagetools'; |
596 | } |
597 | |
598 | private function getDeleteReason( WebRequest $request, string $target ): string { |
599 | $defaultReason = $target === '' |
600 | ? $this->msg( 'nuke-multiplepeople' )->inContentLanguage()->text() |
601 | : $this->msg( 'nuke-defaultreason', $target )->inContentLanguage()->text(); |
602 | |
603 | $dropdownSelection = $request->getText( 'wpDeleteReasonList', 'other' ); |
604 | $reasonInput = $request->getText( 'wpReason', $defaultReason ); |
605 | |
606 | if ( $dropdownSelection === 'other' ) { |
607 | return $reasonInput; |
608 | } elseif ( $reasonInput !== '' ) { |
609 | // Entry from drop down menu + additional comment |
610 | $separator = $this->msg( 'colon-separator' )->inContentLanguage()->text(); |
611 | return $dropdownSelection . $separator . $reasonInput; |
612 | } else { |
613 | return $dropdownSelection; |
614 | } |
615 | } |
616 | |
617 | private function getNukeHookRunner(): NukeHookRunner { |
618 | $this->hookRunner ??= new NukeHookRunner( $this->getHookContainer() ); |
619 | return $this->hookRunner; |
620 | } |
621 | } |