Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 348
0.00% covered (danger)
0.00%
0 / 28
CRAP
0.00% covered (danger)
0.00%
0 / 1
DeleteAction
0.00% covered (danger)
0.00%
0 / 348
0.00% covered (danger)
0.00%
0 / 28
4692
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 getName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onSubmit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onSuccess
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 usesOOUI
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPageTitle
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getRestriction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 alterForm
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 show
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 tempDelete
0.00% covered (danger)
0.00%
0 / 75
0.00% covered (danger)
0.00%
0 / 1
132
 showSuccessMessages
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
72
 showEditedWarning
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 showHistoryWarnings
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
6
 showFormWarnings
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 showBacklinksWarning
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 showSubpagesWarnings
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
20
 tempConfirmDelete
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
30
 showEditReasonsLinks
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 isSuppressionAllowed
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFormFields
0.00% covered (danger)
0.00%
0 / 70
0.00% covered (danger)
0.00%
0 / 1
42
 getDeleteReason
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 showLogEntries
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 prepareOutputForForm
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getFormMessages
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 getFormMsg
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getFormAction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDefaultReason
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 pageHasHistory
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
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
21use MediaWiki\Cache\BacklinkCacheFactory;
22use MediaWiki\CommentStore\CommentStore;
23use MediaWiki\Context\IContextSource;
24use MediaWiki\Html\Html;
25use MediaWiki\HTMLForm\HTMLForm;
26use MediaWiki\Linker\LinkRenderer;
27use MediaWiki\MainConfigNames;
28use MediaWiki\MediaWikiServices;
29use MediaWiki\Message\Message;
30use MediaWiki\Page\DeletePage;
31use MediaWiki\Page\DeletePageFactory;
32use MediaWiki\Revision\RevisionRecord;
33use MediaWiki\Session\CsrfTokenSet;
34use MediaWiki\Status\Status;
35use MediaWiki\Title\NamespaceInfo;
36use MediaWiki\Title\TitleFactory;
37use MediaWiki\Title\TitleFormatter;
38use MediaWiki\User\Options\UserOptionsLookup;
39use MediaWiki\Watchlist\WatchlistManager;
40use Wikimedia\Rdbms\IConnectionProvider;
41use Wikimedia\Rdbms\ReadOnlyMode;
42use Wikimedia\RequestTimeout\TimeoutException;
43
44/**
45 * Handle page deletion
46 *
47 * @ingroup Actions
48 */
49class 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}