MediaWiki  master
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 
55  private $readOnlyMode;
56 
58  private $repoGroup;
59 
62 
64  private $linkRenderer;
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(),
184  $this->title
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,
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 }
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
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
FileDeleteForm\$repoGroup
RepoGroup $repoGroup
Definition: FileDeleteForm.php:58
Xml\listDropDownOptionsOoui
static listDropDownOptionsOoui( $options)
Convert options for a drop-down box into a format accepted by OOUI\DropdownInputWidget etc.
Definition: Xml.php:595
FileDeleteForm\$readOnlyMode
ReadOnlyMode $readOnlyMode
Definition: FileDeleteForm.php:55
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:193
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:37
MediaWiki\Linker\LinkRenderer
Class that generates HTML links for pages.
Definition: LinkRenderer.php:43
ReadOnlyMode
A service class for fetching the wiki's current read-only mode.
Definition: ReadOnlyMode.php:11
Title\getPrefixedText
getPrefixedText()
Get the prefixed title with spaces.
Definition: Title.php:1912
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1182
PermissionsError
Show an error when a user tries to do something they do not have the necessary permissions for.
Definition: PermissionsError.php:32
Title\newMainPage
static newMainPage(MessageLocalizer $localizer=null)
Create a new Title for the Main Page.
Definition: Title.php:713
FileDeleteForm\getTimestamp
getTimestamp()
Extract the timestamp of the old version.
Definition: FileDeleteForm.php:568
FileDeleteForm\__construct
__construct(LocalFile $file, IContextSource $context, ReadOnlyMode $readOnlyMode, RepoGroup $repoGroup, WatchlistManager $watchlistManager, LinkRenderer $linkRenderer, UserOptionsLookup $userOptionsLookup)
Definition: FileDeleteForm.php:78
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:39
Status
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:44
FileDeleteForm\getAction
getAction()
Prepare the form action.
Definition: FileDeleteForm.php:552
MediaWiki\Watchlist\WatchlistManager
WatchlistManager service.
Definition: WatchlistManager.php:52
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2225
LogPage
Class to simplify the use of log pages.
Definition: LogPage.php:38
FileDeleteForm
File deletion user interface.
Definition: FileDeleteForm.php:37
FileDeleteForm\$oldfile
LocalFile null $oldfile
Definition: FileDeleteForm.php:46
LogEventsList\showLogExtract
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
Definition: LogEventsList.php:597
LocalFile
Class to represent a local file in the wiki's own database.
Definition: LocalFile.php:63
FileDeleteForm\$userOptionsLookup
UserOptionsLookup $userOptionsLookup
Definition: FileDeleteForm.php:67
PROTO_CURRENT
const PROTO_CURRENT
Definition: Defines.php:195
Html\hidden
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:831
FileDeleteForm\prepareMessage
prepareMessage(string $message)
Prepare a message referring to the file being deleted, showing an appropriate message depending upon ...
Definition: FileDeleteForm.php:490
FileDeleteForm\$watchlistManager
WatchlistManager $watchlistManager
Definition: FileDeleteForm.php:61
DB_PRIMARY
const DB_PRIMARY
Definition: defines.php:27
Hooks\runner
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:173
FileDeleteForm\showForm
showForm()
Show the confirmation form.
Definition: FileDeleteForm.php:302
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1455
MediaWiki\User\UserOptionsLookup
Provides access to user options.
Definition: UserOptionsLookup.php:29
FileDeleteForm\$linkRenderer
LinkRenderer $linkRenderer
Definition: FileDeleteForm.php:64
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:58
MediaWiki\Permissions\PermissionStatus
A StatusValue for permission errors.
Definition: PermissionStatus.php:35
LocalFile\deleteFile
deleteFile( $reason, UserIdentity $user, $suppress=false)
Delete all versions of the file.
Definition: LocalFile.php:2386
CommentStore\COMMENT_CHARACTER_LIMIT
const COMMENT_CHARACTER_LIMIT
Maximum length of a comment in UTF-8 characters.
Definition: CommentStore.php:48
FileDeleteForm\$context
IContextSource $context
Definition: FileDeleteForm.php:52
LocalFile\deleteOldFile
deleteOldFile( $archiveName, $reason, UserIdentity $user, $suppress=false)
Delete an old version of the file.
Definition: LocalFile.php:2449
File\getTitle
getTitle()
Return the associated title object.
Definition: File.php:360
Title
Represents a title within MediaWiki.
Definition: Title.php:48
FileDeleteForm\$oldimage
string $oldimage
Definition: FileDeleteForm.php:49
FileDeleteForm\execute
execute()
Fulfil the request; shows the form or deletes the file, pending authentication, confirmation,...
Definition: FileDeleteForm.php:101
FileDeleteForm\showLogEntries
showLogEntries()
Show deletion log fragments pertaining to the current file.
Definition: FileDeleteForm.php:474
File\isLocal
isLocal()
Returns true if the file comes from the local file repository.
Definition: File.php:2014
FileDeleteForm\setHeaders
setHeaders()
Set headers, titles and other bits.
Definition: FileDeleteForm.php:513
RepoGroup
Prioritized list of file repositories.
Definition: RepoGroup.php:32
FileDeleteForm\$title
Title $title
Definition: FileDeleteForm.php:40
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:44
FileDeleteForm\doDelete
static doDelete(&$title, &$file, &$oldimage, $reason, $suppress, UserIdentity $user, $tags=[])
Really delete the file.
Definition: FileDeleteForm.php:207
ErrorPageError
An error page which can definitely be safely rendered using the OutputPage.
Definition: ErrorPageError.php:30
FileDeleteForm\$file
LocalFile $file
Definition: FileDeleteForm.php:43
LocalFile\exists
exists()
canRender inherited
Definition: LocalFile.php:1380
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:541
wfExpandUrl
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
Definition: GlobalFunctions.php:474
Xml\listDropDownOptions
static listDropDownOptions( $list, $params=[])
Build options for a drop-down box from a textual list.
Definition: Xml.php:545
FileDeleteForm\isValidOldSpec
static isValidOldSpec( $oldimage)
Is the provided oldimage value valid?
Definition: FileDeleteForm.php:525