Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 115
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
FileDeleteAction
0.00% covered (danger)
0.00%
0 / 115
0.00% covered (danger)
0.00%
0 / 10
870
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getPageTitle
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 tempDelete
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 tempExecute
0.00% covered (danger)
0.00%
0 / 55
0.00% covered (danger)
0.00%
0 / 1
182
 showFormWarnings
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 showConfirm
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 prepareMessage
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 getFormAction
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 checkCanExecute
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getFormMessages
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 * @ingroup Actions
6 */
7
8namespace MediaWiki\Actions;
9
10use MediaWiki\Context\IContextSource;
11use MediaWiki\Exception\ErrorPageError;
12use MediaWiki\Exception\PermissionsError;
13use MediaWiki\FileRepo\File\File;
14use MediaWiki\FileRepo\File\LocalFile;
15use MediaWiki\FileRepo\File\OldLocalFile;
16use MediaWiki\FileRepo\RepoGroup;
17use MediaWiki\Html\Html;
18use MediaWiki\MainConfigNames;
19use MediaWiki\Page\Article;
20use MediaWiki\Page\File\FileDeleteForm;
21use MediaWiki\Permissions\PermissionStatus;
22use MediaWiki\Title\Title;
23use MediaWiki\User\User;
24use MediaWiki\Utils\UrlUtils;
25
26/**
27 * Handle file deletion
28 *
29 * @ingroup Actions
30 */
31class FileDeleteAction extends DeleteAction {
32    private readonly File $file;
33    /** @var string Descriptor for the old version of the image, if applicable */
34    private readonly string $oldImage;
35    /** @var OldLocalFile|null Corresponding to oldImage, if applicable */
36    private $oldFile;
37
38    /**
39     * @inheritDoc
40     */
41    public function __construct(
42        Article $article,
43        IContextSource $context,
44        RepoGroup $repoGroup,
45        private readonly UrlUtils $urlUtils,
46    ) {
47        parent::__construct( $article, $context );
48        $this->file = $this->getArticle()->getFile();
49        $this->oldImage = $this->getRequest()->getText( 'oldimage', '' );
50        if ( $this->oldImage !== '' ) {
51            $this->oldFile = $repoGroup->getLocalRepo()->newFromArchiveName(
52                $this->getTitle(),
53                $this->oldImage
54            );
55        }
56    }
57
58    /** @inheritDoc */
59    protected function getPageTitle() {
60        $title = $this->getTitle();
61        return $this->msg( 'filedelete' )->plaintextParams( $title->getText() );
62    }
63
64    protected function tempDelete() {
65        $file = $this->file;
66        /** @var LocalFile $file */'@phan-var LocalFile $file';
67        $this->tempExecute( $file );
68    }
69
70    private function tempExecute( LocalFile $file ): void {
71        $context = $this->getContext();
72        $title = $this->getTitle();
73        $article = $this->getArticle();
74        $outputPage = $context->getOutput();
75        $request = $context->getRequest();
76
77        $checkFile = $this->oldFile ?: $file;
78        if ( !$checkFile->exists() || !$checkFile->isLocal() ) {
79            $outputPage->addHTML( $this->prepareMessage( 'filedelete-nofile' ) );
80            $outputPage->addReturnTo( $title );
81            return;
82        }
83
84        // Perform the deletion if appropriate
85        $token = $request->getVal( 'wpEditToken' );
86        if (
87            !$request->wasPosted() ||
88            !$context->getUser()->matchEditToken( $token, [ 'delete', $title->getPrefixedText() ] )
89        ) {
90            $this->showConfirm();
91            return;
92        }
93
94        // Check to make sure the page has not been edited while the deletion was being confirmed
95        if ( $article->getRevIdFetched() !== $request->getIntOrNull( 'wpConfirmationRevId' ) ) {
96            $this->showEditedWarning();
97            $this->showConfirm();
98            return;
99        }
100
101        $permissionStatus = PermissionStatus::newEmpty();
102        if ( !$context->getAuthority()->authorizeWrite(
103            'delete', $title, $permissionStatus
104        ) ) {
105            throw new PermissionsError( 'delete', $permissionStatus );
106        }
107
108        $reason = $this->getDeleteReason();
109
110        # Flag to hide all contents of the archived revisions
111        $suppress = $request->getCheck( 'wpSuppress' ) &&
112            $context->getAuthority()->isAllowed( 'suppressrevision' );
113
114        $status = FileDeleteForm::doDelete(
115            $title,
116            $file,
117            $this->oldImage,
118            $reason,
119            $suppress,
120            $context->getUser(),
121            [],
122            $request->getCheck( 'wpDeleteTalk' )
123        );
124
125        if ( !$status->isGood() ) {
126            $outputPage->setPageTitleMsg(
127                $this->msg( 'cannotdelete-title' )->plaintextParams( $title->getPrefixedText() )
128            );
129            $outputPage->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
130            foreach ( $status->getMessages() as $msg ) {
131                $outputPage->addHTML( Html::errorBox(
132                    $context->msg( $msg )->parse()
133                ) );
134            }
135        }
136        if ( $status->isOK() ) {
137            $outputPage->setPageTitleMsg( $context->msg( 'actioncomplete' ) );
138            $outputPage->addHTML( $this->prepareMessage( 'filedelete-success' ) );
139            // Return to the main page if we just deleted all versions of the
140            // file, otherwise go back to the description page
141            $outputPage->addReturnTo( $this->oldImage ? $title : Title::newMainPage() );
142
143            $this->watchlistManager->setWatch(
144                $request->getCheck( 'wpWatch' ),
145                $context->getAuthority(),
146                $title
147            );
148        }
149    }
150
151    protected function showFormWarnings(): void {
152        $this->getOutput()->addHTML( $this->prepareMessage( 'filedelete-intro' ) );
153        $this->showSubpagesWarnings();
154    }
155
156    /**
157     * Show the confirmation form
158     */
159    private function showConfirm() {
160        $this->prepareOutputForForm();
161        $context = $this->getContext();
162        $article = $this->getArticle();
163
164        // oldid is set to the revision id of the page when the page was displayed.
165        // Check to make sure the page has not been edited between loading the page
166        // and clicking the delete link
167        $oldid = $context->getRequest()->getIntOrNull( 'oldid' );
168        if ( $oldid !== null && $oldid !== $article->getRevIdFetched() ) {
169            $this->showEditedWarning();
170        }
171
172        $this->showFormWarnings();
173        $form = $this->getForm();
174        if ( $form->show() ) {
175            $this->onSuccess();
176        }
177        $this->showEditReasonsLinks();
178        $this->showLogEntries();
179    }
180
181    /**
182     * Prepare a message referring to the file being deleted,
183     * showing an appropriate message depending upon whether
184     * it's a current file or an old version
185     *
186     * @param string $message Message base
187     * @return string
188     */
189    private function prepareMessage( string $message ) {
190        if ( $this->oldFile ) {
191            $lang = $this->getContext()->getLanguage();
192            # Message keys used:
193            # 'filedelete-intro-old', 'filedelete-nofile-old', 'filedelete-success-old'
194            return $this->getContext()->msg(
195                "{$message}-old",
196                wfEscapeWikiText( $this->getTitle()->getText() ),
197                $lang->date( $this->oldFile->getTimestamp(), true ),
198                $lang->time( $this->oldFile->getTimestamp(), true ),
199                (string)$this->urlUtils->expand(
200                    $this->file->getArchiveUrl( $this->oldImage ),
201                    PROTO_CURRENT
202                )
203            )->parseAsBlock();
204        } else {
205            return $this->getContext()->msg(
206                $message,
207                wfEscapeWikiText( $this->getTitle()->getText() )
208            )->parseAsBlock();
209        }
210    }
211
212    protected function getFormAction(): string {
213        $q = [ 'action' => 'delete' ];
214        if ( $this->oldImage ) {
215            $q['oldimage'] = $this->oldImage;
216        }
217        return $this->getTitle()->getLocalURL( $q );
218    }
219
220    protected function checkCanExecute( User $user ) {
221        parent::checkCanExecute( $user );
222
223        if ( $this->getContext()->getConfig()->get( MainConfigNames::UploadMaintenance ) ) {
224            throw new ErrorPageError( 'filedelete-maintenance-title', 'filedelete-maintenance' );
225        }
226    }
227
228    /**
229     * TODO Do we need all these messages to be different?
230     * @return string[]
231     */
232    protected function getFormMessages(): array {
233        return [
234            self::MSG_REASON_DROPDOWN => 'filedelete-reason-dropdown',
235            self::MSG_REASON_DROPDOWN_SUPPRESS => 'filedelete-reason-dropdown-suppress',
236            self::MSG_REASON_DROPDOWN_OTHER => 'filedelete-reason-otherlist',
237            self::MSG_COMMENT => 'filedelete-comment',
238            self::MSG_REASON_OTHER => 'filedelete-otherreason',
239            self::MSG_SUBMIT => 'filedelete-submit',
240            self::MSG_LEGEND => 'filedelete-legend',
241            self::MSG_EDIT_REASONS => 'filedelete-edit-reasonlist',
242            self::MSG_EDIT_REASONS_SUPPRESS => 'filedelete-edit-reasonlist-suppress',
243        ];
244    }
245}