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