MediaWiki  master
FileDeleteForm.php
Go to the documentation of this file.
1 <?php
25 
32 
36  private $title = null;
37 
41  private $file = null;
42 
46  private $user = null;
47 
51  private $oldfile = null;
52 
56  private $oldimage = '';
57 
65  public function __construct( $file, $user = null ) {
66  $this->title = $file->getTitle();
67  $this->file = $file;
68 
69  if ( $user === null ) {
71  'Construction of ' . __CLASS__ . ' without a $user parameter ' .
72  'was deprecated in MediaWiki 1.35',
73  '1.35'
74  );
75  global $wgUser;
76  $user = $wgUser;
77  }
78  $this->user = $user;
79  }
80 
85  public function execute() {
87 
88  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
89  $permissionErrors = $permissionManager->getPermissionErrors(
90  'delete',
91  $this->user,
92  $this->title
93  );
94  if ( count( $permissionErrors ) ) {
95  throw new PermissionsError( 'delete', $permissionErrors );
96  }
97 
98  if ( wfReadOnly() ) {
99  throw new ReadOnlyError;
100  }
101 
102  if ( $wgUploadMaintenance ) {
103  throw new ErrorPageError( 'filedelete-maintenance-title', 'filedelete-maintenance' );
104  }
105 
106  $this->setHeaders();
107 
108  $this->oldimage = $wgRequest->getText( 'oldimage', '' );
109  $token = $wgRequest->getText( 'wpEditToken' );
110  # Flag to hide all contents of the archived revisions
111  $suppress = $wgRequest->getCheck( 'wpSuppress' ) &&
112  $permissionManager->userHasRight( $this->user, 'suppressrevision' );
113 
114  if ( $this->oldimage ) {
115  $repoGroup = MediaWikiServices::getInstance()->getRepoGroup();
116  $this->oldfile = $repoGroup->getLocalRepo()->newFromArchiveName(
117  $this->title,
118  $this->oldimage
119  );
120  }
121 
122  if ( !self::haveDeletableFile( $this->file, $this->oldfile, $this->oldimage ) ) {
123  $wgOut->addHTML( $this->prepareMessage( 'filedelete-nofile' ) );
124  $wgOut->addReturnTo( $this->title );
125  return;
126  }
127 
128  // Perform the deletion if appropriate
129  if ( $wgRequest->wasPosted() && $this->user->matchEditToken( $token, $this->oldimage ) ) {
130  $deleteReasonList = $wgRequest->getText( 'wpDeleteReasonList' );
131  $deleteReason = $wgRequest->getText( 'wpReason' );
132 
133  if ( $deleteReasonList == 'other' ) {
134  $reason = $deleteReason;
135  } elseif ( $deleteReason != '' ) {
136  // Entry from drop down menu + additional comment
137  $reason = $deleteReasonList . wfMessage( 'colon-separator' )
138  ->inContentLanguage()->text() . $deleteReason;
139  } else {
140  $reason = $deleteReasonList;
141  }
142 
143  $status = self::doDelete(
144  $this->title,
145  $this->file,
146  $this->oldimage,
147  $reason,
148  $suppress,
149  $this->user
150  );
151 
152  if ( !$status->isGood() ) {
153  $wgOut->addHTML( '<h2>' . $this->prepareMessage( 'filedeleteerror-short' ) . "</h2>\n" );
154  $wgOut->wrapWikiTextAsInterface(
155  'error',
156  $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' )
157  );
158  }
159  if ( $status->isOK() ) {
160  $wgOut->setPageTitle( wfMessage( 'actioncomplete' ) );
161  $wgOut->addHTML( $this->prepareMessage( 'filedelete-success' ) );
162  // Return to the main page if we just deleted all versions of the
163  // file, otherwise go back to the description page
164  $wgOut->addReturnTo( $this->oldimage ? $this->title : Title::newMainPage() );
165 
167  $wgRequest->getCheck( 'wpWatch' ),
168  $this->title,
169  $this->user
170  );
171  }
172  return;
173  }
174 
175  $this->showForm();
176  $this->showLogEntries();
177  }
178 
193  public static function doDelete( &$title, &$file, &$oldimage, $reason,
194  $suppress, User $user = null, $tags = []
195  ) {
196  if ( $user === null ) {
197  wfDeprecated( __METHOD__ . ' without passing a $user parameter', '1.35' );
198  global $wgUser;
199  $user = $wgUser;
200  }
201 
202  if ( $oldimage ) {
203  $page = null;
204  $status = $file->deleteOldFile( $oldimage, $reason, $user, $suppress );
205  if ( $status->isOK() ) {
206  // Need to do a log item
207  $logComment = wfMessage( 'deletedrevision', $oldimage )->inContentLanguage()->text();
208  if ( trim( $reason ) != '' ) {
209  $logComment .= wfMessage( 'colon-separator' )
210  ->inContentLanguage()->text() . $reason;
211  }
212 
213  $logtype = $suppress ? 'suppress' : 'delete';
214 
215  $logEntry = new ManualLogEntry( $logtype, 'delete' );
216  $logEntry->setPerformer( $user );
217  $logEntry->setTarget( $title );
218  $logEntry->setComment( $logComment );
219  $logEntry->addTags( $tags );
220  $logid = $logEntry->insert();
221  $logEntry->publish( $logid );
222 
223  $status->value = $logid;
224  }
225  } else {
226  $status = Status::newFatal( 'cannotdelete',
228  );
229  $page = WikiPage::factory( $title );
230  $dbw = wfGetDB( DB_MASTER );
231  $dbw->startAtomic( __METHOD__ );
232  // delete the associated article first
233  $error = '';
234  $deleteStatus = $page->doDeleteArticleReal(
235  $reason,
236  $user,
237  $suppress,
238  null,
239  $error,
240  null,
241  $tags
242  );
243  // doDeleteArticleReal() returns a non-fatal error status if the page
244  // or revision is missing, so check for isOK() rather than isGood()
245  if ( $deleteStatus->isOK() ) {
246  $status = $file->deleteFile( $reason, $user, $suppress );
247  if ( $status->isOK() ) {
248  if ( $deleteStatus->value === null ) {
249  // No log ID from doDeleteArticleReal(), probably
250  // because the page/revision didn't exist, so create
251  // one here.
252  $logtype = $suppress ? 'suppress' : 'delete';
253  $logEntry = new ManualLogEntry( $logtype, 'delete' );
254  $logEntry->setPerformer( $user );
255  $logEntry->setTarget( clone $title );
256  $logEntry->setComment( $reason );
257  $logEntry->addTags( $tags );
258  $logid = $logEntry->insert();
259  $dbw->onTransactionPreCommitOrIdle(
260  function () use ( $logEntry, $logid ) {
261  $logEntry->publish( $logid );
262  },
263  __METHOD__
264  );
265  $status->value = $logid;
266  } else {
267  $status->value = $deleteStatus->value; // log id
268  }
269  $dbw->endAtomic( __METHOD__ );
270  } else {
271  // Page deleted but file still there? rollback page delete
272  $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
273  $lbFactory->rollbackMasterChanges( __METHOD__ );
274  }
275  } else {
276  $dbw->endAtomic( __METHOD__ );
277  }
278  }
279 
280  if ( $status->isOK() ) {
281  Hooks::runner()->onFileDeleteComplete( $file, $oldimage, $page, $user, $reason );
282  }
283 
284  return $status;
285  }
286 
290  private function showForm() {
291  global $wgOut, $wgRequest;
292  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
293 
294  $wgOut->addModules( 'mediawiki.action.delete.file' );
295 
296  $checkWatch = $this->user->getBoolOption( 'watchdeletion' ) ||
297  $this->user->isWatched( $this->title );
298 
299  $wgOut->enableOOUI();
300 
301  $fields = [];
302 
303  $fields[] = new OOUI\LabelWidget( [ 'label' => new OOUI\HtmlSnippet(
304  $this->prepareMessage( 'filedelete-intro' ) ) ]
305  );
306 
307  $suppressAllowed = $permissionManager->userHasRight( $this->user, 'suppressrevision' );
308  $dropDownReason = $wgOut->msg( 'filedelete-reason-dropdown' )->inContentLanguage()->text();
309  // Add additional specific reasons for suppress
310  if ( $suppressAllowed ) {
311  $dropDownReason .= "\n" . $wgOut->msg( 'filedelete-reason-dropdown-suppress' )
312  ->inContentLanguage()->text();
313  }
314 
315  $options = Xml::listDropDownOptions(
316  $dropDownReason,
317  [ 'other' => $wgOut->msg( 'filedelete-reason-otherlist' )->inContentLanguage()->text() ]
318  );
319  $options = Xml::listDropDownOptionsOoui( $options );
320 
321  $fields[] = new OOUI\FieldLayout(
322  new OOUI\DropdownInputWidget( [
323  'name' => 'wpDeleteReasonList',
324  'inputId' => 'wpDeleteReasonList',
325  'tabIndex' => 1,
326  'infusable' => true,
327  'value' => '',
328  'options' => $options,
329  ] ),
330  [
331  'label' => $wgOut->msg( 'filedelete-comment' )->text(),
332  'align' => 'top',
333  ]
334  );
335 
336  // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
337  // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
338  // Unicode codepoints.
339  $fields[] = new OOUI\FieldLayout(
340  new OOUI\TextInputWidget( [
341  'name' => 'wpReason',
342  'inputId' => 'wpReason',
343  'tabIndex' => 2,
345  'infusable' => true,
346  'value' => $wgRequest->getText( 'wpReason' ),
347  'autofocus' => true,
348  ] ),
349  [
350  'label' => $wgOut->msg( 'filedelete-otherreason' )->text(),
351  'align' => 'top',
352  ]
353  );
354 
355  if ( $suppressAllowed ) {
356  $fields[] = new OOUI\FieldLayout(
357  new OOUI\CheckboxInputWidget( [
358  'name' => 'wpSuppress',
359  'inputId' => 'wpSuppress',
360  'tabIndex' => 3,
361  'selected' => false,
362  ] ),
363  [
364  'label' => $wgOut->msg( 'revdelete-suppress' )->text(),
365  'align' => 'inline',
366  'infusable' => true,
367  ]
368  );
369  }
370 
371  if ( $this->user->isLoggedIn() ) {
372  $fields[] = new OOUI\FieldLayout(
373  new OOUI\CheckboxInputWidget( [
374  'name' => 'wpWatch',
375  'inputId' => 'wpWatch',
376  'tabIndex' => 3,
377  'selected' => $checkWatch,
378  ] ),
379  [
380  'label' => $wgOut->msg( 'watchthis' )->text(),
381  'align' => 'inline',
382  'infusable' => true,
383  ]
384  );
385  }
386 
387  $fields[] = new OOUI\FieldLayout(
388  new OOUI\ButtonInputWidget( [
389  'name' => 'mw-filedelete-submit',
390  'inputId' => 'mw-filedelete-submit',
391  'tabIndex' => 4,
392  'value' => $wgOut->msg( 'filedelete-submit' )->text(),
393  'label' => $wgOut->msg( 'filedelete-submit' )->text(),
394  'flags' => [ 'primary', 'destructive' ],
395  'type' => 'submit',
396  ] ),
397  [
398  'align' => 'top',
399  ]
400  );
401 
402  $fieldset = new OOUI\FieldsetLayout( [
403  'label' => $wgOut->msg( 'filedelete-legend' )->text(),
404  'items' => $fields,
405  ] );
406 
407  $form = new OOUI\FormLayout( [
408  'method' => 'post',
409  'action' => $this->getAction(),
410  'id' => 'mw-img-deleteconfirm',
411  ] );
412  $form->appendContent(
413  $fieldset,
414  new OOUI\HtmlSnippet(
415  Html::hidden( 'wpEditToken', $this->user->getEditToken( $this->oldimage ) )
416  )
417  );
418 
419  $wgOut->addHTML(
420  new OOUI\PanelLayout( [
421  'classes' => [ 'deletepage-wrapper' ],
422  'expanded' => false,
423  'padded' => true,
424  'framed' => true,
425  'content' => $form,
426  ] )
427  );
428 
429  if ( $permissionManager->userHasRight( $this->user, 'editinterface' ) ) {
430  $link = '';
431  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
432  if ( $suppressAllowed ) {
433  $link .= $linkRenderer->makeKnownLink(
434  $wgOut->msg( 'filedelete-reason-dropdown-suppress' )->inContentLanguage()->getTitle(),
435  $wgOut->msg( 'filedelete-edit-reasonlist-suppress' )->text(),
436  [],
437  [ 'action' => 'edit' ]
438  );
439  $link .= $wgOut->msg( 'pipe-separator' )->escaped();
440  }
441  $link .= $linkRenderer->makeKnownLink(
442  $wgOut->msg( 'filedelete-reason-dropdown' )->inContentLanguage()->getTitle(),
443  $wgOut->msg( 'filedelete-edit-reasonlist' )->text(),
444  [],
445  [ 'action' => 'edit' ]
446  );
447  $wgOut->addHTML( '<p class="mw-filedelete-editreasons">' . $link . '</p>' );
448  }
449  }
450 
454  private function showLogEntries() {
455  global $wgOut;
456  $deleteLogPage = new LogPage( 'delete' );
457  $wgOut->addHTML( '<h2>' . $deleteLogPage->getName()->escaped() . "</h2>\n" );
458  LogEventsList::showLogExtract( $wgOut, 'delete', $this->title );
459  }
460 
469  private function prepareMessage( $message ) {
470  global $wgLang;
471  if ( $this->oldimage ) {
472  # Message keys used:
473  # 'filedelete-intro-old', 'filedelete-nofile-old', 'filedelete-success-old'
474  return wfMessage(
475  "{$message}-old",
476  wfEscapeWikiText( $this->title->getText() ),
477  $wgLang->date( $this->getTimestamp(), true ),
478  $wgLang->time( $this->getTimestamp(), true ),
479  wfExpandUrl( $this->file->getArchiveUrl( $this->oldimage ), PROTO_CURRENT ) )->parseAsBlock();
480  } else {
481  return wfMessage(
482  $message,
483  wfEscapeWikiText( $this->title->getText() )
484  )->parseAsBlock();
485  }
486  }
487 
491  private function setHeaders() {
492  global $wgOut;
493  $wgOut->setPageTitle( wfMessage( 'filedelete', $this->title->getText() ) );
494  $wgOut->setRobotPolicy( 'noindex,nofollow' );
495  $wgOut->addBacklinkSubtitle( $this->title );
496  }
497 
504  public static function isValidOldSpec( $oldimage ) {
505  return strlen( $oldimage ) >= 16
506  && strpos( $oldimage, '/' ) === false
507  && strpos( $oldimage, '\\' ) === false;
508  }
509 
520  public static function haveDeletableFile( &$file, &$oldfile, $oldimage ) {
521  return $oldimage
522  ? $oldfile && $oldfile->exists() && $oldfile->isLocal()
523  : $file && $file->exists() && $file->isLocal();
524  }
525 
531  private function getAction() {
532  $q = [];
533  $q['action'] = 'delete';
534 
535  if ( $this->oldimage ) {
536  $q['oldimage'] = $this->oldimage;
537  }
538 
539  return $this->title->getLocalURL( $q );
540  }
541 
547  private function getTimestamp() {
548  return $this->oldfile->getTimestamp();
549  }
550 }
ReadOnlyError
Show an error when the wiki is locked/read-only and the user tries to do something that requires writ...
Definition: ReadOnlyError.php:29
LocalFile\deleteFile
deleteFile( $reason, User $user, $suppress=false)
Delete all versions of the file.
Definition: LocalFile.php:2082
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
Xml\listDropDownOptionsOoui
static listDropDownOptionsOoui( $options)
Convert options for a drop-down box into a format accepted by OOUI\DropdownInputWidget etc.
Definition: Xml.php:585
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:154
$wgUploadMaintenance
$wgUploadMaintenance
To disable file delete/restore temporarily.
Definition: DefaultSettings.php:9077
Title\getPrefixedText
getPrefixedText()
Get the prefixed title with spaces.
Definition: Title.php:1850
wfReadOnly
wfReadOnly()
Check whether the wiki is in read-only mode.
Definition: GlobalFunctions.php:1125
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1219
PermissionsError
Show an error when a user tries to do something they do not have the necessary permissions for.
Definition: PermissionsError.php:31
Title\newMainPage
static newMainPage(MessageLocalizer $localizer=null)
Create a new Title for the Main Page.
Definition: Title.php:654
FileDeleteForm\getTimestamp
getTimestamp()
Extract the timestamp of the old version.
Definition: FileDeleteForm.php:547
FileDeleteForm\getAction
getAction()
Prepare the form action.
Definition: FileDeleteForm.php:531
wfDeprecatedMsg
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
Definition: GlobalFunctions.php:1058
WikiPage\factory
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:156
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1026
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2467
FileDeleteForm\$user
User $user
Definition: FileDeleteForm.php:46
$wgLang
$wgLang
Definition: Setup.php:776
LogPage
Class to simplify the use of log pages.
Definition: LogPage.php:37
PROTO_CURRENT
const PROTO_CURRENT
Definition: Defines.php:211
FileDeleteForm
File deletion user interface.
Definition: FileDeleteForm.php:31
LogEventsList\showLogExtract
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
Definition: LogEventsList.php:639
DB_MASTER
const DB_MASTER
Definition: defines.php:26
LocalFile
Class to represent a local file in the wiki's own database.
Definition: LocalFile.php:59
Html\hidden
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:802
Hooks\runner
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:172
FileDeleteForm\prepareMessage
prepareMessage( $message)
Prepare a message referring to the file being deleted, showing an appropriate message depending upon ...
Definition: FileDeleteForm.php:469
FileDeleteForm\showForm
showForm()
Show the confirmation form.
Definition: FileDeleteForm.php:290
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1487
WatchAction\doWatchOrUnwatch
static doWatchOrUnwatch( $watch, Title $title, User $user, string $expiry=null)
Watch or unwatch a page.
Definition: WatchAction.php:172
CommentStore\COMMENT_CHARACTER_LIMIT
const COMMENT_CHARACTER_LIMIT
Maximum length of a comment in UTF-8 characters.
Definition: CommentStore.php:48
File\getTitle
getTitle()
Return the associated title object.
Definition: File.php:345
Title
Represents a title within MediaWiki.
Definition: Title.php:42
FileDeleteForm\$oldfile
LocalFile $oldfile
Definition: FileDeleteForm.php:51
FileDeleteForm\$oldimage
string $oldimage
Definition: FileDeleteForm.php:56
FileDeleteForm\__construct
__construct( $file, $user=null)
Option to pass a user added in 1.35 Constructing without passing a user is hard deprecated in 1....
Definition: FileDeleteForm.php:65
LocalFile\deleteOldFile
deleteOldFile( $archiveName, $reason, User $user, $suppress=false)
Delete an old version of the file.
Definition: LocalFile.php:2171
FileDeleteForm\execute
execute()
Fulfil the request; shows the form or deletes the file, pending authentication, confirmation,...
Definition: FileDeleteForm.php:85
FileDeleteForm\showLogEntries
showLogEntries()
Show deletion log fragments pertaining to the current file.
Definition: FileDeleteForm.php:454
File\isLocal
isLocal()
Returns true if the file comes from the local file repository.
Definition: File.php:1951
FileDeleteForm\setHeaders
setHeaders()
Set headers, titles and other bits.
Definition: FileDeleteForm.php:491
FileDeleteForm\$title
Title $title
Definition: FileDeleteForm.php:36
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:42
FileDeleteForm\doDelete
static doDelete(&$title, &$file, &$oldimage, $reason, $suppress, User $user=null, $tags=[])
Really delete the file.
Definition: FileDeleteForm.php:193
$wgRequest
if(! $wgDBerrorLogTZ) $wgRequest
Definition: Setup.php:644
$wgOut
$wgOut
Definition: Setup.php:781
ErrorPageError
An error page which can definitely be safely rendered using the OutputPage.
Definition: ErrorPageError.php:30
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:56
FileDeleteForm\$file
LocalFile $file
Definition: FileDeleteForm.php:41
LocalFile\exists
exists()
canRender inherited
Definition: LocalFile.php:1007
FileDeleteForm\haveDeletableFile
static haveDeletableFile(&$file, &$oldfile, $oldimage)
Could we delete the file specified? If an oldimage value was provided, does it correspond to an exist...
Definition: FileDeleteForm.php:520
wfExpandUrl
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
Definition: GlobalFunctions.php:490
Xml\listDropDownOptions
static listDropDownOptions( $list, $params=[])
Build options for a drop-down box from a textual list.
Definition: Xml.php:543
FileDeleteForm\isValidOldSpec
static isValidOldSpec( $oldimage)
Is the provided oldimage value valid?
Definition: FileDeleteForm.php:504