Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
64.62% |
84 / 130 |
|
54.55% |
6 / 11 |
CRAP | |
0.00% |
0 / 1 |
ApiDelete | |
65.12% |
84 / 129 |
|
54.55% |
6 / 11 |
107.92 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
76.19% |
32 / 42 |
|
0.00% |
0 / 1 |
15.28 | |||
delete | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
8 | |||
canDeleteFile | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
12 | |||
deleteFile | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
90 | |||
mustBePosted | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isWriteMode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getAllowedParams | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
1 | |||
needsToken | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getExamplesMessages | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
getHelpUrls | |
0.00% |
0 / 1 |
|
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 | |
23 | namespace MediaWiki\Api; |
24 | |
25 | use ChangeTags; |
26 | use File; |
27 | use MediaWiki\MainConfigNames; |
28 | use MediaWiki\Page\DeletePage; |
29 | use MediaWiki\Page\DeletePageFactory; |
30 | use MediaWiki\Page\File\FileDeleteForm; |
31 | use MediaWiki\Status\Status; |
32 | use MediaWiki\Title\Title; |
33 | use MediaWiki\User\Options\UserOptionsLookup; |
34 | use MediaWiki\Watchlist\WatchlistManager; |
35 | use RepoGroup; |
36 | use StatusValue; |
37 | use Wikimedia\ParamValidator\ParamValidator; |
38 | use 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 | */ |
46 | class 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 */ |
324 | class_alias( ApiDelete::class, 'ApiDelete' ); |