MediaWiki  master
FileDeleteForm.php
Go to the documentation of this file.
1 <?php
25 
32 
34  private $title = null;
35 
37  private $file = null;
38 
40  private $user = null;
41 
43  private $oldfile = null;
44 
46  private $oldimage = '';
47 
49  private $out = null;
50 
56  public function __construct( $file, User $user, OutputPage $out ) {
57  $this->title = $file->getTitle();
58  $this->file = $file;
59  $this->user = $user;
60  $this->out = $out;
61  }
62 
67  public function execute() {
69 
70  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
71  $permissionErrors = $permissionManager->getPermissionErrors(
72  'delete',
73  $this->user,
74  $this->title
75  );
76  if ( count( $permissionErrors ) ) {
77  throw new PermissionsError( 'delete', $permissionErrors );
78  }
79 
80  if ( wfReadOnly() ) {
81  throw new ReadOnlyError;
82  }
83 
84  if ( $wgUploadMaintenance ) {
85  throw new ErrorPageError( 'filedelete-maintenance-title', 'filedelete-maintenance' );
86  }
87 
88  $this->setHeaders();
89 
90  $this->oldimage = $wgRequest->getText( 'oldimage', '' );
91  $token = $wgRequest->getText( 'wpEditToken' );
92  # Flag to hide all contents of the archived revisions
93  $suppress = $wgRequest->getCheck( 'wpSuppress' ) &&
94  $permissionManager->userHasRight( $this->user, 'suppressrevision' );
95 
96  if ( $this->oldimage ) {
97  $repoGroup = MediaWikiServices::getInstance()->getRepoGroup();
98  $this->oldfile = $repoGroup->getLocalRepo()->newFromArchiveName(
99  $this->title,
100  $this->oldimage
101  );
102  }
103 
104  if ( !self::haveDeletableFile( $this->file, $this->oldfile, $this->oldimage ) ) {
105  $this->out->addHTML( $this->prepareMessage( 'filedelete-nofile' ) );
106  $this->out->addReturnTo( $this->title );
107  return;
108  }
109 
110  // Perform the deletion if appropriate
111  if ( $wgRequest->wasPosted() && $this->user->matchEditToken( $token, $this->oldimage ) ) {
112  $deleteReasonList = $wgRequest->getText( 'wpDeleteReasonList' );
113  $deleteReason = $wgRequest->getText( 'wpReason' );
114 
115  if ( $deleteReasonList == 'other' ) {
116  $reason = $deleteReason;
117  } elseif ( $deleteReason != '' ) {
118  // Entry from drop down menu + additional comment
119  $reason = $deleteReasonList . wfMessage( 'colon-separator' )
120  ->inContentLanguage()->text() . $deleteReason;
121  } else {
122  $reason = $deleteReasonList;
123  }
124 
125  $status = self::doDelete(
126  $this->title,
127  $this->file,
128  $this->oldimage,
129  $reason,
130  $suppress,
131  $this->user
132  );
133 
134  if ( !$status->isGood() ) {
135  $this->out->addHTML( '<h2>' . $this->prepareMessage( 'filedeleteerror-short' ) . "</h2>\n" );
136  $this->out->wrapWikiTextAsInterface(
137  'error',
138  $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' )
139  );
140  }
141  if ( $status->isOK() ) {
142  $this->out->setPageTitle( wfMessage( 'actioncomplete' ) );
143  $this->out->addHTML( $this->prepareMessage( 'filedelete-success' ) );
144  // Return to the main page if we just deleted all versions of the
145  // file, otherwise go back to the description page
146  $this->out->addReturnTo( $this->oldimage ? $this->title : Title::newMainPage() );
147 
149  $wgRequest->getCheck( 'wpWatch' ),
150  $this->title,
151  $this->user
152  );
153  }
154  return;
155  }
156 
157  $this->showForm();
158  $this->showLogEntries();
159  }
160 
174  public static function doDelete( &$title, &$file, &$oldimage, $reason,
175  $suppress, User $user, $tags = []
176  ) {
177  if ( $oldimage ) {
178  $page = null;
179  $status = $file->deleteOldFile( $oldimage, $reason, $user, $suppress );
180  if ( $status->isOK() ) {
181  // Need to do a log item
182  $logComment = wfMessage( 'deletedrevision', $oldimage )->inContentLanguage()->text();
183  if ( trim( $reason ) != '' ) {
184  $logComment .= wfMessage( 'colon-separator' )
185  ->inContentLanguage()->text() . $reason;
186  }
187 
188  $logtype = $suppress ? 'suppress' : 'delete';
189 
190  $logEntry = new ManualLogEntry( $logtype, 'delete' );
191  $logEntry->setPerformer( $user );
192  $logEntry->setTarget( $title );
193  $logEntry->setComment( $logComment );
194  $logEntry->addTags( $tags );
195  $logid = $logEntry->insert();
196  $logEntry->publish( $logid );
197 
198  $status->value = $logid;
199  }
200  } else {
201  $status = Status::newFatal( 'cannotdelete',
203  );
204  $page = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title );
205  '@phan-var WikiFilePage $page';
206  $dbw = wfGetDB( DB_MASTER );
207  $dbw->startAtomic( __METHOD__ );
208  // delete the associated article first
209  $error = '';
210  $deleteStatus = $page->doDeleteArticleReal(
211  $reason,
212  $user,
213  $suppress,
214  null,
215  $error,
216  null,
217  $tags
218  );
219  // doDeleteArticleReal() returns a non-fatal error status if the page
220  // or revision is missing, so check for isOK() rather than isGood()
221  if ( $deleteStatus->isOK() ) {
222  $status = $file->deleteFile( $reason, $user, $suppress );
223  if ( $status->isOK() ) {
224  if ( $deleteStatus->value === null ) {
225  // No log ID from doDeleteArticleReal(), probably
226  // because the page/revision didn't exist, so create
227  // one here.
228  $logtype = $suppress ? 'suppress' : 'delete';
229  $logEntry = new ManualLogEntry( $logtype, 'delete' );
230  $logEntry->setPerformer( $user );
231  $logEntry->setTarget( clone $title );
232  $logEntry->setComment( $reason );
233  $logEntry->addTags( $tags );
234  $logid = $logEntry->insert();
235  $dbw->onTransactionPreCommitOrIdle(
236  function () use ( $logEntry, $logid ) {
237  $logEntry->publish( $logid );
238  },
239  __METHOD__
240  );
241  $status->value = $logid;
242  } else {
243  $status->value = $deleteStatus->value; // log id
244  }
245  $dbw->endAtomic( __METHOD__ );
246  } else {
247  // Page deleted but file still there? rollback page delete
248  $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
249  $lbFactory->rollbackMasterChanges( __METHOD__ );
250  }
251  } else {
252  $dbw->endAtomic( __METHOD__ );
253  }
254  }
255 
256  if ( $status->isOK() ) {
257  Hooks::runner()->onFileDeleteComplete( $file, $oldimage, $page, $user, $reason );
258  }
259 
260  return $status;
261  }
262 
266  private function showForm() {
267  global $wgRequest;
268  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
269 
270  $this->out->addModules( 'mediawiki.action.delete' );
271 
272  $checkWatch = $this->user->getBoolOption( 'watchdeletion' ) ||
273  $this->user->isWatched( $this->title );
274 
275  $this->out->enableOOUI();
276 
277  $fields = [];
278 
279  $fields[] = new OOUI\LabelWidget( [ 'label' => new OOUI\HtmlSnippet(
280  $this->prepareMessage( 'filedelete-intro' ) ) ]
281  );
282 
283  $suppressAllowed = $permissionManager->userHasRight( $this->user, 'suppressrevision' );
284  $dropDownReason = $this->out->msg( 'filedelete-reason-dropdown' )->inContentLanguage()->text();
285  // Add additional specific reasons for suppress
286  if ( $suppressAllowed ) {
287  $dropDownReason .= "\n" . $this->out->msg( 'filedelete-reason-dropdown-suppress' )
288  ->inContentLanguage()->text();
289  }
290 
291  $options = Xml::listDropDownOptions(
292  $dropDownReason,
293  [ 'other' => $this->out->msg( 'filedelete-reason-otherlist' )->inContentLanguage()->text() ]
294  );
295  $options = Xml::listDropDownOptionsOoui( $options );
296 
297  $fields[] = new OOUI\FieldLayout(
298  new OOUI\DropdownInputWidget( [
299  'name' => 'wpDeleteReasonList',
300  'inputId' => 'wpDeleteReasonList',
301  'tabIndex' => 1,
302  'infusable' => true,
303  'value' => '',
304  'options' => $options,
305  ] ),
306  [
307  'label' => $this->out->msg( 'filedelete-comment' )->text(),
308  'align' => 'top',
309  ]
310  );
311 
312  // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
313  // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
314  // Unicode codepoints.
315  $fields[] = new OOUI\FieldLayout(
316  new OOUI\TextInputWidget( [
317  'name' => 'wpReason',
318  'inputId' => 'wpReason',
319  'tabIndex' => 2,
321  'infusable' => true,
322  'value' => $wgRequest->getText( 'wpReason' ),
323  'autofocus' => true,
324  ] ),
325  [
326  'label' => $this->out->msg( 'filedelete-otherreason' )->text(),
327  'align' => 'top',
328  ]
329  );
330 
331  if ( $suppressAllowed ) {
332  $fields[] = new OOUI\FieldLayout(
333  new OOUI\CheckboxInputWidget( [
334  'name' => 'wpSuppress',
335  'inputId' => 'wpSuppress',
336  'tabIndex' => 3,
337  'selected' => false,
338  ] ),
339  [
340  'label' => $this->out->msg( 'revdelete-suppress' )->text(),
341  'align' => 'inline',
342  'infusable' => true,
343  ]
344  );
345  }
346 
347  if ( $this->user->isLoggedIn() ) {
348  $fields[] = new OOUI\FieldLayout(
349  new OOUI\CheckboxInputWidget( [
350  'name' => 'wpWatch',
351  'inputId' => 'wpWatch',
352  'tabIndex' => 3,
353  'selected' => $checkWatch,
354  ] ),
355  [
356  'label' => $this->out->msg( 'watchthis' )->text(),
357  'align' => 'inline',
358  'infusable' => true,
359  ]
360  );
361  }
362 
363  $fields[] = new OOUI\FieldLayout(
364  new OOUI\ButtonInputWidget( [
365  'name' => 'mw-filedelete-submit',
366  'inputId' => 'mw-filedelete-submit',
367  'tabIndex' => 4,
368  'value' => $this->out->msg( 'filedelete-submit' )->text(),
369  'label' => $this->out->msg( 'filedelete-submit' )->text(),
370  'flags' => [ 'primary', 'destructive' ],
371  'type' => 'submit',
372  ] ),
373  [
374  'align' => 'top',
375  ]
376  );
377 
378  $fieldset = new OOUI\FieldsetLayout( [
379  'label' => $this->out->msg( 'filedelete-legend' )->text(),
380  'items' => $fields,
381  ] );
382 
383  $form = new OOUI\FormLayout( [
384  'method' => 'post',
385  'action' => $this->getAction(),
386  'id' => 'mw-img-deleteconfirm',
387  ] );
388  $form->appendContent(
389  $fieldset,
390  new OOUI\HtmlSnippet(
391  Html::hidden( 'wpEditToken', $this->user->getEditToken( $this->oldimage ) )
392  )
393  );
394 
395  $this->out->addHTML(
396  new OOUI\PanelLayout( [
397  'classes' => [ 'deletepage-wrapper' ],
398  'expanded' => false,
399  'padded' => true,
400  'framed' => true,
401  'content' => $form,
402  ] )
403  );
404 
405  if ( $permissionManager->userHasRight( $this->user, 'editinterface' ) ) {
406  $link = '';
407  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
408  if ( $suppressAllowed ) {
409  $link .= $linkRenderer->makeKnownLink(
410  $this->out->msg( 'filedelete-reason-dropdown-suppress' )->inContentLanguage()->getTitle(),
411  $this->out->msg( 'filedelete-edit-reasonlist-suppress' )->text(),
412  [],
413  [ 'action' => 'edit' ]
414  );
415  $link .= $this->out->msg( 'pipe-separator' )->escaped();
416  }
417  $link .= $linkRenderer->makeKnownLink(
418  $this->out->msg( 'filedelete-reason-dropdown' )->inContentLanguage()->getTitle(),
419  $this->out->msg( 'filedelete-edit-reasonlist' )->text(),
420  [],
421  [ 'action' => 'edit' ]
422  );
423  $this->out->addHTML( '<p class="mw-filedelete-editreasons">' . $link . '</p>' );
424  }
425  }
426 
430  private function showLogEntries() {
431  $deleteLogPage = new LogPage( 'delete' );
432  $this->out->addHTML( '<h2>' . $deleteLogPage->getName()->escaped() . "</h2>\n" );
433 
434  // False positive. First paramater is assigned to a string if not an instance of
435  // OutputPage, since $this->out is an OutputPage this does not occur
436  // @phan-suppress-next-line PhanTypeMismatchPropertyByRef
437  LogEventsList::showLogExtract( $this->out, 'delete', $this->title );
438  }
439 
448  private function prepareMessage( $message ) {
449  global $wgLang;
450  if ( $this->oldimage ) {
451  # Message keys used:
452  # 'filedelete-intro-old', 'filedelete-nofile-old', 'filedelete-success-old'
453  return wfMessage(
454  "{$message}-old",
455  wfEscapeWikiText( $this->title->getText() ),
456  $wgLang->date( $this->getTimestamp(), true ),
457  $wgLang->time( $this->getTimestamp(), true ),
458  wfExpandUrl( $this->file->getArchiveUrl( $this->oldimage ), PROTO_CURRENT ) )->parseAsBlock();
459  } else {
460  return wfMessage(
461  $message,
462  wfEscapeWikiText( $this->title->getText() )
463  )->parseAsBlock();
464  }
465  }
466 
470  private function setHeaders() {
471  $this->out->setPageTitle( wfMessage( 'filedelete', $this->title->getText() ) );
472  $this->out->setRobotPolicy( 'noindex,nofollow' );
473  $this->out->addBacklinkSubtitle( $this->title );
474  }
475 
482  public static function isValidOldSpec( $oldimage ) {
483  return strlen( $oldimage ) >= 16
484  && strpos( $oldimage, '/' ) === false
485  && strpos( $oldimage, '\\' ) === false;
486  }
487 
498  public static function haveDeletableFile( &$file, &$oldfile, $oldimage ) {
499  return $oldimage
500  ? $oldfile && $oldfile->exists() && $oldfile->isLocal()
501  : $file && $file->exists() && $file->isLocal();
502  }
503 
509  private function getAction() {
510  $q = [];
511  $q['action'] = 'delete';
512 
513  if ( $this->oldimage ) {
514  $q['oldimage'] = $this->oldimage;
515  }
516 
517  return $this->title->getLocalURL( $q );
518  }
519 
525  private function getTimestamp() {
526  return $this->oldfile->getTimestamp();
527  }
528 }
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:2030
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:588
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:165
$wgUploadMaintenance
$wgUploadMaintenance
To disable file delete/restore temporarily.
Definition: DefaultSettings.php:9094
Title\getPrefixedText
getPrefixedText()
Get the prefixed title with spaces.
Definition: Title.php:1852
wfReadOnly
wfReadOnly()
Check whether the wiki is in read-only mode.
Definition: GlobalFunctions.php:1126
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1220
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:653
FileDeleteForm\getTimestamp
getTimestamp()
Extract the timestamp of the old version.
Definition: FileDeleteForm.php:525
$wgLang
$wgLang
Definition: Setup.php:778
FileDeleteForm\getAction
getAction()
Prepare the form action.
Definition: FileDeleteForm.php:509
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2448
FileDeleteForm\$user
User $user
Definition: FileDeleteForm.php:40
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:618
DB_MASTER
const DB_MASTER
Definition: defines.php:26
OutputPage
This is one of the Core classes and should be read at least once by any new developers.
Definition: OutputPage.php:47
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:805
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:448
FileDeleteForm\showForm
showForm()
Show the confirmation form.
Definition: FileDeleteForm.php:266
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1494
FileDeleteForm\$out
OutputPage $out
Definition: FileDeleteForm.php:49
WatchAction\doWatchOrUnwatch
static doWatchOrUnwatch( $watch, Title $title, User $user, string $expiry=null)
Watch or unwatch a page.
Definition: WatchAction.php:241
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:41
FileDeleteForm\$oldfile
LocalFile $oldfile
Definition: FileDeleteForm.php:43
FileDeleteForm\$oldimage
string $oldimage
Definition: FileDeleteForm.php:46
LocalFile\deleteOldFile
deleteOldFile( $archiveName, $reason, User $user, $suppress=false)
Delete an old version of the file.
Definition: LocalFile.php:2093
FileDeleteForm\execute
execute()
Fulfil the request; shows the form or deletes the file, pending authentication, confirmation,...
Definition: FileDeleteForm.php:67
FileDeleteForm\doDelete
static doDelete(&$title, &$file, &$oldimage, $reason, $suppress, User $user, $tags=[])
Really delete the file.
Definition: FileDeleteForm.php:174
FileDeleteForm\showLogEntries
showLogEntries()
Show deletion log fragments pertaining to the current file.
Definition: FileDeleteForm.php:430
File\isLocal
isLocal()
Returns true if the file comes from the local file repository.
Definition: File.php:1927
FileDeleteForm\setHeaders
setHeaders()
Set headers, titles and other bits.
Definition: FileDeleteForm.php:470
FileDeleteForm\__construct
__construct( $file, User $user, OutputPage $out)
Definition: FileDeleteForm.php:56
FileDeleteForm\$title
Title $title
Definition: FileDeleteForm.php:34
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:42
$wgRequest
if(! $wgDBerrorLogTZ) $wgRequest
Definition: Setup.php:646
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:37
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:498
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:482