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