Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
64.89% |
85 / 131 |
|
54.55% |
6 / 11 |
CRAP | |
0.00% |
0 / 1 |
| ApiDelete | |
65.38% |
85 / 130 |
|
54.55% |
6 / 11 |
106.36 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
9 / 9 |
|
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 | * @license GPL-2.0-or-later |
| 6 | * @file |
| 7 | */ |
| 8 | |
| 9 | namespace MediaWiki\Api; |
| 10 | |
| 11 | use MediaWiki\ChangeTags\ChangeTags; |
| 12 | use MediaWiki\FileRepo\File\File; |
| 13 | use MediaWiki\FileRepo\RepoGroup; |
| 14 | use MediaWiki\MainConfigNames; |
| 15 | use MediaWiki\Page\DeletePage; |
| 16 | use MediaWiki\Page\DeletePageFactory; |
| 17 | use MediaWiki\Page\File\FileDeleteForm; |
| 18 | use MediaWiki\Page\WikiPage; |
| 19 | use MediaWiki\Status\Status; |
| 20 | use MediaWiki\Title\Title; |
| 21 | use MediaWiki\User\Options\UserOptionsLookup; |
| 22 | use MediaWiki\Watchlist\WatchedItemStoreInterface; |
| 23 | use MediaWiki\Watchlist\WatchlistManager; |
| 24 | use StatusValue; |
| 25 | use 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 | */ |
| 33 | class 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 */ |
| 320 | class_alias( ApiDelete::class, 'ApiDelete' ); |