* (bug 10937) Distinguish overwritten files in upload log
* Introduce 'ArticleUpdateBeforeRedirect' hook; see docs/hooks.txt for more
information
+* Confirmation is now required when deleting old versions of files
+* Users can now enter comments when deleting old versions of files
== Bugfixes since 1.10 ==
'ImagePage' => 'includes/ImagePage.php',
'ImageHistoryList' => 'includes/ImagePage.php',
'ImageRemote' => 'includes/ImageRemote.php',
+ 'FileDeleteForm' => 'includes/FileDeleteForm.php',
'FileRevertForm' => 'includes/FileRevertForm.php',
'Job' => 'includes/JobQueue.php',
'EmaillingJob' => 'includes/EmaillingJob.php',
require( $file );
}
}
-}
-
-
-
+}
\ No newline at end of file
--- /dev/null
+<?php
+
+/**
+ * File deletion user interface
+ *
+ * @addtogroup Media
+ * @author Rob Church <robchur@gmail.com>
+ */
+class FileDeleteForm {
+
+ private $title = null;
+ private $file = null;
+ private $oldimage = '';
+
+ /**
+ * Constructor
+ *
+ * @param File $file File we're deleting
+ */
+ public function __construct( $file ) {
+ $this->title = $file->getTitle();
+ $this->file = $file;
+ }
+
+ /**
+ * Fulfil the request; shows the form or deletes the file,
+ * pending authentication, confirmation, etc.
+ */
+ public function execute() {
+ global $wgOut, $wgRequest, $wgUser, $wgLang, $wgServer;
+ $this->setHeaders();
+
+ if( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ } elseif( !$wgUser->isLoggedIn() ) {
+ $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' );
+ return;
+ } elseif( !$wgUser->isAllowed( 'delete' ) ) {
+ $wgOut->permissionError( 'delete' );
+ return;
+ } elseif( $wgUser->isBlocked() ) {
+ $wgOut->blockedPage();
+ return;
+ }
+
+ $this->oldimage = $wgRequest->getText( 'oldimage', false );
+ $token = $wgRequest->getText( 'wpEditToken' );
+ if( $this->oldimage && !$this->isValidOldSpec() ) {
+ $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars( $this->oldimage ) );
+ return;
+ }
+
+ if( !$this->haveDeletableFile() ) {
+ $wgOut->addHtml( $this->prepareMessage( 'filedelete-nofile' ) );
+ $wgOut->addReturnTo( $this->title );
+ return;
+ }
+
+ // Don't allow accidental deletion of a single file revision
+ // if this is, in fact, the current revision; things might break
+ if( $this->oldimage && $this->file->getTimestamp() == $this->getTimestamp() ) {
+ $wgOut->addHtml( wfMsgExt( 'filedelete-iscurrent', 'parse' ) );
+ $wgOut->addReturnTo( $this->title );
+ return;
+ }
+
+ // Perform the deletion if appropriate
+ if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $token, $this->oldimage ) ) {
+ $comment = $wgRequest->getText( 'wpComment' );
+ if( $this->oldimage ) {
+ $status = $this->file->deleteOld( $this->oldimage, $comment );
+ if( $status->ok ) {
+ // Need to do a log item
+ $log = new LogPage( 'delete' );
+ $log->addEntry( 'delete', $this->title, wfMsg( 'deletedrevision' , $this->oldimage ) );
+ }
+ } else {
+ $status = $this->file->delete( $comment );
+ if( $status->ok ) {
+ // Need to delete the associated article
+ $article = new Article( $this->title );
+ $article->doDeleteArticle( $comment );
+ }
+ }
+ if( !$status->isGood() )
+ $wgOut->addWikiText( $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) );
+ if( $status->ok ) {
+ $wgOut->addHtml( $this->prepareMessage( 'filedelete-success' ) );
+ // Return to the main page if we just deleted all versions of the
+ // file, otherwise go back to the description page
+ $wgOut->addReturnTo( $this->oldimage ? $this->title : Title::newMainPage() );
+ }
+ return;
+ }
+
+ // Show the form
+ $this->showForm();
+ }
+
+ /**
+ * Show the confirmation form
+ */
+ private function showForm() {
+ global $wgOut, $wgUser, $wgRequest, $wgLang, $wgContLang, $wgServer;
+
+ $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getAction() ) );
+ $form .= Xml::hidden( 'wpEditToken', $wgUser->editToken( $this->oldimage ) );
+ $form .= '<fieldset><legend>' . wfMsgHtml( 'filedelete-legend' ) . '</legend>';
+ $form .= $this->prepareMessage( 'filedelete-intro' );
+
+ $form .= '<p>' . Xml::inputLabel( wfMsg( 'filedelete-comment' ), 'wpComment', 'wpComment', 60 ) . '</p>';
+ $form .= '<p>' . Xml::submitButton( wfMsg( 'filedelete-submit' ) ) . '</p>';
+ $form .= '</fieldset>';
+ $form .= '</form>';
+
+ $wgOut->addHtml( $form );
+ }
+
+ /**
+ * Prepare a message referring to the file being deleted,
+ * showing an appropriate message depending upon whether
+ * it's a current file or an old version
+ *
+ * @param string $message Message base
+ * @return string
+ */
+ private function prepareMessage( $message ) {
+ global $wgLang, $wgServer;
+ if( $this->oldimage ) {
+ return wfMsgExt(
+ "{$message}-old",
+ 'parse',
+ $this->title->getText(),
+ $wgLang->date( $this->getTimestamp() ),
+ $wgLang->time( $this->getTimestamp() ),
+ $wgServer . $this->file->getArchiveUrl( $this->oldimage )
+ );
+ } else {
+ return wfMsgExt(
+ $message,
+ 'parse',
+ $this->title->getText()
+ );
+ }
+ }
+
+ /**
+ * Set headers, titles and other bits
+ */
+ private function setHeaders() {
+ global $wgOut;
+ $wgOut->setPageTitle( wfMsg( 'filedelete', $this->title->getText() ) );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
+ }
+
+ /**
+ * Is the provided `oldimage` value valid?
+ *
+ * @return bool
+ */
+ private function isValidOldSpec() {
+ return strlen( $this->oldimage ) >= 16
+ && strpos( $this->oldimage, '/' ) === false
+ && strpos( $this->oldimage, '\\' ) === false;
+ }
+
+ /**
+ * Could we delete the file specified? If an `oldimage`
+ * value was provided, does it correspond to an
+ * existing, local, old version of this file?
+ *
+ * @return bool
+ */
+ private function haveDeletableFile() {
+ $file = wfFindFile( $this->title, $this->oldimage );
+ return $file && $file->exists() && $file->isLocal();
+ }
+
+ /**
+ * Prepare the form action
+ *
+ * @return string
+ */
+ private function getAction() {
+ $q = array();
+ $q[] = 'action=delete';
+ if( $this->oldimage )
+ $q[] = 'oldimage=' . urlencode( $this->oldimage );
+ return $this->title->getLocalUrl( implode( '&', $q ) );
+ }
+
+ /**
+ * Extract the timestamp of the old version
+ *
+ * @return string
+ */
+ private function getTimestamp() {
+ return substr( $this->oldimage, 0, 14 );
+ }
+
+}
\ No newline at end of file
$wgOut->addHTML( "</ul>\n" );
}
- function delete()
- {
- global $wgUser, $wgOut, $wgRequest;
-
- if ( !$this->img->exists() || !$this->img->isLocal() ) {
- # Use standard article deletion
- Article::delete();
- return;
- }
-
- $confirm = $wgRequest->wasPosted();
- $reason = $wgRequest->getVal( 'wpReason' );
- $image = $wgRequest->getVal( 'image' );
- $oldimage = $wgRequest->getVal( 'oldimage' );
-
- # Only sysops can delete images. Previously ordinary users could delete
- # old revisions, but this is no longer the case.
- if ( !$wgUser->isAllowed('delete') ) {
- $wgOut->permissionRequired( 'delete' );
- return;
- }
- if ( $wgUser->isBlocked() ) {
- $wgOut->blockedPage();
- return;
- }
- if ( wfReadOnly() ) {
- $wgOut->readOnlyPage();
- return;
- }
-
- # Better double-check that it hasn't been deleted yet!
- $wgOut->setPagetitle( wfMsg( 'confirmdelete' ) );
- if ( ( !is_null( $image ) )
- && ( '' == trim( $image ) ) ) {
- $wgOut->showFatalError( wfMsg( 'cannotdelete' ) );
- return;
- }
-
- # Deleting old images doesn't require confirmation
- if ( !is_null( $oldimage ) || $confirm ) {
- if( $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ), $oldimage ) ) {
- $this->doDeleteImage( $reason );
- } else {
- $wgOut->showFatalError( wfMsg( 'sessionfailure' ) );
- }
- return;
- }
-
- if ( !is_null( $image ) ) {
- $q = '&image=' . urlencode( $image );
- } else if ( !is_null( $oldimage ) ) {
- $q = '&oldimage=' . urlencode( $oldimage );
- } else {
- $q = '';
- }
- return $this->confirmDelete( $q, $wgRequest->getText( 'wpReason' ) );
- }
-
- /*
- * Delete an image.
- * Called doDeleteImage() not doDelete() so that Article::delete() doesn't
- * call back to here.
- *
- * @param $reason User provided reason for deletion.
- */
- function doDeleteImage( $reason ) {
- global $wgOut, $wgRequest;
-
- $oldimage = $wgRequest->getVal( 'oldimage' );
-
- if ( !is_null( $oldimage ) ) {
- if ( strlen( $oldimage ) < 16 ) {
- $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars($oldimage) );
- return;
- }
- if( strpos( $oldimage, '/' ) !== false || strpos( $oldimage, '\\' ) !== false ) {
- $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars($oldimage) );
- return;
- }
- $status = $this->doDeleteOldImage( $oldimage );
- $deleted = $oldimage;
- } else {
- $status = $this->img->delete( $reason );
- if ( !$status->isGood() ) {
- // Warning or error
- $wgOut->addWikiText( $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) );
- }
- if ( $status->ok ) {
- # Image itself is now gone, and database is cleaned.
- # Now we remove the image description page.
- $article = new Article( $this->mTitle );
- $article->doDeleteArticle( $reason ); # ignore errors
- $deleted = $this->img->getName();
- }
- }
-
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
-
- if ( !$status->ok ) {
- // Fatal error flagged
- $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
- $wgOut->returnToMain( false, $this->mTitle->getPrefixedText() );
- } else {
- // Operation completed
- $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
- $loglink = '[[Special:Log/delete|' . wfMsg( 'deletionlog' ) . ']]';
- $text = wfMsg( 'deletedtext', $deleted, $loglink );
- $wgOut->addWikiText( $text );
- $wgOut->returnToMain( false, $this->mTitle->getPrefixedText() );
- }
- }
-
/**
- * Delete an old revision of an image,
- * @return FileRepoStatus
+ * Delete the file, or an earlier version of it
*/
- function doDeleteOldImage( $oldimage ) {
- global $wgOut;
-
- $status = $this->img->deleteOld( $oldimage, '' );
- if( !$status->isGood() ) {
- $wgOut->addWikiText( $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) );
- }
- if ( $status->ok ) {
- # Log the deletion
- $log = new LogPage( 'delete' );
- $log->addEntry( 'delete', $this->mTitle, wfMsg('deletedrevision',$oldimage) );
+ public function delete() {
+ if( !$this->img->exists() || !$this->img->isLocal() ) {
+ // Standard article deletion
+ Article::delete();
+ return;
}
- return $status;
+ $deleter = new FileDeleteForm( $this->img );
+ $deleter->execute();
}
/**
* Revert the file to an earlier version
*/
- function revert() {
+ public function revert() {
$reverter = new FileRevertForm( $this->img );
$reverter->execute();
}
'filerevert-success' => "<span class=\"plainlinks\">'''[[Media:$1|$1]]''' has been reverted to the [$4 version as of $2, $3].</span>",
'filerevert-badversion' => 'There is no previous local version of this file with the provided timestamp.',
+# File deletion
+'filedelete' => 'Delete $1',
+'filedelete-legend' => 'Delete file',
+'filedelete-intro' => "You are deleting '''[[Media:$1|$1]]'''.",
+'filedelete-intro-old' => "<span class=\"plainlinks\">You are deleting the version of '''[[Media:$1|$1]]''' as of [$4 $2, $3].</span>",
+'filedelete-comment' => 'Comment:',
+'filedelete-submit' => 'Delete',
+'filedelete-success' => "'''$1''' has been deleted.",
+'filedelete-success-old' => "<span class=\"plainlinks\">The version of '''[[Media:$1|$1]]''' as of $2, $3 has been deleted.</span>",
+'filedelete-nofile' => "'''$1''' does not exist on this site.",
+'filedelete-nofile-old' => "There is no version of '''$1''' dated $2, $3.",
+'filedelete-iscurrent' => 'You are attempting to delete the most recent version of this file. Please revert to an older version first.',
+
# MIME search
'mimesearch' => 'MIME search',
'mimesearch-summary' => 'This page enables the filtering of files for its MIME-type. Input: contenttype/subtype, e.g. <tt>image/jpeg</tt>.',
'filerevert-success',
'filerevert-badversion',
),
+ 'filedelete' => array(
+ 'filedelete',
+ 'filedelete-legend',
+ 'filedelete-intro',
+ 'filedelete-intro-old',
+ 'filedelete-comment',
+ 'filedelete-submit',
+ 'filedelete-success',
+ 'filedelete-success-old',
+ 'filedelete-nofile',
+ 'filedelete-nofile-old',
+ 'filedelete-iscurrent',
+ ),
'mimesearch' => array(
'mimesearch',
'mimesearch-summary',
'licenses' => '',
'imagelist' => 'Image list',
'filerevert' => 'File reversion',
+ 'filedelete' => 'File deletion',
'mimesearch' => 'MIME search',
'unwatchedpages' => 'Unwatched pages',
'listredirects' => 'List redirects',
'enotif_subject',
'enotif_body',
'allmessagesnotsupportedDB',
-);
+);
\ No newline at end of file