Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
64.62% covered (warning)
64.62%
84 / 130
54.55% covered (warning)
54.55%
6 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiDelete
65.12% covered (warning)
65.12%
84 / 129
54.55% covered (warning)
54.55%
6 / 11
107.92
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 execute
76.19% covered (warning)
76.19%
32 / 42
0.00% covered (danger)
0.00%
0 / 1
15.28
 delete
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
8
 canDeleteFile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
12
 deleteFile
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
90
 mustBePosted
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isWriteMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllowedParams
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
1
 needsToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 getHelpUrls
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Copyright © 2007 Roan Kattouw <roan.kattouw@gmail.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 */
22
23namespace MediaWiki\Api;
24
25use ChangeTags;
26use File;
27use MediaWiki\MainConfigNames;
28use MediaWiki\Page\DeletePage;
29use MediaWiki\Page\DeletePageFactory;
30use MediaWiki\Page\File\FileDeleteForm;
31use MediaWiki\Status\Status;
32use MediaWiki\Title\Title;
33use MediaWiki\User\Options\UserOptionsLookup;
34use MediaWiki\Watchlist\WatchlistManager;
35use RepoGroup;
36use StatusValue;
37use Wikimedia\ParamValidator\ParamValidator;
38use WikiPage;
39
40/**
41 * API module that facilitates deleting pages. The API equivalent of action=delete.
42 * Requires API write mode to be enabled.
43 *
44 * @ingroup API
45 */
46class ApiDelete extends ApiBase {
47
48    use ApiWatchlistTrait;
49
50    private RepoGroup $repoGroup;
51    private DeletePageFactory $deletePageFactory;
52
53    public function __construct(
54        ApiMain $mainModule,
55        string $moduleName,
56        RepoGroup $repoGroup,
57        WatchlistManager $watchlistManager,
58        UserOptionsLookup $userOptionsLookup,
59        DeletePageFactory $deletePageFactory
60    ) {
61        parent::__construct( $mainModule, $moduleName );
62        $this->repoGroup = $repoGroup;
63        $this->deletePageFactory = $deletePageFactory;
64
65        // Variables needed in ApiWatchlistTrait trait
66        $this->watchlistExpiryEnabled = $this->getConfig()->get( MainConfigNames::WatchlistExpiry );
67        $this->watchlistMaxDuration =
68            $this->getConfig()->get( MainConfigNames::WatchlistExpiryMaxDuration );
69        $this->watchlistManager = $watchlistManager;
70        $this->userOptionsLookup = $userOptionsLookup;
71    }
72
73    /**
74     * Extracts the title and reason from the request parameters and invokes
75     * the local delete() function with these as arguments. It does not make use of
76     * the delete function specified by Article.php. If the deletion succeeds, the
77     * details of the article deleted and the reason for deletion are added to the
78     * result object.
79     */
80    public function execute() {
81        $this->useTransactionalTimeLimit();
82
83        $params = $this->extractRequestParams();
84
85        $pageObj = $this->getTitleOrPageId( $params, 'fromdbmaster' );
86        $titleObj = $pageObj->getTitle();
87        $this->getErrorFormatter()->setContextTitle( $titleObj );
88        if ( !$pageObj->exists() &&
89            // @phan-suppress-next-line PhanUndeclaredMethod
90            !( $titleObj->getNamespace() === NS_FILE && self::canDeleteFile( $pageObj->getFile() ) )
91        ) {
92            $this->dieWithError( 'apierror-missingtitle' );
93        }
94
95        $reason = $params['reason'];
96        $user = $this->getUser();
97
98        $tags = $params['tags'] ?: [];
99
100        if ( $titleObj->getNamespace() === NS_FILE ) {
101            $status = $this->deleteFile(
102                $pageObj,
103                $params['oldimage'],
104                $reason,
105                false,
106                $tags,
107                $params['deletetalk']
108            );
109            // TODO What kind of non-fatal errors should we expect here?
110            $wasScheduled = $status->isOK() && $status->getValue() === false;
111        } else {
112            $status = $this->delete( $pageObj, $reason, $tags, $params['deletetalk'] );
113            $wasScheduled = $status->isGood() && $status->getValue() === false;
114        }
115
116        if ( !$status->isOK() ) {
117            $this->dieStatus( $status );
118        }
119
120        if ( $wasScheduled ) {
121            $this->addWarning( [ 'delete-scheduled', $titleObj->getPrefixedText() ] );
122        }
123
124        // Deprecated parameters
125        if ( $params['watch'] ) {
126            $watch = 'watch';
127        } elseif ( $params['unwatch'] ) {
128            $watch = 'unwatch';
129        } else {
130            $watch = $params['watchlist'];
131        }
132
133        $watchlistExpiry = $this->getExpiryFromParams( $params );
134        $this->setWatch( $watch, $titleObj, $user, 'watchdeletion', $watchlistExpiry );
135
136        $r = [
137            'title' => $titleObj->getPrefixedText(),
138            'reason' => $reason,
139        ];
140
141        // TODO: We could expose additional information (scheduled and log ID) about the status of the talk page
142        // deletion.
143        if ( $wasScheduled ) {
144            $r['scheduled'] = true;
145        } else {
146            // Scheduled deletions don't currently have a log entry available at this point
147            $r['logid'] = $status->value;
148        }
149        $this->getResult()->addValue( null, $this->getModuleName(), $r );
150    }
151
152    /**
153     * We have our own delete() function, since Article.php's implementation is split in two phases
154     *
155     * @param WikiPage $page WikiPage object to work on
156     * @param string|null &$reason Reason for the deletion. Autogenerated if null
157     * @param string[] $tags Tags to tag the deletion with
158     * @param bool $deleteTalk
159     * @return StatusValue Same as DeletePage::deleteIfAllowed, but if the status is good, then:
160     *  - For immediate deletions, the value is the ID of the deletion
161     *  - For scheduled deletions, the value is false
162     *   If $deleteTalk is set, no information about the deletion of the talk page is included in the returned Status.
163     */
164    private function delete( WikiPage $page, &$reason, array $tags, bool $deleteTalk ): StatusValue {
165        $title = $page->getTitle();
166
167        // Auto-generate a summary, if necessary
168        if ( $reason === null ) {
169            $reason = $page->getAutoDeleteReason();
170            if ( $reason === false ) {
171                // Should be reachable only if the page has no revisions
172                return Status::newFatal( 'cannotdelete', $title->getPrefixedText() ); // @codeCoverageIgnore
173            }
174        }
175
176        $deletePage = $this->deletePageFactory->newDeletePage( $page, $this->getAuthority() );
177        if ( $deleteTalk ) {
178            $checkStatus = $deletePage->canProbablyDeleteAssociatedTalk();
179            if ( !$checkStatus->isGood() ) {
180                foreach ( $checkStatus->getMessages() as $msg ) {
181                    $this->addWarning( $msg );
182                }
183            } else {
184                $deletePage->setDeleteAssociatedTalk( true );
185            }
186        }
187        $deletionStatus = $deletePage->setTags( $tags )->deleteIfAllowed( $reason );
188        if ( $deletionStatus->isGood() ) {
189            $deletionStatus->value = $deletePage->deletionsWereScheduled()[DeletePage::PAGE_BASE]
190                ? false
191                : $deletePage->getSuccessfulDeletionsIDs()[DeletePage::PAGE_BASE];
192        }
193        return $deletionStatus;
194    }
195
196    /**
197     * @param File $file
198     * @return bool
199     */
200    protected static function canDeleteFile( File $file ) {
201        return $file->exists() && $file->isLocal() && !$file->getRedirected();
202    }
203
204    /**
205     * @param WikiPage $page Object to work on
206     * @param string $oldimage Archive name
207     * @param string|null &$reason Reason for the deletion. Autogenerated if null.
208     * @param bool $suppress Whether to mark all deleted versions as restricted
209     * @param string[] $tags Tags to tag the deletion with
210     * @param bool $deleteTalk
211     * @return StatusValue
212     */
213    private function deleteFile(
214        WikiPage $page,
215        $oldimage,
216        &$reason,
217        bool $suppress,
218        array $tags,
219        bool $deleteTalk
220    ) {
221        $title = $page->getTitle();
222
223        // @phan-suppress-next-line PhanUndeclaredMethod There's no right typehint for it
224        $file = $page->getFile();
225        if ( !self::canDeleteFile( $file ) ) {
226            return $this->delete( $page, $reason, $tags, $deleteTalk );
227        }
228
229        // Check that the user is allowed to carry out the deletion
230        $this->checkTitleUserPermissions( $page->getTitle(), 'delete' );
231        if ( $tags ) {
232            // If change tagging was requested, check that the user is allowed to tag,
233            // and the tags are valid
234            $tagStatus = ChangeTags::canAddTagsAccompanyingChange( $tags, $this->getAuthority() );
235            if ( !$tagStatus->isOK() ) {
236                $this->dieStatus( $tagStatus );
237            }
238        }
239
240        if ( $oldimage ) {
241            if ( !FileDeleteForm::isValidOldSpec( $oldimage ) ) {
242                return Status::newFatal( 'invalidoldimage' );
243            }
244            $oldfile = $this->repoGroup->getLocalRepo()->newFromArchiveName( $title, $oldimage );
245            if ( !$oldfile->exists() || !$oldfile->isLocal() || $oldfile->getRedirected() ) {
246                return Status::newFatal( 'nodeleteablefile' );
247            }
248        }
249
250        return FileDeleteForm::doDelete(
251            $title,
252            $file,
253            $oldimage,
254            // Log and RC don't like null reasons
255            $reason ?? '',
256            $suppress,
257            $this->getUser(),
258            $tags,
259            $deleteTalk
260        );
261    }
262
263    public function mustBePosted() {
264        return true;
265    }
266
267    public function isWriteMode() {
268        return true;
269    }
270
271    public function getAllowedParams() {
272        $params = [
273            'title' => null,
274            'pageid' => [
275                ParamValidator::PARAM_TYPE => 'integer'
276            ],
277            'reason' => null,
278            'tags' => [
279                ParamValidator::PARAM_TYPE => 'tags',
280                ParamValidator::PARAM_ISMULTI => true,
281            ],
282            'deletetalk' => false,
283            'watch' => [
284                ParamValidator::PARAM_DEFAULT => false,
285                ParamValidator::PARAM_DEPRECATED => true,
286            ],
287        ];
288
289        // Params appear in the docs in the order they are defined,
290        // which is why this is here and not at the bottom.
291        $params += $this->getWatchlistParams();
292
293        return $params + [
294            'unwatch' => [
295                ParamValidator::PARAM_DEFAULT => false,
296                ParamValidator::PARAM_DEPRECATED => true,
297            ],
298            'oldimage' => null,
299        ];
300    }
301
302    public function needsToken() {
303        return 'csrf';
304    }
305
306    protected function getExamplesMessages() {
307        $title = Title::newMainPage()->getPrefixedText();
308        $mp = rawurlencode( $title );
309
310        return [
311            "action=delete&title={$mp}&token=123ABC"
312                => 'apihelp-delete-example-simple',
313            "action=delete&title={$mp}&token=123ABC&reason=Preparing%20for%20move"
314                => 'apihelp-delete-example-reason',
315        ];
316    }
317
318    public function getHelpUrls() {
319        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Delete';
320    }
321}
322
323/** @deprecated class alias since 1.43 */
324class_alias( ApiDelete::class, 'ApiDelete' );