Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
99.26% |
267 / 269 |
|
93.33% |
14 / 15 |
CRAP | |
0.00% |
0 / 1 |
SpecialNuke | |
99.26% |
267 / 269 |
|
93.33% |
14 / 15 |
69 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
1 | |||
doesWrites | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
execute | |
100.00% |
37 / 37 |
|
100.00% |
1 / 1 |
14 | |||
getTempAccounts | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
3.07 | |||
getNukeContextFromRequest | |
100.00% |
34 / 34 |
|
100.00% |
1 / 1 |
8 | |||
getUIRenderer | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
4 | |||
loadNamespacesFromRequest | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
assertUserCanAccessTemporaryAccounts | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
showPromptForm | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
showListForm | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
showConfirmForm | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
showResultPage | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getNewPages | |
100.00% |
78 / 78 |
|
100.00% |
1 / 1 |
14 | |||
doDelete | |
100.00% |
45 / 45 |
|
100.00% |
1 / 1 |
8 | |||
prefixSearchSubpages | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getGroupName | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
getNukeHookRunner | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Nuke; |
4 | |
5 | use DateTime; |
6 | use DeletePageJob; |
7 | use ErrorPageError; |
8 | use JobQueueGroup; |
9 | use MediaWiki\CheckUser\Services\CheckUserTemporaryAccountsByIPLookup; |
10 | use MediaWiki\Extension\Nuke\Form\SpecialNukeHTMLFormUIRenderer; |
11 | use MediaWiki\Extension\Nuke\Form\SpecialNukeUIRenderer; |
12 | use MediaWiki\Extension\Nuke\Hooks\NukeHookRunner; |
13 | use MediaWiki\Language\Language; |
14 | use MediaWiki\Page\File\FileDeleteForm; |
15 | use MediaWiki\Permissions\PermissionManager; |
16 | use MediaWiki\Request\WebRequest; |
17 | use MediaWiki\SpecialPage\SpecialPage; |
18 | use MediaWiki\Status\Status; |
19 | use MediaWiki\Title\NamespaceInfo; |
20 | use MediaWiki\Title\Title; |
21 | use MediaWiki\User\Options\UserOptionsLookup; |
22 | use MediaWiki\User\User; |
23 | use MediaWiki\User\UserFactory; |
24 | use MediaWiki\User\UserNamePrefixSearch; |
25 | use MediaWiki\User\UserNameUtils; |
26 | use PermissionsError; |
27 | use RepoGroup; |
28 | use UserBlockedError; |
29 | use Wikimedia\IPUtils; |
30 | use Wikimedia\Rdbms\IConnectionProvider; |
31 | |
32 | class SpecialNuke extends SpecialPage { |
33 | |
34 | /** @var NukeHookRunner|null */ |
35 | private $hookRunner; |
36 | |
37 | private JobQueueGroup $jobQueueGroup; |
38 | private IConnectionProvider $dbProvider; |
39 | private PermissionManager $permissionManager; |
40 | private RepoGroup $repoGroup; |
41 | private UserFactory $userFactory; |
42 | private UserOptionsLookup $userOptionsLookup; |
43 | private UserNamePrefixSearch $userNamePrefixSearch; |
44 | private UserNameUtils $userNameUtils; |
45 | private NamespaceInfo $namespaceInfo; |
46 | private Language $contentLanguage; |
47 | /** @var CheckUserTemporaryAccountsByIPLookup|null */ |
48 | private $checkUserTemporaryAccountsByIPLookup = null; |
49 | |
50 | /** |
51 | * Action keyword for the "prompt" step. |
52 | */ |
53 | public const ACTION_PROMPT = 'prompt'; |
54 | /** |
55 | * Action keyword for the "list" step. |
56 | */ |
57 | public const ACTION_LIST = 'list'; |
58 | /** |
59 | * Action keyword for the "confirm" step. |
60 | */ |
61 | public const ACTION_CONFIRM = 'confirm'; |
62 | /** |
63 | * Action keyword for the "delete/results" step. |
64 | */ |
65 | public const ACTION_DELETE = 'delete'; |
66 | |
67 | /** |
68 | * Separator for the hidden "page list" fields. |
69 | */ |
70 | public const PAGE_LIST_SEPARATOR = '|'; |
71 | |
72 | /** |
73 | * Separator for the namespace list. This constant comes from the separator used by |
74 | * HTMLNamespacesMultiselectField. |
75 | */ |
76 | public const NAMESPACE_LIST_SEPARATOR = "\n"; |
77 | |
78 | /** |
79 | * @inheritDoc |
80 | */ |
81 | public function __construct( |
82 | JobQueueGroup $jobQueueGroup, |
83 | IConnectionProvider $dbProvider, |
84 | PermissionManager $permissionManager, |
85 | RepoGroup $repoGroup, |
86 | UserFactory $userFactory, |
87 | UserOptionsLookup $userOptionsLookup, |
88 | UserNamePrefixSearch $userNamePrefixSearch, |
89 | UserNameUtils $userNameUtils, |
90 | NamespaceInfo $namespaceInfo, |
91 | Language $contentLanguage, |
92 | $checkUserTemporaryAccountsByIPLookup = null |
93 | ) { |
94 | parent::__construct( 'Nuke', 'nuke' ); |
95 | $this->jobQueueGroup = $jobQueueGroup; |
96 | $this->dbProvider = $dbProvider; |
97 | $this->permissionManager = $permissionManager; |
98 | $this->repoGroup = $repoGroup; |
99 | $this->userFactory = $userFactory; |
100 | $this->userOptionsLookup = $userOptionsLookup; |
101 | $this->userNamePrefixSearch = $userNamePrefixSearch; |
102 | $this->userNameUtils = $userNameUtils; |
103 | $this->namespaceInfo = $namespaceInfo; |
104 | $this->contentLanguage = $contentLanguage; |
105 | $this->checkUserTemporaryAccountsByIPLookup = $checkUserTemporaryAccountsByIPLookup; |
106 | } |
107 | |
108 | /** |
109 | * @inheritDoc |
110 | * @codeCoverageIgnore |
111 | */ |
112 | public function doesWrites() { |
113 | return true; |
114 | } |
115 | |
116 | /** |
117 | * @param null|string $par |
118 | */ |
119 | public function execute( $par ) { |
120 | $this->setHeaders(); |
121 | $this->checkPermissions(); |
122 | $this->checkReadOnly(); |
123 | $this->outputHeader(); |
124 | $this->addHelpLink( 'Help:Extension:Nuke' ); |
125 | |
126 | $currentUser = $this->getUser(); |
127 | $block = $currentUser->getBlock(); |
128 | |
129 | // appliesToRight is presently a no-op, since there is no handling for `delete`, |
130 | // and so will return `null`. `true` will be returned if the block actively |
131 | // applies to `delete`, and both `null` and `true` should result in an error |
132 | if ( $block && ( $block->isSitewide() || |
133 | ( $block->appliesToRight( 'delete' ) !== false ) ) |
134 | ) { |
135 | throw new UserBlockedError( $block ); |
136 | } |
137 | |
138 | $req = $this->getRequest(); |
139 | $nukeContext = $this->getNukeContextFromRequest( $par ); |
140 | |
141 | if ( $nukeContext->validatePrompt() !== true ) { |
142 | // Something is wrong with filters. Immediately return the prompt form again. |
143 | $this->showPromptForm( $nukeContext ); |
144 | return; |
145 | } |
146 | |
147 | switch ( $nukeContext->getAction() ) { |
148 | case self::ACTION_DELETE: |
149 | case self::ACTION_CONFIRM: |
150 | if ( !$req->wasPosted() |
151 | || !$currentUser->matchEditToken( $req->getVal( 'wpEditToken' ) ) |
152 | ) { |
153 | // If the form was not posted or the edit token didn't match, something |
154 | // must have gone wrong. Show the prompt form again. |
155 | $this->showPromptForm( $nukeContext ); |
156 | break; |
157 | } |
158 | |
159 | if ( !$nukeContext->hasPages() ) { |
160 | if ( !$nukeContext->hasOriginalPages() ) { |
161 | // No pages were requested. This is an early confirm attempt without having |
162 | // listed the pages at all. Show the list form again. |
163 | $this->showPromptForm( $nukeContext ); |
164 | } else { |
165 | // Pages were not requested but a page list exists. The user did not select any |
166 | // pages. Show the list form again. |
167 | $this->showListForm( $nukeContext ); |
168 | } |
169 | break; |
170 | } |
171 | |
172 | if ( $nukeContext->getAction() === self::ACTION_DELETE ) { |
173 | $deletedPageStatuses = $this->doDelete( $nukeContext ); |
174 | $this->showResultPage( $nukeContext, $deletedPageStatuses ); |
175 | } else { |
176 | $this->showConfirmForm( $nukeContext ); |
177 | } |
178 | break; |
179 | case self::ACTION_LIST: |
180 | $this->showListForm( $nukeContext ); |
181 | break; |
182 | default: |
183 | $this->showPromptForm( $nukeContext ); |
184 | break; |
185 | } |
186 | } |
187 | |
188 | /** |
189 | * Return a list of temporary accounts that are known to have edited from the context's target. |
190 | * Calls to this method result in a log entry being generated for the logged-in user account |
191 | * making the request. |
192 | * |
193 | * @param NukeContext $context |
194 | * @return string[] A list of temporary account usernames associated with the IP address |
195 | */ |
196 | protected function getTempAccounts( NukeContext $context ): array { |
197 | if ( !$this->checkUserTemporaryAccountsByIPLookup ) { |
198 | return []; |
199 | } |
200 | $status = $this->checkUserTemporaryAccountsByIPLookup->get( |
201 | $context->getTarget(), |
202 | $this->getAuthority(), |
203 | true |
204 | ); |
205 | if ( $status->isGood() ) { |
206 | return $status->getValue(); |
207 | } |
208 | return []; |
209 | } |
210 | |
211 | /** |
212 | * Load the Nuke context from request data ({@link SpecialPage::getRequest}). |
213 | * |
214 | * @param string|null $par |
215 | * @return NukeContext |
216 | */ |
217 | protected function getNukeContextFromRequest( ?string $par ): NukeContext { |
218 | $req = $this->getRequest(); |
219 | |
220 | $target = trim( $req->getText( 'target', $par ?? '' ) ); |
221 | |
222 | // Normalise name |
223 | if ( $target !== '' ) { |
224 | $user = $this->userFactory->newFromName( $target ); |
225 | if ( $user ) { |
226 | $target = $user->getName(); |
227 | } |
228 | } |
229 | |
230 | $namespaces = $this->loadNamespacesFromRequest( $req ); |
231 | // Set $namespaces to null if it's empty |
232 | if ( count( $namespaces ) == 0 ) { |
233 | $namespaces = null; |
234 | } |
235 | |
236 | $action = $req->getRawVal( 'action' ); |
237 | if ( !$action ) { |
238 | if ( $target !== '' ) { |
239 | // Target was supplied but action was not. Imply 'list' action. |
240 | $action = self::ACTION_LIST; |
241 | } else { |
242 | $action = self::ACTION_PROMPT; |
243 | } |
244 | } |
245 | |
246 | // This uses a string value to avoid having to generate hundreds of hidden <input>s. |
247 | $originalPages = explode( |
248 | self::PAGE_LIST_SEPARATOR, |
249 | $req->getText( 'originalPageList' ) |
250 | ); |
251 | if ( count( $originalPages ) == 1 && $originalPages[0] == "" ) { |
252 | $originalPages = []; |
253 | } |
254 | |
255 | return new NukeContext( [ |
256 | 'requestContext' => $this->getContext(), |
257 | 'useTemporaryAccounts' => $this->checkUserTemporaryAccountsByIPLookup != null, |
258 | |
259 | 'action' => $action, |
260 | 'target' => $target, |
261 | 'listedTarget' => trim( $req->getText( 'listedTarget', $target ) ), |
262 | 'pattern' => $req->getText( 'pattern' ), |
263 | 'limit' => $req->getInt( 'limit', 500 ), |
264 | 'namespaces' => $namespaces, |
265 | |
266 | 'dateFrom' => $req->getText( 'wpdateFrom' ), |
267 | 'dateTo' => $req->getText( 'wpdateTo' ), |
268 | |
269 | 'pages' => $req->getArray( 'pages', [] ), |
270 | 'originalPages' => $originalPages |
271 | ] ); |
272 | } |
273 | |
274 | /** |
275 | * Get the UI renderer for a given type. |
276 | * |
277 | * @param NukeContext $context |
278 | * @return SpecialNukeUIRenderer |
279 | */ |
280 | protected function getUIRenderer( |
281 | NukeContext $context |
282 | ): SpecialNukeUIRenderer { |
283 | // Permit overriding the UI type with the `?nukeUI=` query parameter. |
284 | $formType = $this->getRequest()->getText( 'nukeUI' ); |
285 | if ( !$formType ) { |
286 | $formType = $this->getConfig()->get( NukeConfigNames::UIType ) ?? 'htmlform'; |
287 | } |
288 | |
289 | // Possible values: 'codex', 'htmlform' |
290 | switch ( $formType ) { |
291 | // case 'codex': to be implemented (T153988) |
292 | case 'htmlform': |
293 | default: |
294 | return new SpecialNukeHTMLFormUIRenderer( |
295 | $context, |
296 | $this, |
297 | $this->repoGroup, |
298 | $this->getLinkRenderer(), |
299 | $this->namespaceInfo, |
300 | $this->getLanguage() |
301 | ); |
302 | } |
303 | } |
304 | |
305 | /** |
306 | * Load namespaces from the provided request and return them as an array. This also performs |
307 | * validation, ensuring that only valid namespaces are returned. |
308 | * |
309 | * @param WebRequest $req The request |
310 | * @return array An array of namespace IDs |
311 | */ |
312 | private function loadNamespacesFromRequest( WebRequest $req ): array { |
313 | $validNamespaces = $this->namespaceInfo->getValidNamespaces(); |
314 | |
315 | return array_map( |
316 | 'intval', array_filter( |
317 | explode( self::NAMESPACE_LIST_SEPARATOR, $req->getText( "namespace" ) ), |
318 | static function ( $ns ) use ( $validNamespaces ) { |
319 | return is_numeric( $ns ) && in_array( intval( $ns ), $validNamespaces ); |
320 | } |
321 | ) |
322 | ); |
323 | } |
324 | |
325 | /** |
326 | * Does the user have the appropriate permissions and have they enabled in preferences? |
327 | * Adapted from MediaWiki\CheckUser\Api\Rest\Handler\AbstractTemporaryAccountHandler::checkPermissions |
328 | * |
329 | * @param User $currentUser |
330 | * |
331 | * @throws PermissionsError if the user does not have the 'checkuser-temporary-account' right |
332 | * @throws ErrorPageError if the user has not enabled the 'checkuser-temporary-account-enabled' preference |
333 | */ |
334 | private function assertUserCanAccessTemporaryAccounts( User $currentUser ) { |
335 | if ( |
336 | !$currentUser->isAllowed( 'checkuser-temporary-account-no-preference' ) |
337 | ) { |
338 | if ( |
339 | !$currentUser->isAllowed( 'checkuser-temporary-account' ) |
340 | ) { |
341 | throw new PermissionsError( 'checkuser-temporary-account' ); |
342 | } |
343 | if ( |
344 | !$this->userOptionsLookup->getOption( |
345 | $currentUser, |
346 | 'checkuser-temporary-account-enable' |
347 | ) |
348 | ) { |
349 | throw new ErrorPageError( |
350 | $this->msg( 'checkuser-ip-contributions-permission-error-title' ), |
351 | $this->msg( 'checkuser-ip-contributions-permission-error-description' ) |
352 | ); |
353 | } |
354 | } |
355 | } |
356 | |
357 | /** |
358 | * Prompt for a username or IP address. |
359 | * |
360 | * @param NukeContext $context |
361 | */ |
362 | public function showPromptForm( NukeContext $context ): void { |
363 | $this->getUIRenderer( $context ) |
364 | ->showPromptForm(); |
365 | } |
366 | |
367 | /** |
368 | * Display the prompt form and a list of pages to delete. |
369 | * |
370 | * @param NukeContext $context |
371 | */ |
372 | public function showListForm( NukeContext $context ): void { |
373 | // Check for temporary accounts, if applicable. |
374 | $tempAccounts = []; |
375 | if ( |
376 | $this->checkUserTemporaryAccountsByIPLookup && |
377 | IPUtils::isValid( $context->getTarget() ) |
378 | ) { |
379 | // if the target is an ip address and temp account lookup is available, |
380 | // list pages created by the ip user or by temp accounts associated with the ip address |
381 | $this->assertUserCanAccessTemporaryAccounts( $this->getUser() ); |
382 | $tempAccounts = $this->getTempAccounts( $context ); |
383 | } |
384 | |
385 | // Get list of pages to show the user. |
386 | $pages = $this->getNewPages( $context, $tempAccounts ); |
387 | |
388 | $this->getUIRenderer( $context ) |
389 | ->showListForm( $pages ); |
390 | } |
391 | |
392 | /** |
393 | * Display a page confirming all pages to be deleted. |
394 | * |
395 | * @param NukeContext $context |
396 | * |
397 | * @return void |
398 | */ |
399 | public function showConfirmForm( NukeContext $context ): void { |
400 | $this->getUIRenderer( $context ) |
401 | ->showConfirmForm(); |
402 | } |
403 | |
404 | /** |
405 | * Show the result page, showing what pages were deleted and what pages were skipped by the |
406 | * user. |
407 | * |
408 | * @param NukeContext $context |
409 | * deletion. Can be either `"job"` to indicate that the page was queued for deletion, a |
410 | * {@link Status} to indicate if the page was successfully deleted, or `false` if the user |
411 | * did not select the page for deletion. |
412 | * @param (Status|string|boolean)[] $deletedPageStatuses The status for each page queued for |
413 | * @return void |
414 | */ |
415 | public function showResultPage( NukeContext $context, array $deletedPageStatuses ): void { |
416 | $this->getUIRenderer( $context ) |
417 | ->showResultPage( $deletedPageStatuses ); |
418 | } |
419 | |
420 | /** |
421 | * Gets a list of new pages by the specified user or everyone when none is specified. |
422 | * |
423 | * @param NukeContext $context |
424 | * @param string[] $tempAccounts Temporary accounts to search for. This is passed directly |
425 | * instead of through context to ensure permissions checks happen first. |
426 | * |
427 | * @return array{0:Title,1:string|false}[] |
428 | */ |
429 | protected function getNewPages( NukeContext $context, array $tempAccounts = [] ): array { |
430 | $dbr = $this->dbProvider->getReplicaDatabase(); |
431 | |
432 | $nukeMaxAge = $context->getNukeMaxAge(); |
433 | |
434 | $min = $context->getDateFrom(); |
435 | if ( !$min || $min->getTimestamp() < time() - $nukeMaxAge ) { |
436 | // Requested $min is way too far in the past (or null). Set it to the earliest possible |
437 | // value. |
438 | $min = time() - $nukeMaxAge; |
439 | } else { |
440 | $min = $min->getTimestamp(); |
441 | } |
442 | |
443 | $max = $context->getDateTo(); |
444 | if ( $max ) { |
445 | // Increment by 1 day to include all edits from that day. |
446 | $max = ( clone $max ) |
447 | ->modify( "+1 day" ) |
448 | ->getTimestamp(); |
449 | } |
450 | // $min and $max are int|null here. |
451 | |
452 | if ( $max && $max < $min ) { |
453 | // Impossible range. Skip the query and fail gracefully. |
454 | return []; |
455 | } |
456 | if ( $min > time() ) { |
457 | // Improbable range (since revisions cannot be in the future). |
458 | // Skip the query and fail gracefully. |
459 | return []; |
460 | } |
461 | $maxPossibleDate = ( new DateTime() ) |
462 | ->modify( "+1 day" ) |
463 | ->getTimestamp(); |
464 | if ( $max > $maxPossibleDate ) { |
465 | // Truncate to the current day, since there shouldn't be any future revisions. |
466 | $max = $maxPossibleDate; |
467 | } |
468 | |
469 | $target = $context->getTarget(); |
470 | if ( $target ) { |
471 | // Enable revision table searches only when a target has been specified. |
472 | // Running queries on the revision table when there's no actor causes timeouts, since |
473 | // the entirety of the `page` table needs to be scanned. (T380846) |
474 | $nukeQueryBuilder = new NukeQueryBuilder( |
475 | $dbr, |
476 | $this->getConfig(), |
477 | $this->namespaceInfo, |
478 | $this->contentLanguage, |
479 | NukeQueryBuilder::TABLE_REVISION |
480 | ); |
481 | } else { |
482 | // Switch to `recentchanges` table searching when running an all-user search. (T380846) |
483 | $nukeQueryBuilder = new NukeQueryBuilder( |
484 | $dbr, |
485 | $this->getConfig(), |
486 | $this->namespaceInfo, |
487 | $this->contentLanguage, |
488 | NukeQueryBuilder::TABLE_RECENTCHANGES |
489 | ); |
490 | } |
491 | |
492 | // Follow the `$wgNukeMaxAge` config variable, or the user-specified minimum date. |
493 | $nukeQueryBuilder->filterFromTimestamp( $min ); |
494 | |
495 | // Follow the user-specified maximum date, if applicable. |
496 | if ( $max ) { |
497 | $nukeQueryBuilder->filterToTimestamp( $max ); |
498 | } |
499 | |
500 | // Limit the number of rows that can be returned by the query. |
501 | $limit = $context->getLimit(); |
502 | $nukeQueryBuilder->limit( $limit ); |
503 | |
504 | // Filter by actors, if applicable. |
505 | $nukeQueryBuilder->filterActor( array_filter( [ $target, ...$tempAccounts ] ) ); |
506 | |
507 | // Filter by namespace, if applicable |
508 | $namespaces = $context->getNamespaces(); |
509 | $nukeQueryBuilder->filterNamespaces( $namespaces ); |
510 | |
511 | // Filter by pattern, if applicable |
512 | $pattern = $context->getPattern(); |
513 | $nukeQueryBuilder->filterPattern( |
514 | $pattern, |
515 | $namespaces |
516 | ); |
517 | |
518 | $result = $nukeQueryBuilder |
519 | ->build() |
520 | ->caller( __METHOD__ ) |
521 | ->fetchResultSet(); |
522 | /** @var array{0:Title,1:string|false}[] $pages */ |
523 | $pages = []; |
524 | foreach ( $result as $row ) { |
525 | $pages[] = [ |
526 | Title::makeTitle( $row->page_namespace, $row->page_title ), |
527 | $row->actor_name |
528 | ]; |
529 | } |
530 | |
531 | // Allows other extensions to provide pages to be mass-deleted that |
532 | // don't use the revision table the way mediawiki-core does. |
533 | if ( $namespaces ) { |
534 | foreach ( $namespaces as $namespace ) { |
535 | $this->getNukeHookRunner()->onNukeGetNewPages( |
536 | $target, |
537 | $pattern, |
538 | $namespace, |
539 | $limit, |
540 | $pages |
541 | ); |
542 | } |
543 | } else { |
544 | $this->getNukeHookRunner()->onNukeGetNewPages( |
545 | $target, |
546 | $pattern, |
547 | null, |
548 | $limit, |
549 | $pages |
550 | ); |
551 | } |
552 | |
553 | // Re-enforcing the limit *after* the hook because other extensions |
554 | // may add and/or remove pages. We need to make sure we don't end up |
555 | // with more pages than $limit. |
556 | if ( count( $pages ) > $limit ) { |
557 | $pages = array_slice( $pages, 0, $limit ); |
558 | } |
559 | |
560 | return $pages; |
561 | } |
562 | |
563 | /** |
564 | * Does the actual deletion of the pages. |
565 | * |
566 | * @return array An associative array of statuses (or the string "job") keyed by the page title |
567 | * @throws PermissionsError |
568 | */ |
569 | protected function doDelete( NukeContext $context ): array { |
570 | $statuses = []; |
571 | $jobs = []; |
572 | $user = $this->getUser(); |
573 | |
574 | $reason = $context->getDeleteReason(); |
575 | $localRepo = $this->repoGroup->getLocalRepo(); |
576 | foreach ( $context->getPages() as $page ) { |
577 | $title = Title::newFromText( $page ); |
578 | |
579 | $deletionResult = false; |
580 | if ( !$this->getNukeHookRunner()->onNukeDeletePage( $title, $reason, $deletionResult ) ) { |
581 | $statuses[$title->getPrefixedDBkey()] = $deletionResult ? |
582 | Status::newGood() : |
583 | Status::newFatal( |
584 | $this->msg( 'nuke-not-deleted' ) |
585 | ); |
586 | continue; |
587 | } |
588 | |
589 | $permission_errors = $this->permissionManager->getPermissionErrors( 'delete', $user, $title ); |
590 | |
591 | if ( $permission_errors !== [] ) { |
592 | throw new PermissionsError( 'delete', $permission_errors ); |
593 | } |
594 | |
595 | $file = $title->getNamespace() === NS_FILE ? $localRepo->newFile( $title ) : false; |
596 | if ( $file ) { |
597 | // Must be passed by reference |
598 | $oldimage = null; |
599 | $status = FileDeleteForm::doDelete( |
600 | $title, |
601 | $file, |
602 | $oldimage, |
603 | $reason, |
604 | false, |
605 | $user |
606 | ); |
607 | } else { |
608 | $job = new DeletePageJob( [ |
609 | 'namespace' => $title->getNamespace(), |
610 | 'title' => $title->getDBKey(), |
611 | 'reason' => $reason, |
612 | 'userId' => $user->getId(), |
613 | 'wikiPageId' => $title->getId(), |
614 | 'suppress' => false, |
615 | 'tags' => '["nuke"]', |
616 | 'logsubtype' => 'delete', |
617 | ] ); |
618 | $jobs[] = $job; |
619 | $status = 'job'; |
620 | } |
621 | |
622 | $statuses[$title->getPrefixedDBkey()] = $status; |
623 | } |
624 | |
625 | if ( $jobs ) { |
626 | $this->jobQueueGroup->push( $jobs ); |
627 | } |
628 | |
629 | return $statuses; |
630 | } |
631 | |
632 | /** |
633 | * Return an array of subpages beginning with $search that this special page will accept. |
634 | * |
635 | * @param string $search Prefix to search for |
636 | * @param int $limit Maximum number of results to return (usually 10) |
637 | * @param int $offset Number of results to skip (usually 0) |
638 | * @return string[] Matching subpages |
639 | */ |
640 | public function prefixSearchSubpages( $search, $limit, $offset ) { |
641 | $search = $this->userNameUtils->getCanonical( $search ); |
642 | if ( !$search ) { |
643 | // No prefix suggestion for invalid user |
644 | return []; |
645 | } |
646 | |
647 | // Autocomplete subpage as user list - public to allow caching |
648 | return $this->userNamePrefixSearch |
649 | ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset ); |
650 | } |
651 | |
652 | /** |
653 | * Group Special:Nuke with pagetools |
654 | * |
655 | * @codeCoverageIgnore |
656 | * @return string |
657 | */ |
658 | protected function getGroupName() { |
659 | return 'pagetools'; |
660 | } |
661 | |
662 | private function getNukeHookRunner(): NukeHookRunner { |
663 | $this->hookRunner ??= new NukeHookRunner( $this->getHookContainer() ); |