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 $oldfile = null;
47  private $oldimage = '';
48 
52  public function __construct( $file ) {
53  $this->title = $file->getTitle();
54  $this->file = $file;
55  }
56 
61  public function execute() {
62  global $wgOut, $wgRequest, $wgUser, $wgUploadMaintenance;
63 
64  $permissionErrors = $this->title->getUserPermissionsErrors( 'delete', $wgUser );
65  if ( count( $permissionErrors ) ) {
66  throw new PermissionsError( 'delete', $permissionErrors );
67  }
68 
69  if ( wfReadOnly() ) {
70  throw new ReadOnlyError;
71  }
72 
73  if ( $wgUploadMaintenance ) {
74  throw new ErrorPageError( 'filedelete-maintenance-title', 'filedelete-maintenance' );
75  }
76 
77  $this->setHeaders();
78 
79  $this->oldimage = $wgRequest->getText( 'oldimage', false );
80  $token = $wgRequest->getText( 'wpEditToken' );
81  # Flag to hide all contents of the archived revisions
82  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
83  $suppress = $wgRequest->getCheck( 'wpSuppress' ) &&
84  $permissionManager->userHasRight( $wgUser, 'suppressrevision' );
85 
86  if ( $this->oldimage ) {
87  $this->oldfile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName(
88  $this->title,
89  $this->oldimage
90  );
91  }
92 
93  if ( !self::haveDeletableFile( $this->file, $this->oldfile, $this->oldimage ) ) {
94  $wgOut->addHTML( $this->prepareMessage( 'filedelete-nofile' ) );
95  $wgOut->addReturnTo( $this->title );
96  return;
97  }
98 
99  // Perform the deletion if appropriate
100  if ( $wgRequest->wasPosted() && $wgUser->matchEditToken( $token, $this->oldimage ) ) {
101  $deleteReasonList = $wgRequest->getText( 'wpDeleteReasonList' );
102  $deleteReason = $wgRequest->getText( 'wpReason' );
103 
104  if ( $deleteReasonList == 'other' ) {
105  $reason = $deleteReason;
106  } elseif ( $deleteReason != '' ) {
107  // Entry from drop down menu + additional comment
108  $reason = $deleteReasonList . wfMessage( 'colon-separator' )
109  ->inContentLanguage()->text() . $deleteReason;
110  } else {
111  $reason = $deleteReasonList;
112  }
113 
114  $status = self::doDelete(
115  $this->title,
116  $this->file,
117  $this->oldimage,
118  $reason,
119  $suppress,
120  $wgUser
121  );
122 
123  if ( !$status->isGood() ) {
124  $wgOut->addHTML( '<h2>' . $this->prepareMessage( 'filedeleteerror-short' ) . "</h2>\n" );
125  $wgOut->wrapWikiTextAsInterface(
126  'error',
127  $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' )
128  );
129  }
130  if ( $status->isOK() ) {
131  $wgOut->setPageTitle( wfMessage( 'actioncomplete' ) );
132  $wgOut->addHTML( $this->prepareMessage( 'filedelete-success' ) );
133  // Return to the main page if we just deleted all versions of the
134  // file, otherwise go back to the description page
135  $wgOut->addReturnTo( $this->oldimage ? $this->title : Title::newMainPage() );
136 
137  WatchAction::doWatchOrUnwatch( $wgRequest->getCheck( 'wpWatch' ), $this->title, $wgUser );
138  }
139  return;
140  }
141 
142  $this->showForm();
143  $this->showLogEntries();
144  }
145 
159  public static function doDelete( &$title, &$file, &$oldimage, $reason,
160  $suppress, User $user = null, $tags = []
161  ) {
162  if ( $user === null ) {
163  global $wgUser;
164  $user = $wgUser;
165  }
166 
167  if ( $oldimage ) {
168  $page = null;
169  $status = $file->deleteOld( $oldimage, $reason, $suppress, $user );
170  if ( $status->isOK() ) {
171  // Need to do a log item
172  $logComment = wfMessage( 'deletedrevision', $oldimage )->inContentLanguage()->text();
173  if ( trim( $reason ) != '' ) {
174  $logComment .= wfMessage( 'colon-separator' )
175  ->inContentLanguage()->text() . $reason;
176  }
177 
178  $logtype = $suppress ? 'suppress' : 'delete';
179 
180  $logEntry = new ManualLogEntry( $logtype, 'delete' );
181  $logEntry->setPerformer( $user );
182  $logEntry->setTarget( $title );
183  $logEntry->setComment( $logComment );
184  $logEntry->addTags( $tags );
185  $logid = $logEntry->insert();
186  $logEntry->publish( $logid );
187 
188  $status->value = $logid;
189  }
190  } else {
191  $status = Status::newFatal( 'cannotdelete',
193  );
194  $page = WikiPage::factory( $title );
195  $dbw = wfGetDB( DB_MASTER );
196  $dbw->startAtomic( __METHOD__ );
197  // delete the associated article first
198  $error = '';
199  $deleteStatus = $page->doDeleteArticleReal( $reason, $suppress, 0, false, $error,
200  $user, $tags );
201  // doDeleteArticleReal() returns a non-fatal error status if the page
202  // or revision is missing, so check for isOK() rather than isGood()
203  if ( $deleteStatus->isOK() ) {
204  $status = $file->delete( $reason, $suppress, $user );
205  if ( $status->isOK() ) {
206  if ( $deleteStatus->value === null ) {
207  // No log ID from doDeleteArticleReal(), probably
208  // because the page/revision didn't exist, so create
209  // one here.
210  $logtype = $suppress ? 'suppress' : 'delete';
211  $logEntry = new ManualLogEntry( $logtype, 'delete' );
212  $logEntry->setPerformer( $user );
213  $logEntry->setTarget( clone $title );
214  $logEntry->setComment( $reason );
215  $logEntry->addTags( $tags );
216  $logid = $logEntry->insert();
217  $dbw->onTransactionPreCommitOrIdle(
218  function () use ( $logEntry, $logid ) {
219  $logEntry->publish( $logid );
220  },
221  __METHOD__
222  );
223  $status->value = $logid;
224  } else {
225  $status->value = $deleteStatus->value; // log id
226  }
227  $dbw->endAtomic( __METHOD__ );
228  } else {
229  // Page deleted but file still there? rollback page delete
230  $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
231  $lbFactory->rollbackMasterChanges( __METHOD__ );
232  }
233  } else {
234  $dbw->endAtomic( __METHOD__ );
235  }
236  }
237 
238  if ( $status->isOK() ) {
239  Hooks::run( 'FileDeleteComplete', [ &$file, &$oldimage, &$page, &$user, &$reason ] );
240  }
241 
242  return $status;
243  }
244 
248  private function showForm() {
249  global $wgOut, $wgUser, $wgRequest;
250  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
251 
252  $wgOut->addModules( 'mediawiki.action.delete.file' );
253 
254  $checkWatch = $wgUser->getBoolOption( 'watchdeletion' ) || $wgUser->isWatched( $this->title );
255 
256  $wgOut->enableOOUI();
257 
258  $fields = [];
259 
260  $fields[] = new OOUI\LabelWidget( [ 'label' => new OOUI\HtmlSnippet(
261  $this->prepareMessage( 'filedelete-intro' ) ) ]
262  );
263 
264  $options = Xml::listDropDownOptions(
265  $wgOut->msg( 'filedelete-reason-dropdown' )->inContentLanguage()->text(),
266  [ 'other' => $wgOut->msg( 'filedelete-reason-otherlist' )->inContentLanguage()->text() ]
267  );
268  $options = Xml::listDropDownOptionsOoui( $options );
269 
270  $fields[] = new OOUI\FieldLayout(
271  new OOUI\DropdownInputWidget( [
272  'name' => 'wpDeleteReasonList',
273  'inputId' => 'wpDeleteReasonList',
274  'tabIndex' => 1,
275  'infusable' => true,
276  'value' => '',
277  'options' => $options,
278  ] ),
279  [
280  'label' => $wgOut->msg( 'filedelete-comment' )->text(),
281  'align' => 'top',
282  ]
283  );
284 
285  // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
286  // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
287  // Unicode codepoints.
288  $fields[] = new OOUI\FieldLayout(
289  new OOUI\TextInputWidget( [
290  'name' => 'wpReason',
291  'inputId' => 'wpReason',
292  'tabIndex' => 2,
294  'infusable' => true,
295  'value' => $wgRequest->getText( 'wpReason' ),
296  'autofocus' => true,
297  ] ),
298  [
299  'label' => $wgOut->msg( 'filedelete-otherreason' )->text(),
300  'align' => 'top',
301  ]
302  );
303 
304  if ( $permissionManager->userHasRight( $wgUser, 'suppressrevision' ) ) {
305  $fields[] = new OOUI\FieldLayout(
306  new OOUI\CheckboxInputWidget( [
307  'name' => 'wpSuppress',
308  'inputId' => 'wpSuppress',
309  'tabIndex' => 3,
310  'selected' => false,
311  ] ),
312  [
313  'label' => $wgOut->msg( 'revdelete-suppress' )->text(),
314  'align' => 'inline',
315  'infusable' => true,
316  ]
317  );
318  }
319 
320  if ( $wgUser->isLoggedIn() ) {
321  $fields[] = new OOUI\FieldLayout(
322  new OOUI\CheckboxInputWidget( [
323  'name' => 'wpWatch',
324  'inputId' => 'wpWatch',
325  'tabIndex' => 3,
326  'selected' => $checkWatch,
327  ] ),
328  [
329  'label' => $wgOut->msg( 'watchthis' )->text(),
330  'align' => 'inline',
331  'infusable' => true,
332  ]
333  );
334  }
335 
336  $fields[] = new OOUI\FieldLayout(
337  new OOUI\ButtonInputWidget( [
338  'name' => 'mw-filedelete-submit',
339  'inputId' => 'mw-filedelete-submit',
340  'tabIndex' => 4,
341  'value' => $wgOut->msg( 'filedelete-submit' )->text(),
342  'label' => $wgOut->msg( 'filedelete-submit' )->text(),
343  'flags' => [ 'primary', 'destructive' ],
344  'type' => 'submit',
345  ] ),
346  [
347  'align' => 'top',
348  ]
349  );
350 
351  $fieldset = new OOUI\FieldsetLayout( [
352  'label' => $wgOut->msg( 'filedelete-legend' )->text(),
353  'items' => $fields,
354  ] );
355 
356  $form = new OOUI\FormLayout( [
357  'method' => 'post',
358  'action' => $this->getAction(),
359  'id' => 'mw-img-deleteconfirm',
360  ] );
361  $form->appendContent(
362  $fieldset,
363  new OOUI\HtmlSnippet(
364  Html::hidden( 'wpEditToken', $wgUser->getEditToken( $this->oldimage ) )
365  )
366  );
367 
368  $wgOut->addHTML(
369  new OOUI\PanelLayout( [
370  'classes' => [ 'deletepage-wrapper' ],
371  'expanded' => false,
372  'padded' => true,
373  'framed' => true,
374  'content' => $form,
375  ] )
376  );
377 
378  if ( $permissionManager->userHasRight( $wgUser, 'editinterface' ) ) {
379  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
380  $link = $linkRenderer->makeKnownLink(
381  $wgOut->msg( 'filedelete-reason-dropdown' )->inContentLanguage()->getTitle(),
382  wfMessage( 'filedelete-edit-reasonlist' )->text(),
383  [],
384  [ 'action' => 'edit' ]
385  );
386  $wgOut->addHTML( '<p class="mw-filedelete-editreasons">' . $link . '</p>' );
387  }
388  }
389 
393  private function showLogEntries() {
394  global $wgOut;
395  $deleteLogPage = new LogPage( 'delete' );
396  $wgOut->addHTML( '<h2>' . $deleteLogPage->getName()->escaped() . "</h2>\n" );
397  LogEventsList::showLogExtract( $wgOut, 'delete', $this->title );
398  }
399 
408  private function prepareMessage( $message ) {
409  global $wgLang;
410  if ( $this->oldimage ) {
411  # Message keys used:
412  # 'filedelete-intro-old', 'filedelete-nofile-old', 'filedelete-success-old'
413  return wfMessage(
414  "{$message}-old",
415  wfEscapeWikiText( $this->title->getText() ),
416  $wgLang->date( $this->getTimestamp(), true ),
417  $wgLang->time( $this->getTimestamp(), true ),
418  wfExpandUrl( $this->file->getArchiveUrl( $this->oldimage ), PROTO_CURRENT ) )->parseAsBlock();
419  } else {
420  return wfMessage(
421  $message,
422  wfEscapeWikiText( $this->title->getText() )
423  )->parseAsBlock();
424  }
425  }
426 
430  private function setHeaders() {
431  global $wgOut;
432  $wgOut->setPageTitle( wfMessage( 'filedelete', $this->title->getText() ) );
433  $wgOut->setRobotPolicy( 'noindex,nofollow' );
434  $wgOut->addBacklinkSubtitle( $this->title );
435  }
436 
443  public static function isValidOldSpec( $oldimage ) {
444  return strlen( $oldimage ) >= 16
445  && strpos( $oldimage, '/' ) === false
446  && strpos( $oldimage, '\\' ) === false;
447  }
448 
459  public static function haveDeletableFile( &$file, &$oldfile, $oldimage ) {
460  return $oldimage
461  ? $oldfile && $oldfile->exists() && $oldfile->isLocal()
462  : $file && $file->exists() && $file->isLocal();
463  }
464 
470  private function getAction() {
471  $q = [];
472  $q['action'] = 'delete';
473 
474  if ( $this->oldimage ) {
475  $q['oldimage'] = $this->oldimage;
476  }
477 
478  return $this->title->getLocalURL( $q );
479  }
480 
486  private function getTimestamp() {
487  return $this->oldfile->getTimestamp();
488  }
489 }
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:142
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:69
exists()
canRender inherited
Definition: LocalFile.php:969
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking, formatting, etc.
static newMainPage(MessageLocalizer $localizer=null)
Create a new Title for the Main Page.
Definition: Title.php:648
Show an error when the wiki is locked/read-only and the user tries to do something that requires writ...
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
LocalFile $oldfile
const PROTO_CURRENT
Definition: Defines.php:202
delete( $reason, $suppress=false, $user=null)
Delete all versions of the file.
Definition: LocalFile.php:1932
getTimestamp()
Extract the timestamp of the old version.
getPrefixedText()
Get the prefixed title with spaces.
Definition: Title.php:1858
const COMMENT_CHARACTER_LIMIT
Maximum length of a comment in UTF-8 characters.
isLocal()
Returns true if the file comes from the local file repository.
Definition: File.php:1861
getTitle()
Return the associated title object.
Definition: File.php:336
const DB_MASTER
Definition: defines.php:26
Class to simplify the use of log pages.
Definition: LogPage.php:33
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:51
wfReadOnly()
Check whether the wiki is in read-only mode.
$wgLang
Definition: Setup.php:856
An error page which can definitely be safely rendered using the OutputPage.
getAction()
Prepare the form action.
static isValidOldSpec( $oldimage)
Is the provided oldimage value valid?
static doWatchOrUnwatch( $watch, Title $title, User $user)
Watch or unwatch a page.
Definition: WatchAction.php:91
static singleton()
Definition: RepoGroup.php:60
static listDropDownOptions( $list, $params=[])
Build options for a drop-down box from a textual list.
Definition: Xml.php:539
setHeaders()
Set headers, titles and other bits.
static doDelete(&$title, &$file, &$oldimage, $reason, $suppress, User $user=null, $tags=[])
Really delete the file.
static haveDeletableFile(&$file, &$oldfile, $oldimage)
Could we delete the file specified? If an oldimage value was provided, does it correspond to an exist...
showLogEntries()
Show deletion log fragments pertaining to the current file.
prepareMessage( $message)
Prepare a message referring to the file being deleted, showing an appropriate message depending upon ...
deleteOld( $archiveName, $reason, $suppress=false, $user=null)
Delete an old version of the file.
Definition: LocalFile.php:1990
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:802
Show an error when a user tries to do something they do not have the necessary permissions for...
execute()
Fulfil the request; shows the form or deletes the file, pending authentication, confirmation, etc.
if(! $wgDBerrorLogTZ) $wgRequest
Definition: Setup.php:727
$wgOut
Definition: Setup.php:861
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
showForm()
Show the confirmation form.
$wgUploadMaintenance
To disable file delete/restore temporarily.
File deletion user interface.
return true
Definition: router.php:92
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
static listDropDownOptionsOoui( $options)
Convert options for a drop-down box into a format accepted by OOUI\DropdownInputWidget etc...
Definition: Xml.php:581
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.