MediaWiki REL1_37
FileDeleteForm.php
Go to the documentation of this file.
1<?php
31
38
40 private $title;
41
43 private $file;
44
46 private $oldfile = null;
47
49 private $oldimage = '';
50
52 private $context;
53
56
58 private $repoGroup;
59
62
65
68
78 public function __construct(
86 ) {
87 $this->title = $file->getTitle();
88 $this->file = $file;
89 $this->context = $context;
90 $this->readOnlyMode = $readOnlyMode;
91 $this->repoGroup = $repoGroup;
92 $this->watchlistManager = $watchlistManager;
93 $this->linkRenderer = $linkRenderer;
94 $this->userOptionsLookup = $userOptionsLookup;
95 }
96
101 public function execute() {
102 if ( $this->readOnlyMode->isReadOnly() ) {
103 throw new ReadOnlyError;
104 }
105
106 if ( $this->context->getConfig()->get( 'UploadMaintenance' ) ) {
107 throw new ErrorPageError( 'filedelete-maintenance-title', 'filedelete-maintenance' );
108 }
109
110 $this->setHeaders();
111
112 $request = $this->context->getRequest();
113 $this->oldimage = $request->getText( 'oldimage', '' );
114 $token = $request->getText( 'wpEditToken' );
115 # Flag to hide all contents of the archived revisions
116 $suppress = $request->getCheck( 'wpSuppress' ) &&
117 $this->context->getAuthority()->isAllowed( 'suppressrevision' );
118
119 if ( $this->oldimage ) {
120 $this->oldfile = $this->repoGroup->getLocalRepo()->newFromArchiveName(
121 $this->title,
122 $this->oldimage
123 );
124 }
125
126 if ( !self::haveDeletableFile( $this->file, $this->oldfile, $this->oldimage ) ) {
127 $this->context->getOutput()->addHTML( $this->prepareMessage( 'filedelete-nofile' ) );
128 $this->context->getOutput()->addReturnTo( $this->title );
129 return;
130 }
131
132 // Perform the deletion if appropriate
133 if ( $request->wasPosted() && $this->context->getUser()->matchEditToken( $token, $this->oldimage ) ) {
134 $permissionStatus = PermissionStatus::newEmpty();
135 if ( !$this->context->getAuthority()->authorizeWrite(
136 'delete', $this->title, $permissionStatus
137 ) ) {
138 throw new PermissionsError( 'delete', $permissionStatus );
139 }
140
141 $deleteReasonList = $request->getText( 'wpDeleteReasonList' );
142 $deleteReason = $request->getText( 'wpReason' );
143
144 if ( $deleteReasonList == 'other' ) {
145 $reason = $deleteReason;
146 } elseif ( $deleteReason != '' ) {
147 // Entry from drop down menu + additional comment
148 $reason = $deleteReasonList . $this->context->msg( 'colon-separator' )
149 ->inContentLanguage()->text() . $deleteReason;
150 } else {
151 $reason = $deleteReasonList;
152 }
153
154 $status = self::doDelete(
155 $this->title,
156 $this->file,
157 $this->oldimage,
158 $reason,
159 $suppress,
160 $this->context->getUser()
161 );
162
163 $out = $this->context->getOutput();
164
165 if ( !$status->isGood() ) {
166 $out->addHTML(
167 '<h2>' . $this->prepareMessage( 'filedeleteerror-short' ) . "</h2>\n"
168 );
169 $out->wrapWikiTextAsInterface(
170 'error',
171 $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' )
172 );
173 }
174 if ( $status->isOK() ) {
175 $out->setPageTitle( $this->context->msg( 'actioncomplete' ) );
176 $out->addHTML( $this->prepareMessage( 'filedelete-success' ) );
177 // Return to the main page if we just deleted all versions of the
178 // file, otherwise go back to the description page
179 $out->addReturnTo( $this->oldimage ? $this->title : Title::newMainPage() );
180
181 $this->watchlistManager->setWatch(
182 $request->getCheck( 'wpWatch' ),
183 $this->context->getAuthority(),
185 );
186 }
187 return;
188 }
189
190 $this->showForm();
191 $this->showLogEntries();
192 }
193
207 public static function doDelete( &$title, &$file, &$oldimage, $reason,
208 $suppress, UserIdentity $user, $tags = []
209 ): Status {
210 if ( $oldimage ) {
211 $page = null;
212 $status = $file->deleteOldFile( $oldimage, $reason, $user, $suppress );
213 if ( $status->isOK() ) {
214 // Need to do a log item
215 $logComment = wfMessage( 'deletedrevision', $oldimage )->inContentLanguage()->text();
216 if ( trim( $reason ) != '' ) {
217 $logComment .= wfMessage( 'colon-separator' )
218 ->inContentLanguage()->text() . $reason;
219 }
220
221 $logtype = $suppress ? 'suppress' : 'delete';
222
223 $logEntry = new ManualLogEntry( $logtype, 'delete' );
224 $logEntry->setPerformer( $user );
225 $logEntry->setTarget( $title );
226 $logEntry->setComment( $logComment );
227 $logEntry->addTags( $tags );
228 $logid = $logEntry->insert();
229 $logEntry->publish( $logid );
230
231 $status->value = $logid;
232 }
233 } else {
234 $status = Status::newFatal( 'cannotdelete',
236 );
237 $page = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title );
238 '@phan-var WikiFilePage $page';
239 $dbw = wfGetDB( DB_PRIMARY );
240 $dbw->startAtomic( __METHOD__ );
241 // delete the associated article first
242 $error = '';
243 $deleteStatus = $page->doDeleteArticleReal(
244 $reason,
245 $user,
246 $suppress,
247 null,
248 $error,
249 null,
250 $tags
251 );
252 // doDeleteArticleReal() returns a non-fatal error status if the page
253 // or revision is missing, so check for isOK() rather than isGood()
254 if ( $deleteStatus->isOK() ) {
255 $status = $file->deleteFile( $reason, $user, $suppress );
256 if ( $status->isOK() ) {
257 if ( $deleteStatus->value === null ) {
258 // No log ID from doDeleteArticleReal(), probably
259 // because the page/revision didn't exist, so create
260 // one here.
261 $logtype = $suppress ? 'suppress' : 'delete';
262 $logEntry = new ManualLogEntry( $logtype, 'delete' );
263 $logEntry->setPerformer( $user );
264 $logEntry->setTarget( clone $title );
265 $logEntry->setComment( $reason );
266 $logEntry->addTags( $tags );
267 $logid = $logEntry->insert();
268 $dbw->onTransactionPreCommitOrIdle(
269 static function () use ( $logEntry, $logid ) {
270 $logEntry->publish( $logid );
271 },
272 __METHOD__
273 );
274 $status->value = $logid;
275 } else {
276 $status->value = $deleteStatus->value; // log id
277 }
278 $dbw->endAtomic( __METHOD__ );
279 } else {
280 // Page deleted but file still there? rollback page delete
281 $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
282 $lbFactory->rollbackPrimaryChanges( __METHOD__ );
283 }
284 } else {
285 $dbw->endAtomic( __METHOD__ );
286 }
287 }
288
289 if ( $status->isOK() ) {
290 $legacyUser = MediaWikiServices::getInstance()
291 ->getUserFactory()
292 ->newFromUserIdentity( $user );
293 Hooks::runner()->onFileDeleteComplete( $file, $oldimage, $page, $legacyUser, $reason );
294 }
295
296 return $status;
297 }
298
302 private function showForm() {
303 $permissionStatus = PermissionStatus::newEmpty();
304 if ( !$this->context->getAuthority()->definitelyCan(
305 'delete', $this->title, $permissionStatus
306 ) ) {
307 throw new PermissionsError( 'delete', $permissionStatus );
308 }
309
310 $this->context->getOutput()->addModules( 'mediawiki.action.delete' );
311 $this->context->getOutput()->addModuleStyles( 'mediawiki.action.styles' );
312
313 $checkWatch =
314 $this->userOptionsLookup->getBoolOption( $this->context->getUser(), 'watchdeletion' ) ||
315 $this->watchlistManager->isWatched( $this->context->getUser(), $this->title );
316
317 $this->context->getOutput()->enableOOUI();
318
319 $fields = [];
320
321 $fields[] = new OOUI\LabelWidget( [ 'label' => new OOUI\HtmlSnippet(
322 $this->prepareMessage( 'filedelete-intro' ) ) ]
323 );
324
325 $suppressAllowed = $this->context->getAuthority()->isAllowed( 'suppressrevision' );
326 $dropDownReason = $this->context->msg( 'filedelete-reason-dropdown' )->inContentLanguage()->text();
327 // Add additional specific reasons for suppress
328 if ( $suppressAllowed ) {
329 $dropDownReason .= "\n" . $this->context->msg( 'filedelete-reason-dropdown-suppress' )
330 ->inContentLanguage()->text();
331 }
332
333 $options = Xml::listDropDownOptions(
334 $dropDownReason,
335 [ 'other' => $this->context->msg( 'filedelete-reason-otherlist' )->inContentLanguage()->text() ]
336 );
337 $options = Xml::listDropDownOptionsOoui( $options );
338
339 $fields[] = new OOUI\FieldLayout(
340 new OOUI\DropdownInputWidget( [
341 'name' => 'wpDeleteReasonList',
342 'inputId' => 'wpDeleteReasonList',
343 'tabIndex' => 1,
344 'infusable' => true,
345 'value' => '',
346 'options' => $options,
347 ] ),
348 [
349 'label' => $this->context->msg( 'filedelete-comment' )->text(),
350 'align' => 'top',
351 ]
352 );
353
354 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
355 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
356 // Unicode codepoints.
357 $fields[] = new OOUI\FieldLayout(
358 new OOUI\TextInputWidget( [
359 'name' => 'wpReason',
360 'inputId' => 'wpReason',
361 'tabIndex' => 2,
362 'maxLength' => CommentStore::COMMENT_CHARACTER_LIMIT,
363 'infusable' => true,
364 'value' => $this->context->getRequest()->getText( 'wpReason' ),
365 'autofocus' => true,
366 ] ),
367 [
368 'label' => $this->context->msg( 'filedelete-otherreason' )->text(),
369 'align' => 'top',
370 ]
371 );
372
373 if ( $suppressAllowed ) {
374 $fields[] = new OOUI\FieldLayout(
375 new OOUI\CheckboxInputWidget( [
376 'name' => 'wpSuppress',
377 'inputId' => 'wpSuppress',
378 'tabIndex' => 3,
379 'selected' => false,
380 ] ),
381 [
382 'label' => $this->context->msg( 'revdelete-suppress' )->text(),
383 'align' => 'inline',
384 'infusable' => true,
385 ]
386 );
387 }
388
389 if ( $this->context->getUser()->isRegistered() ) {
390 $fields[] = new OOUI\FieldLayout(
391 new OOUI\CheckboxInputWidget( [
392 'name' => 'wpWatch',
393 'inputId' => 'wpWatch',
394 'tabIndex' => 3,
395 'selected' => $checkWatch,
396 ] ),
397 [
398 'label' => $this->context->msg( 'watchthis' )->text(),
399 'align' => 'inline',
400 'infusable' => true,
401 ]
402 );
403 }
404
405 $fields[] = new OOUI\FieldLayout(
406 new OOUI\ButtonInputWidget( [
407 'name' => 'mw-filedelete-submit',
408 'inputId' => 'mw-filedelete-submit',
409 'tabIndex' => 4,
410 'value' => $this->context->msg( 'filedelete-submit' )->text(),
411 'label' => $this->context->msg( 'filedelete-submit' )->text(),
412 'flags' => [ 'primary', 'destructive' ],
413 'type' => 'submit',
414 ] ),
415 [
416 'align' => 'top',
417 ]
418 );
419
420 $fieldset = new OOUI\FieldsetLayout( [
421 'label' => $this->context->msg( 'filedelete-legend' )->text(),
422 'items' => $fields,
423 ] );
424
425 $form = new OOUI\FormLayout( [
426 'method' => 'post',
427 'action' => $this->getAction(),
428 'id' => 'mw-img-deleteconfirm',
429 ] );
430 $form->appendContent(
431 $fieldset,
432 new OOUI\HtmlSnippet(
433 Html::hidden(
434 'wpEditToken',
435 $this->context->getUser()->getEditToken( $this->oldimage )
436 )
437 )
438 );
439
440 $this->context->getOutput()->addHTML(
441 new OOUI\PanelLayout( [
442 'classes' => [ 'deletepage-wrapper' ],
443 'expanded' => false,
444 'padded' => true,
445 'framed' => true,
446 'content' => $form,
447 ] )
448 );
449
450 if ( $this->context->getAuthority()->isAllowed( 'editinterface' ) ) {
451 $link = '';
452 if ( $suppressAllowed ) {
453 $link .= $this->linkRenderer->makeKnownLink(
454 $this->context->msg( 'filedelete-reason-dropdown-suppress' )->inContentLanguage()->getTitle(),
455 $this->context->msg( 'filedelete-edit-reasonlist-suppress' )->text(),
456 [],
457 [ 'action' => 'edit' ]
458 );
459 $link .= $this->context->msg( 'pipe-separator' )->escaped();
460 }
461 $link .= $this->linkRenderer->makeKnownLink(
462 $this->context->msg( 'filedelete-reason-dropdown' )->inContentLanguage()->getTitle(),
463 $this->context->msg( 'filedelete-edit-reasonlist' )->text(),
464 [],
465 [ 'action' => 'edit' ]
466 );
467 $this->context->getOutput()->addHTML( '<p class="mw-filedelete-editreasons">' . $link . '</p>' );
468 }
469 }
470
474 private function showLogEntries() {
475 $deleteLogPage = new LogPage( 'delete' );
476 $this->context->getOutput()->addHTML( '<h2>' . $deleteLogPage->getName()->escaped() . "</h2>\n" );
477
478 $out = $this->context->getOutput();
479 LogEventsList::showLogExtract( $out, 'delete', $this->title );
480 }
481
490 private function prepareMessage( string $message ) {
491 if ( $this->oldimage ) {
492 $lang = $this->context->getLanguage();
493 # Message keys used:
494 # 'filedelete-intro-old', 'filedelete-nofile-old', 'filedelete-success-old'
495 return $this->context->msg(
496 "{$message}-old",
497 wfEscapeWikiText( $this->title->getText() ),
498 $lang->date( $this->getTimestamp(), true ),
499 $lang->time( $this->getTimestamp(), true ),
500 wfExpandUrl( $this->file->getArchiveUrl( $this->oldimage ), PROTO_CURRENT )
501 )->parseAsBlock();
502 } else {
503 return $this->context->msg(
504 $message,
505 wfEscapeWikiText( $this->title->getText() )
506 )->parseAsBlock();
507 }
508 }
509
513 private function setHeaders() {
514 $this->context->getOutput()->setPageTitle( $this->context->msg( 'filedelete', $this->title->getText() ) );
515 $this->context->getOutput()->setRobotPolicy( 'noindex,nofollow' );
516 $this->context->getOutput()->addBacklinkSubtitle( $this->title );
517 }
518
525 public static function isValidOldSpec( $oldimage ) {
526 return strlen( $oldimage ) >= 16
527 && strpos( $oldimage, '/' ) === false
528 && strpos( $oldimage, '\\' ) === false;
529 }
530
541 public static function haveDeletableFile( &$file, &$oldfile, $oldimage ) {
542 return $oldimage
543 ? $oldfile && $oldfile->exists() && $oldfile->isLocal()
544 : $file && $file->exists() && $file->isLocal();
545 }
546
552 private function getAction() {
553 $q = [];
554 $q['action'] = 'delete';
555
556 if ( $this->oldimage ) {
557 $q['oldimage'] = $this->oldimage;
558 }
559
560 return $this->title->getLocalURL( $q );
561 }
562
568 private function getTimestamp() {
569 return $this->oldfile->getTimestamp();
570 }
571}
const PROTO_CURRENT
Definition Defines.php:195
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
if(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition Setup.php:88
An error page which can definitely be safely rendered using the OutputPage.
File deletion user interface.
getAction()
Prepare the form action.
__construct(LocalFile $file, IContextSource $context, ReadOnlyMode $readOnlyMode, RepoGroup $repoGroup, WatchlistManager $watchlistManager, LinkRenderer $linkRenderer, UserOptionsLookup $userOptionsLookup)
prepareMessage(string $message)
Prepare a message referring to the file being deleted, showing an appropriate message depending upon ...
static haveDeletableFile(&$file, &$oldfile, $oldimage)
Could we delete the file specified? If an oldimage value was provided, does it correspond to an exist...
getTimestamp()
Extract the timestamp of the old version.
showForm()
Show the confirmation form.
ReadOnlyMode $readOnlyMode
LinkRenderer $linkRenderer
RepoGroup $repoGroup
execute()
Fulfil the request; shows the form or deletes the file, pending authentication, confirmation,...
showLogEntries()
Show deletion log fragments pertaining to the current file.
setHeaders()
Set headers, titles and other bits.
LocalFile null $oldfile
WatchlistManager $watchlistManager
static isValidOldSpec( $oldimage)
Is the provided oldimage value valid?
IContextSource $context
UserOptionsLookup $userOptionsLookup
static doDelete(&$title, &$file, &$oldimage, $reason, $suppress, UserIdentity $user, $tags=[])
Really delete the file.
getTitle()
Return the associated title object.
Definition File.php:360
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition Hooks.php:173
Class to represent a local file in the wiki's own database.
Definition LocalFile.php:63
deleteOldFile( $archiveName, $reason, UserIdentity $user, $suppress=false)
Delete an old version of the file.
deleteFile( $reason, UserIdentity $user, $suppress=false)
Delete all versions of the file.
Class to simplify the use of log pages.
Definition LogPage.php:38
Class for creating new log entries and inserting them into the database.
Class that generates HTML links for pages.
MediaWikiServices is the service locator for the application scope of MediaWiki.
A StatusValue for permission errors.
Provides access to user options.
Show an error when a user tries to do something they do not have the necessary permissions for.
Show an error when the wiki is locked/read-only and the user tries to do something that requires writ...
A service class for fetching the wiki's current read-only mode.
Prioritized list of file repositories.
Definition RepoGroup.php:33
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
Represents a title within MediaWiki.
Definition Title.php:48
getPrefixedText()
Get the prefixed title with spaces.
Definition Title.php:1921
Interface for objects which can provide a MediaWiki context on request.
Interface for objects representing user identity.
const DB_PRIMARY
Definition defines.php:27
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42
if(!isset( $args[0])) $lang