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