Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 348 |
|
0.00% |
0 / 28 |
CRAP | |
0.00% |
0 / 1 |
DeleteAction | |
0.00% |
0 / 348 |
|
0.00% |
0 / 28 |
4692 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
getName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onSubmit | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onSuccess | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
usesOOUI | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPageTitle | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getRestriction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
alterForm | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
show | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
tempDelete | |
0.00% |
0 / 75 |
|
0.00% |
0 / 1 |
132 | |||
showSuccessMessages | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
72 | |||
showEditedWarning | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
showHistoryWarnings | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
6 | |||
showFormWarnings | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
showBacklinksWarning | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
showSubpagesWarnings | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
20 | |||
tempConfirmDelete | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
30 | |||
showEditReasonsLinks | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
12 | |||
isSuppressionAllowed | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getFormFields | |
0.00% |
0 / 70 |
|
0.00% |
0 / 1 |
42 | |||
getDeleteReason | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
showLogEntries | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
prepareOutputForForm | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getFormMessages | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
getFormMsg | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getFormAction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDefaultReason | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
pageHasHistory | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License |
14 | * along with this program; if not, write to the Free Software |
15 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA |
16 | * |
17 | * @file |
18 | * @ingroup Actions |
19 | */ |
20 | |
21 | use MediaWiki\Cache\BacklinkCacheFactory; |
22 | use MediaWiki\CommentStore\CommentStore; |
23 | use MediaWiki\Context\IContextSource; |
24 | use MediaWiki\Html\Html; |
25 | use MediaWiki\HTMLForm\HTMLForm; |
26 | use MediaWiki\Linker\LinkRenderer; |
27 | use MediaWiki\MainConfigNames; |
28 | use MediaWiki\MediaWikiServices; |
29 | use MediaWiki\Message\Message; |
30 | use MediaWiki\Page\DeletePage; |
31 | use MediaWiki\Page\DeletePageFactory; |
32 | use MediaWiki\Revision\RevisionRecord; |
33 | use MediaWiki\Session\CsrfTokenSet; |
34 | use MediaWiki\Status\Status; |
35 | use MediaWiki\Title\NamespaceInfo; |
36 | use MediaWiki\Title\TitleFactory; |
37 | use MediaWiki\Title\TitleFormatter; |
38 | use MediaWiki\User\Options\UserOptionsLookup; |
39 | use MediaWiki\Watchlist\WatchlistManager; |
40 | use Wikimedia\Rdbms\IConnectionProvider; |
41 | use Wikimedia\Rdbms\ReadOnlyMode; |
42 | use Wikimedia\RequestTimeout\TimeoutException; |
43 | |
44 | /** |
45 | * Handle page deletion |
46 | * |
47 | * @ingroup Actions |
48 | */ |
49 | class DeleteAction extends FormAction { |
50 | |
51 | /** |
52 | * Constants used to localize form fields |
53 | */ |
54 | protected const MSG_REASON_DROPDOWN = 'reason-dropdown'; |
55 | protected const MSG_REASON_DROPDOWN_SUPPRESS = 'reason-dropdown-suppress'; |
56 | protected const MSG_REASON_DROPDOWN_OTHER = 'reason-dropdown-other'; |
57 | protected const MSG_COMMENT = 'comment'; |
58 | protected const MSG_REASON_OTHER = 'reason-other'; |
59 | protected const MSG_SUBMIT = 'submit'; |
60 | protected const MSG_LEGEND = 'legend'; |
61 | protected const MSG_EDIT_REASONS = 'edit-reasons'; |
62 | protected const MSG_EDIT_REASONS_SUPPRESS = 'edit-reasons-suppress'; |
63 | |
64 | protected WatchlistManager $watchlistManager; |
65 | protected LinkRenderer $linkRenderer; |
66 | private BacklinkCacheFactory $backlinkCacheFactory; |
67 | protected ReadOnlyMode $readOnlyMode; |
68 | protected UserOptionsLookup $userOptionsLookup; |
69 | private DeletePageFactory $deletePageFactory; |
70 | private int $deleteRevisionsLimit; |
71 | private NamespaceInfo $namespaceInfo; |
72 | private TitleFormatter $titleFormatter; |
73 | private TitleFactory $titleFactory; |
74 | |
75 | private IConnectionProvider $dbProvider; |
76 | |
77 | /** |
78 | * @inheritDoc |
79 | */ |
80 | public function __construct( Article $article, IContextSource $context ) { |
81 | parent::__construct( $article, $context ); |
82 | $services = MediaWikiServices::getInstance(); |
83 | $this->watchlistManager = $services->getWatchlistManager(); |
84 | $this->linkRenderer = $services->getLinkRenderer(); |
85 | $this->backlinkCacheFactory = $services->getBacklinkCacheFactory(); |
86 | $this->readOnlyMode = $services->getReadOnlyMode(); |
87 | $this->userOptionsLookup = $services->getUserOptionsLookup(); |
88 | $this->deletePageFactory = $services->getDeletePageFactory(); |
89 | $this->deleteRevisionsLimit = $services->getMainConfig()->get( MainConfigNames::DeleteRevisionsLimit ); |
90 | $this->namespaceInfo = $services->getNamespaceInfo(); |
91 | $this->titleFormatter = $services->getTitleFormatter(); |
92 | $this->titleFactory = $services->getTitleFactory(); |
93 | $this->dbProvider = $services->getConnectionProvider(); |
94 | } |
95 | |
96 | public function getName() { |
97 | return 'delete'; |
98 | } |
99 | |
100 | public function onSubmit( $data ) { |
101 | return false; |
102 | } |
103 | |
104 | public function onSuccess() { |
105 | return false; |
106 | } |
107 | |
108 | protected function usesOOUI() { |
109 | return true; |
110 | } |
111 | |
112 | protected function getPageTitle() { |
113 | $title = $this->getTitle(); |
114 | return $this->msg( 'delete-confirm' )->plaintextParams( $title->getPrefixedText() ); |
115 | } |
116 | |
117 | public function getRestriction() { |
118 | return 'delete'; |
119 | } |
120 | |
121 | protected function alterForm( HTMLForm $form ) { |
122 | $title = $this->getTitle(); |
123 | $form |
124 | ->setAction( $this->getFormAction() ) |
125 | ->setWrapperLegendMsg( $this->getFormMsg( self::MSG_LEGEND ) ) |
126 | ->setWrapperAttributes( [ 'id' => 'mw-delete-table' ] ) |
127 | ->suppressDefaultSubmit() |
128 | ->setId( 'deleteconfirm' ) |
129 | ->setTokenSalt( [ 'delete', $title->getPrefixedText() ] ); |
130 | } |
131 | |
132 | public function show() { |
133 | $this->setHeaders(); |
134 | $this->useTransactionalTimeLimit(); |
135 | $this->addHelpLink( 'Help:Sysop deleting and undeleting' ); |
136 | |
137 | // This will throw exceptions if there's a problem |
138 | $this->checkCanExecute( $this->getUser() ); |
139 | |
140 | $this->tempDelete(); |
141 | } |
142 | |
143 | protected function tempDelete() { |
144 | $article = $this->getArticle(); |
145 | $title = $this->getTitle(); |
146 | $context = $this->getContext(); |
147 | $user = $context->getUser(); |
148 | $request = $context->getRequest(); |
149 | $outputPage = $context->getOutput(); |
150 | |
151 | # Better double-check that it hasn't been deleted yet! |
152 | $article->getPage()->loadPageData( |
153 | $request->wasPosted() ? IDBAccessObject::READ_LATEST : IDBAccessObject::READ_NORMAL |
154 | ); |
155 | if ( !$article->getPage()->exists() ) { |
156 | $outputPage->setPageTitleMsg( |
157 | $context->msg( 'cannotdelete-title' )->plaintextParams( $title->getPrefixedText() ) |
158 | ); |
159 | $outputPage->wrapWikiMsg( "<div class=\"error mw-error-cannotdelete\">\n$1\n</div>", |
160 | [ 'cannotdelete', wfEscapeWikiText( $title->getPrefixedText() ) ] |
161 | ); |
162 | $this->showLogEntries(); |
163 | |
164 | return; |
165 | } |
166 | |
167 | $hasValidCsrfToken = $this->getContext() |
168 | ->getCsrfTokenSet() |
169 | ->matchTokenField( |
170 | CsrfTokenSet::DEFAULT_FIELD_NAME, |
171 | [ 'delete', $title->getPrefixedText() ] |
172 | ); |
173 | |
174 | # If we are not processing the results of the deletion confirmation dialog, show the form |
175 | if ( !$request->wasPosted() || !$hasValidCsrfToken ) { |
176 | $this->tempConfirmDelete(); |
177 | return; |
178 | } |
179 | |
180 | # Check to make sure the page has not been edited while the deletion was being confirmed |
181 | if ( $article->getRevIdFetched() !== $request->getIntOrNull( 'wpConfirmationRevId' ) ) { |
182 | $this->showEditedWarning(); |
183 | $this->tempConfirmDelete(); |
184 | return; |
185 | } |
186 | |
187 | # Flag to hide all contents of the archived revisions |
188 | $suppress = $request->getCheck( 'wpSuppress' ) && |
189 | $context->getAuthority()->isAllowed( 'suppressrevision' ); |
190 | |
191 | $context = $this->getContext(); |
192 | $deletePage = $this->deletePageFactory->newDeletePage( |
193 | $this->getWikiPage(), |
194 | $context->getAuthority() |
195 | ); |
196 | $shouldDeleteTalk = $request->getCheck( 'wpDeleteTalk' ) && |
197 | $deletePage->canProbablyDeleteAssociatedTalk()->isGood(); |
198 | $deletePage->setDeleteAssociatedTalk( $shouldDeleteTalk ); |
199 | $status = $deletePage |
200 | ->setSuppress( $suppress ) |
201 | ->deleteIfAllowed( $this->getDeleteReason() ); |
202 | |
203 | if ( $status->isOK() ) { |
204 | $outputPage->setPageTitleMsg( $this->msg( 'actioncomplete' ) ); |
205 | $outputPage->setRobotPolicy( 'noindex,nofollow' ); |
206 | |
207 | if ( !$status->isGood() ) { |
208 | // If the page (and/or its talk) couldn't be found (e.g. because it was deleted in another request), |
209 | // let the user know. |
210 | $outputPage->addHTML( |
211 | Html::warningBox( |
212 | $outputPage->parseAsContent( |
213 | Status::wrap( $status )->getWikiText( |
214 | false, |
215 | false, |
216 | $context->getLanguage() |
217 | ) |
218 | ) |
219 | ) |
220 | ); |
221 | } |
222 | |
223 | $this->showSuccessMessages( |
224 | $deletePage->getSuccessfulDeletionsIDs(), |
225 | $deletePage->deletionsWereScheduled() |
226 | ); |
227 | |
228 | if ( !$status->isGood() ) { |
229 | $this->showLogEntries(); |
230 | } |
231 | $outputPage->returnToMain(); |
232 | } else { |
233 | $outputPage->setPageTitleMsg( |
234 | $this->msg( 'cannotdelete-title' )->plaintextParams( $this->getTitle()->getPrefixedText() ) |
235 | ); |
236 | |
237 | $outputPage->wrapWikiTextAsInterface( |
238 | 'error mw-error-cannotdelete', |
239 | Status::wrap( $status )->getWikiText( false, false, $context->getLanguage() ) |
240 | ); |
241 | $this->showLogEntries(); |
242 | } |
243 | |
244 | $this->watchlistManager->setWatch( $request->getCheck( 'wpWatch' ), $context->getAuthority(), $title ); |
245 | } |
246 | |
247 | /** |
248 | * Display success messages |
249 | * |
250 | * @param array $deleted |
251 | * @param array $scheduled |
252 | * @return void |
253 | */ |
254 | private function showSuccessMessages( array $deleted, array $scheduled ): void { |
255 | $outputPage = $this->getContext()->getOutput(); |
256 | $loglink = '[[Special:Log/delete|' . $this->msg( 'deletionlog' )->text() . ']]'; |
257 | $pageBaseDisplayTitle = wfEscapeWikiText( $this->getTitle()->getPrefixedText() ); |
258 | $pageTalkDisplayTitle = wfEscapeWikiText( $this->titleFormatter->getPrefixedText( |
259 | $this->namespaceInfo->getTalkPage( $this->getTitle() ) |
260 | ) ); |
261 | |
262 | $deletedTalk = $deleted[DeletePage::PAGE_TALK] ?? false; |
263 | $deletedBase = $deleted[DeletePage::PAGE_BASE]; |
264 | $scheduledTalk = $scheduled[DeletePage::PAGE_TALK] ?? false; |
265 | $scheduledBase = $scheduled[DeletePage::PAGE_BASE]; |
266 | |
267 | if ( $deletedBase && $deletedTalk ) { |
268 | $outputPage->addWikiMsg( 'deleted-page-and-talkpage', |
269 | $pageBaseDisplayTitle, |
270 | $pageTalkDisplayTitle, |
271 | $loglink ); |
272 | } elseif ( $deletedBase ) { |
273 | $outputPage->addWikiMsg( 'deletedtext', $pageBaseDisplayTitle, $loglink ); |
274 | } elseif ( $deletedTalk ) { |
275 | $outputPage->addWikiMsg( 'deletedtext', $pageTalkDisplayTitle, $loglink ); |
276 | } |
277 | |
278 | // run hook if article was deleted |
279 | if ( $deletedBase ) { |
280 | $this->getHookRunner()->onArticleDeleteAfterSuccess( $this->getTitle(), $outputPage ); |
281 | } |
282 | |
283 | if ( $scheduledBase ) { |
284 | $outputPage->addWikiMsg( 'delete-scheduled', $pageBaseDisplayTitle ); |
285 | } |
286 | |
287 | if ( $scheduledTalk ) { |
288 | $outputPage->addWikiMsg( 'delete-scheduled', $pageTalkDisplayTitle ); |
289 | } |
290 | } |
291 | |
292 | protected function showEditedWarning(): void { |
293 | $this->getOutput()->addHTML( |
294 | Html::warningBox( $this->getContext()->msg( 'editedwhiledeleting' )->parse() ) |
295 | ); |
296 | } |
297 | |
298 | private function showHistoryWarnings(): void { |
299 | $context = $this->getContext(); |
300 | $title = $this->getTitle(); |
301 | |
302 | // The following can use the real revision count as this is only being shown for users |
303 | // that can delete this page. |
304 | // This, as a side-effect, also makes sure that the following query isn't being run for |
305 | // pages with a larger history, unless the user has the 'bigdelete' right |
306 | // (and is about to delete this page). |
307 | $revisions = (int)$this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder() |
308 | ->select( 'COUNT(rev_page)' ) |
309 | ->from( 'revision' ) |
310 | ->where( [ 'rev_page' => $title->getArticleID() ] ) |
311 | ->caller( __METHOD__ ) |
312 | ->fetchField(); |
313 | |
314 | // @todo i18n issue/patchwork message |
315 | $context->getOutput()->addHTML( |
316 | '<strong class="mw-delete-warning-revisions">' . |
317 | $context->msg( 'historywarning' )->numParams( $revisions )->parse() . |
318 | $context->msg( 'word-separator' )->escaped() . $this->linkRenderer->makeKnownLink( |
319 | $title, |
320 | $context->msg( 'history' )->text(), |
321 | [], |
322 | [ 'action' => 'history' ] ) . |
323 | '</strong>' |
324 | ); |
325 | |
326 | if ( $title->isBigDeletion() ) { |
327 | $context->getOutput()->wrapWikiMsg( "<div class='error'>\n$1\n</div>\n", |
328 | [ |
329 | 'delete-warning-toobig', |
330 | $context->getLanguage()->formatNum( $this->deleteRevisionsLimit ) |
331 | ] |
332 | ); |
333 | } |
334 | } |
335 | |
336 | protected function showFormWarnings(): void { |
337 | $this->showBacklinksWarning(); |
338 | $this->showSubpagesWarnings(); |
339 | } |
340 | |
341 | private function showBacklinksWarning(): void { |
342 | $backlinkCache = $this->backlinkCacheFactory->getBacklinkCache( $this->getTitle() ); |
343 | if ( $backlinkCache->hasLinks( 'pagelinks' ) || $backlinkCache->hasLinks( 'templatelinks' ) ) { |
344 | $this->getOutput()->addHTML( |
345 | Html::warningBox( |
346 | $this->msg( 'deleting-backlinks-warning' )->parse(), |
347 | 'plainlinks' |
348 | ) |
349 | ); |
350 | } |
351 | } |
352 | |
353 | protected function showSubpagesWarnings(): void { |
354 | $title = $this->getTitle(); |
355 | $subpageCount = count( $title->getSubpages( 51 ) ); |
356 | if ( $subpageCount ) { |
357 | $this->getOutput()->addHTML( |
358 | Html::warningBox( |
359 | $this->msg( 'deleting-subpages-warning' )->numParams( $subpageCount )->parse(), |
360 | 'plainlinks' |
361 | ) |
362 | ); |
363 | } |
364 | |
365 | if ( !$title->isTalkPage() ) { |
366 | $talkPageTitle = $this->titleFactory->newFromLinkTarget( $this->namespaceInfo->getTalkPage( $title ) ); |
367 | $subpageCount = count( $talkPageTitle->getSubpages( 51 ) ); |
368 | if ( $subpageCount ) { |
369 | $this->getOutput()->addHTML( |
370 | Html::warningBox( |
371 | $this->msg( 'deleting-talkpage-subpages-warning' )->numParams( $subpageCount )->parse(), |
372 | 'plainlinks' |
373 | ) |
374 | ); |
375 | } |
376 | } |
377 | } |
378 | |
379 | private function tempConfirmDelete(): void { |
380 | $this->prepareOutputForForm(); |
381 | $context = $this->getContext(); |
382 | $outputPage = $context->getOutput(); |
383 | $article = $this->getArticle(); |
384 | |
385 | $reason = $this->getDefaultReason(); |
386 | |
387 | // oldid is set to the revision id of the page when the page was displayed. |
388 | // Check to make sure the page has not been edited between loading the page |
389 | // and clicking the delete link |
390 | $oldid = $context->getRequest()->getIntOrNull( 'oldid' ); |
391 | if ( $oldid !== null && $oldid !== $article->getRevIdFetched() ) { |
392 | $this->showEditedWarning(); |
393 | } |
394 | // If the page has a history, insert a warning |
395 | if ( $this->pageHasHistory() ) { |
396 | $this->showHistoryWarnings(); |
397 | } |
398 | $this->showFormWarnings(); |
399 | |
400 | $outputPage->addWikiMsg( 'confirmdeletetext' ); |
401 | |
402 | // FIXME: Replace (or at least rename) this hook |
403 | $this->getHookRunner()->onArticleConfirmDelete( $this->getArticle(), $outputPage, $reason ); |
404 | |
405 | $form = $this->getForm(); |
406 | if ( $form->show() ) { |
407 | $this->onSuccess(); |
408 | } |
409 | |
410 | $this->showEditReasonsLinks(); |
411 | $this->showLogEntries(); |
412 | } |
413 | |
414 | protected function showEditReasonsLinks(): void { |
415 | if ( $this->getAuthority()->isAllowed( 'editinterface' ) ) { |
416 | $link = ''; |
417 | if ( $this->isSuppressionAllowed() ) { |
418 | $link .= $this->linkRenderer->makeKnownLink( |
419 | $this->getFormMsg( self::MSG_REASON_DROPDOWN_SUPPRESS )->inContentLanguage()->getTitle(), |
420 | $this->getFormMsg( self::MSG_EDIT_REASONS_SUPPRESS )->text(), |
421 | [], |
422 | [ 'action' => 'edit' ] |
423 | ); |
424 | $link .= $this->msg( 'pipe-separator' )->escaped(); |
425 | } |
426 | $link .= $this->linkRenderer->makeKnownLink( |
427 | $this->getFormMsg( self::MSG_REASON_DROPDOWN )->inContentLanguage()->getTitle(), |
428 | $this->getFormMsg( self::MSG_EDIT_REASONS )->text(), |
429 | [], |
430 | [ 'action' => 'edit' ] |
431 | ); |
432 | $this->getOutput()->addHTML( '<p class="mw-delete-editreasons">' . $link . '</p>' ); |
433 | } |
434 | } |
435 | |
436 | /** |
437 | * @return bool |
438 | */ |
439 | protected function isSuppressionAllowed(): bool { |
440 | return $this->getAuthority()->isAllowed( 'suppressrevision' ); |
441 | } |
442 | |
443 | /** |
444 | * @return array |
445 | */ |
446 | protected function getFormFields(): array { |
447 | $user = $this->getUser(); |
448 | $title = $this->getTitle(); |
449 | $article = $this->getArticle(); |
450 | |
451 | $fields = []; |
452 | |
453 | $dropdownReason = $this->getFormMsg( self::MSG_REASON_DROPDOWN )->inContentLanguage()->text(); |
454 | // Add additional specific reasons for suppress |
455 | if ( $this->isSuppressionAllowed() ) { |
456 | $dropdownReason .= "\n" . $this->getFormMsg( self::MSG_REASON_DROPDOWN_SUPPRESS ) |
457 | ->inContentLanguage()->text(); |
458 | } |
459 | |
460 | $options = Html::listDropdownOptions( |
461 | $dropdownReason, |
462 | [ 'other' => $this->getFormMsg( self::MSG_REASON_DROPDOWN_OTHER )->text() ] |
463 | ); |
464 | |
465 | $fields['DeleteReasonList'] = [ |
466 | 'type' => 'select', |
467 | 'id' => 'wpDeleteReasonList', |
468 | 'tabindex' => 1, |
469 | 'infusable' => true, |
470 | 'options' => $options, |
471 | 'label' => $this->getFormMsg( self::MSG_COMMENT )->text(), |
472 | ]; |
473 | |
474 | // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP |
475 | // (e.g. emojis) count for two each. This limit is overridden in JS to instead count |
476 | // Unicode codepoints. |
477 | $fields['Reason'] = [ |
478 | 'type' => 'text', |
479 | 'id' => 'wpReason', |
480 | 'tabindex' => 2, |
481 | 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT, |
482 | 'infusable' => true, |
483 | 'default' => $this->getDefaultReason(), |
484 | 'autofocus' => true, |
485 | 'label' => $this->getFormMsg( self::MSG_REASON_OTHER )->text(), |
486 | ]; |
487 | |
488 | $delPage = $this->deletePageFactory->newDeletePage( $this->getWikiPage(), $this->getAuthority() ); |
489 | if ( $delPage->canProbablyDeleteAssociatedTalk()->isGood() ) { |
490 | $fields['DeleteTalk'] = [ |
491 | 'type' => 'check', |
492 | 'id' => 'wpDeleteTalk', |
493 | 'tabindex' => 3, |
494 | 'default' => false, |
495 | 'label-message' => 'deletepage-deletetalk', |
496 | ]; |
497 | } |
498 | |
499 | if ( $user->isRegistered() ) { |
500 | $checkWatch = $this->userOptionsLookup->getBoolOption( $user, 'watchdeletion' ) || |
501 | $this->watchlistManager->isWatched( $user, $title ); |
502 | $fields['Watch'] = [ |
503 | 'type' => 'check', |
504 | 'id' => 'wpWatch', |
505 | 'tabindex' => 4, |
506 | 'default' => $checkWatch, |
507 | 'label-message' => 'watchthis', |
508 | ]; |
509 | } |
510 | if ( $this->isSuppressionAllowed() ) { |
511 | $fields['Suppress'] = [ |
512 | 'type' => 'check', |
513 | 'id' => 'wpSuppress', |
514 | 'tabindex' => 5, |
515 | 'default' => false, |
516 | 'label-message' => 'revdelete-suppress', |
517 | ]; |
518 | } |
519 | |
520 | $fields['ConfirmB'] = [ |
521 | 'type' => 'submit', |
522 | 'id' => 'wpConfirmB', |
523 | 'tabindex' => 6, |
524 | 'buttonlabel' => $this->getFormMsg( self::MSG_SUBMIT )->text(), |
525 | 'flags' => [ 'primary', 'destructive' ], |
526 | ]; |
527 | |
528 | $fields['ConfirmationRevId'] = [ |
529 | 'type' => 'hidden', |
530 | 'id' => 'wpConfirmationRevId', |
531 | 'default' => $article->getRevIdFetched(), |
532 | ]; |
533 | |
534 | return $fields; |
535 | } |
536 | |
537 | /** |
538 | * @return string |
539 | */ |
540 | protected function getDeleteReason(): string { |
541 | $deleteReasonList = $this->getRequest()->getText( 'wpDeleteReasonList', 'other' ); |
542 | $deleteReason = $this->getRequest()->getText( 'wpReason' ); |
543 | |
544 | if ( $deleteReasonList === 'other' ) { |
545 | return $deleteReason; |
546 | } elseif ( $deleteReason !== '' ) { |
547 | // Entry from drop down menu + additional comment |
548 | $colonseparator = $this->msg( 'colon-separator' )->inContentLanguage()->text(); |
549 | return $deleteReasonList . $colonseparator . $deleteReason; |
550 | } else { |
551 | return $deleteReasonList; |
552 | } |
553 | } |
554 | |
555 | /** |
556 | * Show deletion log fragments pertaining to the current page |
557 | */ |
558 | protected function showLogEntries(): void { |
559 | $deleteLogPage = new LogPage( 'delete' ); |
560 | $outputPage = $this->getContext()->getOutput(); |
561 | $outputPage->addHTML( Html::element( 'h2', [], $deleteLogPage->getName()->text() ) ); |
562 | LogEventsList::showLogExtract( $outputPage, 'delete', $this->getTitle() ); |
563 | } |
564 | |
565 | protected function prepareOutputForForm(): void { |
566 | $outputPage = $this->getOutput(); |
567 | $outputPage->addModules( 'mediawiki.misc-authed-ooui' ); |
568 | $outputPage->addModuleStyles( 'mediawiki.action.styles' ); |
569 | $outputPage->enableOOUI(); |
570 | } |
571 | |
572 | /** |
573 | * @return string[] |
574 | */ |
575 | protected function getFormMessages(): array { |
576 | return [ |
577 | self::MSG_REASON_DROPDOWN => 'deletereason-dropdown', |
578 | self::MSG_REASON_DROPDOWN_SUPPRESS => 'deletereason-dropdown-suppress', |
579 | self::MSG_REASON_DROPDOWN_OTHER => 'deletereasonotherlist', |
580 | self::MSG_COMMENT => 'deletecomment', |
581 | self::MSG_REASON_OTHER => 'deleteotherreason', |
582 | self::MSG_SUBMIT => 'deletepage-submit', |
583 | self::MSG_LEGEND => 'delete-legend', |
584 | self::MSG_EDIT_REASONS => 'delete-edit-reasonlist', |
585 | self::MSG_EDIT_REASONS_SUPPRESS => 'delete-edit-reasonlist-suppress', |
586 | ]; |
587 | } |
588 | |
589 | /** |
590 | * @param string $field One of the self::MSG_* constants |
591 | * @return Message |
592 | */ |
593 | protected function getFormMsg( string $field ): Message { |
594 | $messages = $this->getFormMessages(); |
595 | if ( !isset( $messages[$field] ) ) { |
596 | throw new InvalidArgumentException( "Invalid field $field" ); |
597 | } |
598 | return $this->msg( $messages[$field] ); |
599 | } |
600 | |
601 | /** |
602 | * @return string |
603 | */ |
604 | protected function getFormAction(): string { |
605 | return $this->getTitle()->getLocalURL( 'action=delete' ); |
606 | } |
607 | |
608 | /** |
609 | * Default reason to be used for the deletion form |
610 | * |
611 | * @return string |
612 | */ |
613 | protected function getDefaultReason(): string { |
614 | $requestReason = $this->getRequest()->getText( 'wpReason' ); |
615 | if ( $requestReason ) { |
616 | return $requestReason; |
617 | } |
618 | |
619 | try { |
620 | return $this->getArticle()->getPage()->getAutoDeleteReason(); |
621 | } catch ( TimeoutException $e ) { |
622 | throw $e; |
623 | } catch ( Exception $e ) { |
624 | # if a page is horribly broken, we still want to be able to |
625 | # delete it. So be lenient about errors here. |
626 | # For example, WMF logs show MWException thrown from |
627 | # ContentHandler::checkModelID(). |
628 | MWExceptionHandler::logException( $e ); |
629 | return ''; |
630 | } |
631 | } |
632 | |
633 | /** |
634 | * Determines whether a page has a history of more than one revision. |
635 | * @fixme We should use WikiPage::isNew() here, but it doesn't work right for undeleted pages (T289008) |
636 | * @return bool |
637 | */ |
638 | private function pageHasHistory(): bool { |
639 | $dbr = $this->dbProvider->getReplicaDatabase(); |
640 | $res = $dbr->newSelectQueryBuilder() |
641 | ->select( '*' ) |
642 | ->from( 'revision' ) |
643 | ->where( [ 'rev_page' => $this->getTitle()->getArticleID() ] ) |
644 | ->andWhere( |
645 | [ $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) . ' = 0' ] |
646 | )->limit( 2 ) |
647 | ->caller( __METHOD__ ) |
648 | ->fetchRowCount(); |
649 | |
650 | return $res > 1; |
651 | } |
652 | } |