From 91e4d80448dacc223d06a55a7fd7bf771c1af4e8 Mon Sep 17 00:00:00 2001 From: Bryan Tong Minh Date: Sun, 18 Oct 2009 19:41:01 +0000 Subject: [PATCH] Rewrote Special:Upload to allow easier extension. Mostly backwards compatible towards the end user: tested with Commons' upload scripts. * Special:Upload now uses HTMLForm for form generation * Upload errors that can be solved by changing the filename now do not require reuploading. --- RELEASE-NOTES | 4 + includes/AutoLoader.php | 1 + includes/Licenses.php | 82 +- includes/Setup.php | 4 +- includes/SpecialPage.php | 2 +- includes/api/ApiUpload.php | 2 +- includes/specials/SpecialUpload.php | 1367 ++++++++++++--------------- includes/upload/UploadBase.php | 133 ++- includes/upload/UploadFromStash.php | 11 + languages/messages/MessagesEn.php | 7 +- skins/common/upload.js | 83 +- skins/common/wikibits.js | 30 - 12 files changed, 828 insertions(+), 898 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 58bfc33ad9..e58ff0b271 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -249,6 +249,10 @@ it from source control: http://www.mediawiki.org/wiki/Download_from_SVN limit exceeded, __NOINDEX__ tracking, etc) can now be disabled by setting the system message ([[MediaWiki:expensive-parserfunction-category]] etc) to "-". * Added maintenance script sqlite.php for SQLite-specific maintenance tasks. +* Rewrote Special:Upload to allow easier extension. +* Upload errors that can be solved by changing the filename now do not require + reuploading. + === Bug fixes in 1.16 === diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 8ca1a4d696..b24163dd59 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -550,6 +550,7 @@ $wgAutoloadLocalClasses = array( 'SpecialSearch' => 'includes/specials/SpecialSearch.php', 'SpecialStatistics' => 'includes/specials/SpecialStatistics.php', 'SpecialTags' => 'includes/specials/SpecialTags.php', + 'SpecialUpload' => 'includes/specials/SpecialUpload.php', 'SpecialVersion' => 'includes/specials/SpecialVersion.php', 'SpecialWhatlinkshere' => 'includes/specials/SpecialWhatlinkshere.php', 'UncategorizedCategoriesPage' => 'includes/specials/SpecialUncategorizedcategories.php', diff --git a/includes/Licenses.php b/includes/Licenses.php index 6398c887d3..4b81bfaead 100644 --- a/includes/Licenses.php +++ b/includes/Licenses.php @@ -9,46 +9,39 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later */ -class Licenses { - /**#@+ - * @private - */ +class Licenses extends HTMLFormField { /** * @var string */ - var $msg; + protected $msg; /** * @var array */ - var $licenses = array(); + protected $licenses = array(); /** * @var string */ - var $html; + protected $html; /**#@-*/ /** * Constructor - * - * @param $str String: the string to build the licenses member from, will use - * wfMsgForContent( 'licenses' ) if null (default: null) */ - function __construct( $str = null ) { - // PHP sucks, this should be possible in the constructor - $this->msg = is_null( $str ) ? wfMsgForContent( 'licenses' ) : $str; - $this->html = ''; + public function __construct( $params ) { + parent::__construct( $params ); + + $this->msg = empty( $params['licenses'] ) ? wfMsgForContent( 'licenses' ) : $params['licenses']; + $this->selected = null; $this->makeLicenses(); - $tmp = $this->getLicenses(); - $this->makeHtml( $tmp ); } /**#@+ * @private */ - function makeLicenses() { + protected function makeLicenses() { $levels = array(); $lines = explode( "\n", $this->msg ); @@ -75,18 +68,17 @@ class Licenses { } } - function trimStars( $str ) { + protected static function trimStars( $str ) { $i = $count = 0; - wfSuppressWarnings(); - while ($str[$i++] == '*') - ++$count; - wfRestoreWarnings(); - - return array( $count, ltrim( $str, '* ' ) ); + $length = strlen( $str ); + for ( $i = 0; $i < $length; $i++ ) { + if ( $str[$i] != '*' ) + return array( $i, ltrim( $str, '* ' ) ); + } } - function stackItem( &$list, $path, $item ) { + protected function stackItem( &$list, $path, $item ) { $position =& $list; if ( $path ) foreach( $path as $key ) @@ -94,13 +86,12 @@ class Licenses { $position[] = $item; } - function makeHtml( &$tagset, $depth = 0 ) { + protected function makeHtml( &$tagset, $depth = 0 ) { foreach ( $tagset as $key => $val ) if ( is_array( $val ) ) { $this->html .= $this->outputOption( - $this->msg( $key ), + $this->msg( $key ), '', array( - 'value' => '', 'disabled' => 'disabled', 'style' => 'color: GrayText', // for MSIE ), @@ -109,22 +100,22 @@ class Licenses { $this->makeHtml( $val, $depth + 1 ); } else { $this->html .= $this->outputOption( - $this->msg( $val->text ), - array( - 'value' => $val->template, - 'title' => '{{' . $val->template . '}}' - ), + $this->msg( $val->text ), $val->template, + array( 'title' => '{{' . $val->template . '}}' ), $depth ); } } - function outputOption( $val, $attribs = null, $depth ) { - $val = str_repeat( /*   */ "\xc2\xa0", $depth * 2 ) . $val; + protected function outputOption( $text, $value, $attribs = null, $depth = 0 ) { + $attribs['value'] = $value; + if ( $value === $this->selected ) + $attribs['selected'] = 'selected'; + $val = str_repeat( /*   */ "\xc2\xa0", $depth * 2 ) . $text; return str_repeat( "\t", $depth ) . Xml::element( 'option', $attribs, $val ) . "\n"; } - function msg( $str ) { + protected function msg( $str ) { $out = wfMsg( $str ); return wfEmptyMsg( $str, $out ) ? $str : $out; } @@ -136,14 +127,29 @@ class Licenses { * * @return array */ - function getLicenses() { return $this->licenses; } + public function getLicenses() { return $this->licenses; } /** * Accessor for $this->html * * @return string */ - function getHtml() { return $this->html; } + public function getInputHTML( $value ) { + $this->selected = $value; + + $this->html = $this->outputOption( wfMsg( 'nolicense' ), '', + (bool)$this->selected ? null : array( 'selected' => 'selected' ) ); + $this->makeHtml( $this->getLicenses() ); + + $attribs = array( + 'name' => $this->mName, + 'id' => $this->mID + ); + if ( !empty( $this->mParams['disabled'] ) ) + $attibs['disabled'] = 'disabled'; + + return Html::rawElement( 'select', $attribs, $this->html ); + } } /** diff --git a/includes/Setup.php b/includes/Setup.php index 2f16ed4b90..04b9b473c6 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -306,9 +306,9 @@ $wgDeferredUpdateList = array(); $wgPostCommitUpdateList = array(); if ( $wgAjaxWatch ) $wgAjaxExportList[] = 'wfAjaxWatch'; -if ( $wgAjaxUploadDestCheck ) $wgAjaxExportList[] = 'UploadForm::ajaxGetExistsWarning'; +if ( $wgAjaxUploadDestCheck ) $wgAjaxExportList[] = 'SpecialUpload::ajaxGetExistsWarning'; if( $wgAjaxLicensePreview ) - $wgAjaxExportList[] = 'UploadForm::ajaxGetLicensePreview'; + $wgAjaxExportList[] = 'SpecialUpload::ajaxGetLicensePreview'; # Placeholders in case of DB error $wgTitle = null; diff --git a/includes/SpecialPage.php b/includes/SpecialPage.php index dbf097482b..47ba99383d 100644 --- a/includes/SpecialPage.php +++ b/includes/SpecialPage.php @@ -141,7 +141,7 @@ class SpecialPage { 'Filepath' => array( 'SpecialPage', 'Filepath' ), 'MIMEsearch' => array( 'SpecialPage', 'MIMEsearch' ), 'FileDuplicateSearch' => array( 'SpecialPage', 'FileDuplicateSearch' ), - 'Upload' => 'UploadForm', + 'Upload' => 'SpecialUpload', # Wiki data and tools 'Statistics' => 'SpecialStatistics', diff --git a/includes/api/ApiUpload.php b/includes/api/ApiUpload.php index d37e54f371..c421e47c00 100644 --- a/includes/api/ApiUpload.php +++ b/includes/api/ApiUpload.php @@ -230,7 +230,7 @@ class ApiUpload extends ApiBase { $this->dieUsage( 'This file did not pass file verification', 'verification-error', 0, array( 'details' => $verification['details'] ) ); break; - case UploadBase::UPLOAD_VERIFICATION_ERROR: + case UploadBase::HOOK_ABORTED: $this->dieUsage( "The modification you tried to make was aborted by an extension hook", 'hookaborted', 0, array( 'error' => $verification['error'] ) ); break; diff --git a/includes/specials/SpecialUpload.php b/includes/specials/SpecialUpload.php index 9025fc2284..713a6ff559 100644 --- a/includes/specials/SpecialUpload.php +++ b/includes/specials/SpecialUpload.php @@ -2,82 +2,80 @@ /** * @file * @ingroup SpecialPage + * @ingroup Upload + * + * Form for handling uploads and special page. + * */ -/** - * implements Special:Upload - * @ingroup SpecialPage - */ -class UploadForm extends SpecialPage { - /**#@+ - * @access private - */ - var $mComment, $mLicense, $mIgnoreWarning; - var $mCopyrightStatus, $mCopyrightSource, $mReUpload, $mAction, $mUploadClicked; - var $mDestWarningAck; - var $mLocalFile; - - var $mUpload; // Instance of UploadBase or derivative - - # Placeholders for text injection by hooks (must be HTML) - # extensions should take care to _append_ to the present value - var $uploadFormTextTop; - var $uploadFormTextAfterSummary; - var $mTokenOk = false; - var $mForReUpload = false; - /**#@-*/ - +class SpecialUpload extends SpecialPage { /** * Constructor : initialise object * Get data POSTed through the form and assign them to the object - * @param $request Data posted. + * @param WebRequest $request Data posted. */ - function __construct( $request = null ) { + public function __construct( $request = null ) { + global $wgRequest; + parent::__construct( 'Upload', 'upload' ); - $this->mRequest = $request; + + $this->loadRequest( is_null( $request ) ? $wgRequest : $request ); } + + /** Misc variables **/ + protected $mRequest; // The WebRequest or FauxRequest this form is supposed to handle + protected $mSourceType; + protected $mUpload; + protected $mLocalFile; + protected $mUploadClicked; + + /** User input variables from the "description" section **/ + protected $mDesiredDestName; // The requested target file name + protected $mComment; + protected $mLicense; + + /** User input variables from the root section **/ + protected $mIgnoreWarning; + protected $mWatchThis; + protected $mCopyrightStatus; + protected $mCopyrightSource; + + /** Hidden variables **/ + protected $mForReUpload; // The user followed an "overwrite this file" link + protected $mCancelUpload; // The user clicked "Cancel and return to upload form" button + protected $mTokenOk; + + /** + * Initialize instance variables from request and create an Upload handler + * + * @param WebRequest $request The request to extract variables from + */ + protected function loadRequest( $request ) { + global $wgUser; - protected function initForm() { - global $wgRequest, $wgUser; - - if ( is_null( $this->mRequest ) ) { - $request = $wgRequest; - } else { - $request = $this->mRequest; - } + $this->mRequest = $request; + $this->mSourceType = $request->getVal( 'wpSourceType', 'file' ); + $this->mUpload = UploadBase::createFromRequest( $request ); + $this->mUploadClicked = $request->getCheck( 'wpUpload' ) && $request->wasPosted(); + // Guess the desired name from the filename if not provided $this->mDesiredDestName = $request->getText( 'wpDestFile' ); if( !$this->mDesiredDestName ) $this->mDesiredDestName = $request->getText( 'wpUploadFile' ); - - $this->mForReUpload = $request->getBool( 'wpForReUpload' ); // updating a file - $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' ); $this->mComment = $request->getText( 'wpUploadDescription' ); - - if( !$request->wasPosted() ) { - # GET requests just give the main form; no data except destination - # filename and description - return; - } - - # Placeholders for text injection by hooks (empty per default) - $this->uploadFormTextTop = ""; - $this->uploadFormTextAfterSummary = ""; - - $this->mUploadClicked = $request->getCheck( 'wpUpload' ); - - $this->mLicense = $request->getText( 'wpLicense' ); + $this->mLicense = $request->getText( 'wpLicense' ); + + + $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' ); + $this->mWatchthis = $request->getBool( 'wpWatchthis' ); $this->mCopyrightStatus = $request->getText( 'wpUploadCopyStatus' ); $this->mCopyrightSource = $request->getText( 'wpUploadSource' ); - $this->mWatchthis = $request->getBool( 'wpWatchthis' ); - $this->mSourceType = $request->getVal( 'wpSourceType', 'file' ); - $this->mDestWarningAck = $request->getText( 'wpDestFileWarningAck' ); - $this->mReUpload = $request->getCheck( 'wpReUpload' ); // retrying upload - $this->mAction = $request->getVal( 'action' ); - $this->mUpload = UploadBase::createFromRequest( $request ); - + $this->mForReUpload = $request->getBool( 'wpForReUpload' ); // updating a file + $this->mCancelUpload = $request->getCheck( 'wpCancelUpload' ) + || $request->getCheck( 'wpReUpload' ); // b/w compat + // If it was posted check for the token (no remote POST'ing with user credentials) $token = $request->getVal( 'wpEditToken' ); if( $this->mSourceType == 'file' && $token == null ) { @@ -89,23 +87,28 @@ class UploadForm extends SpecialPage { $this->mTokenOk = $wgUser->matchEditToken( $token ); } } - + + /** + * This page can be shown if uploading is enabled. + * Handle permission checking elsewhere in order to be able to show + * custom error messages. + * + * @param User $user + * @return bool + */ public function userCanExecute( $user ) { return UploadBase::isEnabled() && parent::userCanExecute( $user ); } - + /** - * Start doing stuff - * @access public + * Special page entry point */ - function execute( $par ) { + public function execute() { global $wgUser, $wgOut, $wgRequest; - + $this->setHeaders(); $this->outputHeader(); - - $this->initForm(); - + # Check uploading enabled if( !UploadBase::isEnabled() ) { $wgOut->showErrorPage( 'uploaddisabled', 'uploaddisabledtext' ); @@ -131,91 +134,302 @@ class UploadForm extends SpecialPage { return; } + # Check whether we actually want to allow changing stuff if( wfReadOnly() ) { $wgOut->readOnlyPage(); return; } - //check token if uploading or reUploading - if( !$this->mTokenOk && !$this->mReUpload && ($this->mUpload && ( - 'submit' == $this->mAction || $this->mUploadClicked ) ) ) - { - $this->mainUploadForm ( wfMsgExt( 'session_fail_preview', 'parseinline' ) ); - return ; - } - - - if( $this->mReUpload && $this->mUpload) { - // User choose to cancel upload - if( !$this->mUpload->unsaveUploadedFile() ) { + + # Unsave the temporary file in case this was a cancelled upload + if ( $this->mCancelUpload ) { + if ( !$this->unsaveUploadedFile() ) + # Something went wrong, so unsaveUploadedFile showed a warning return; - } - # Because it is probably checked and shouldn't be - $this->mIgnoreWarning = false; - $this->mainUploadForm(); - } elseif( $this->mUpload && ( - 'submit' == $this->mAction || - $this->mUploadClicked - ) ) { + } + + # Process upload or show a form + if ( $this->mTokenOk && !$this->mCancelUpload + && ( $this->mUpload && $this->mUploadClicked ) ) { $this->processUpload(); } else { - $this->mainUploadForm(); + $this->showUploadForm( $this->getUploadForm() ); } - - if( $this->mUpload ) + + # Cleanup + if ( $this->mUpload ) $this->mUpload->cleanupTempFile(); } + + /** + * Show the main upload form and optionally add the session key to the + * output. This hides the source selection. + * + * @param string $message HTML message to be shown at top of form + * @param string $sessionKey Session key of the stashed upload + */ + protected function showUploadForm( $form ) { + # Add links if file was previously deleted + if ( !$this->mDesiredDestName ) + $this->showViewDeletedLinks(); + + $form->show(); + } + + /** + * Get an UploadForm instance with title and text properly set. + * + * @param string $message HTML string to add to the form + * @param string $sessionKey Session key in case this is a stashed upload + * @return UploadForm + */ + protected function getUploadForm( $message = '', $sessionKey = '' ) { + # Initialize form + $form = new UploadForm( $this->watchCheck(), $this->mForReUpload, $sessionKey ); + $form->setTitle( $this->getTitle() ); + + # Check the token, but only if necessary + if( !$this->mTokenOk && !$this->mCancelUpload + && ( $this->mUpload && $this->mUploadClicked ) ) + $form->addPreText( wfMsgExt( 'session_fail_preview', 'parseinline' ) ); + + # Add text to form + $form->addPreText( '
' . wfMsgExt( 'uploadtext', 'parse' ) . '
'); + # Add upload error message + $form->addPreText( $message ); + + return $form; + } + + /** + * TODO: DOCUMENT + */ + protected function showViewDeletedLinks() { + global $wgOut, $wgUser; + + $title = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName ); + // Show a subtitle link to deleted revisions (to sysops et al only) + if( $title instanceof Title && ( $count = $title->isDeleted() ) > 0 && $wgUser->isAllowed( 'deletedhistory' ) ) { + $link = wfMsgExt( + $wgUser->isAllowed( 'delete' ) ? 'thisisdeleted' : 'viewdeleted', + array( 'parse', 'replaceafter' ), + $wgUser->getSkin()->linkKnown( + SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedText() ), + wfMsgExt( 'restorelink', array( 'parsemag', 'escape' ), $count ) + ) + ); + $wgOut->addHTML( "
{$link}
" ); + } + // Show the relevant lines from deletion log (for still deleted files only) + if( $title instanceof Title && $title->isDeletedQuick() && !$title->exists() ) { + $this->showDeletionLog( $wgOut, $title->getPrefixedText() ); + } + } + /** - * Do the upload + * Stashes the upload and shows the main upload form. + * + * Note: only errors that can be handled by changing the name or + * description should be redirected here. It should be assumed that the + * file itself is sane and has passed UploadBase::verifyFile. This + * essentially means that UploadBase::VERIFICATION_ERROR and + * UploadBase::EMPTY_FILE should not be passed here. + * + * @param string $message HTML message to be passed to mainUploadForm + */ + protected function recoverableUploadError( $message ) { + $sessionKey = $this->mUpload->stashSession(); + $message = '

' . wfMsgHtml( 'uploadwarning' ) . "

\n" . + '
' . $message . "
\n"; + $this->showUploadForm( $this->getUploadForm( $message, $sessionKey ) ); + } + /** + * Stashes the upload, shows the main form, but adds an "continue anyway button" + * + * @param array $warnings + */ + protected function uploadWarning( $warnings ) { + global $wgUser; + + $sessionKey = $this->mUpload->stashSession(); + + $sk = $wgUser->getSkin(); + + $warningHtml = '

' . wfMsgHtml( 'uploadwarning' ) . "

\n" + . '