Jack Phoenix has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/334788 )
Change subject: Publishing ShoutWiki's version (v. 3.1-SW) of MultiUpload for MW 1.28+ ...................................................................... Publishing ShoutWiki's version (v. 3.1-SW) of MultiUpload for MW 1.28+ * Old legacy cruft removed for good; if you need support for an EOL'd version of MediaWiki, use an older version of the extension * Extension registration is now required for loading this extension * Windows unzipping support in the API module, sorta * JavaScript fixes * Lots and lots of ugly copypasta Might work/probably works with MW 1.27, but that hasn't been tested. Still, there was merely one line I had to comment out to make this 1.28-compatible when we upgraded from 1.27 to 1.28, so I'm pretty confident that this'll work with 1.27, but as always, use at your own risk. Change-Id: I3390021444af2eafcb54dc1d1b8a399f8a2bb819 --- D MultiUpload.i18n.php D MultiUpload.php M MultiUploadApi.php M README M SpecialMultiUpload.php D SpecialUpload.php M extension.json M resources/ext.multiupload.shared.js M resources/ext.multiupload.unpack.js D resources/mediawiki.special.upload.js.patched.1.21.3 M resources/mw.FormDataTransport.js D resources/upload.js.patched.1.21.3 12 files changed, 427 insertions(+), 2,496 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/MultiUpload refs/changes/88/334788/1 diff --git a/MultiUpload.i18n.php b/MultiUpload.i18n.php deleted file mode 100644 index d252b1c..0000000 --- a/MultiUpload.i18n.php +++ /dev/null @@ -1,35 +0,0 @@ -<?php -/** - * This is a backwards-compatibility shim, generated by: - * https://git.wikimedia.org/blob/mediawiki%2Fcore.git/HEAD/maintenance%2FgenerateJsonI18n.php - * - * Beginning with MediaWiki 1.23, translation strings are stored in json files, - * and the EXTENSION.i18n.php file only exists to provide compatibility with - * older releases of MediaWiki. For more information about this migration, see: - * https://www.mediawiki.org/wiki/Requests_for_comment/Localisation_format - * - * This shim maintains compatibility back to MediaWiki 1.17. - */ -$messages = array(); -if ( !function_exists( 'wfJsonI18nShim97d1b12e3cd5e296' ) ) { - function wfJsonI18nShim97d1b12e3cd5e296( $cache, $code, &$cachedData ) { - $codeSequence = array_merge( array( $code ), $cachedData['fallbackSequence'] ); - foreach ( $codeSequence as $csCode ) { - $fileName = dirname( __FILE__ ) . "/i18n/$csCode.json"; - if ( is_readable( $fileName ) ) { - $data = FormatJson::decode( file_get_contents( $fileName ), true ); - foreach ( array_keys( $data ) as $key ) { - if ( $key === '' || $key[0] === '@' ) { - unset( $data[$key] ); - } - } - $cachedData['messages'] = array_merge( $data, $cachedData['messages'] ); - } - - $cachedData['deps'][] = new FileDependency( $fileName ); - } - return true; - } - - $GLOBALS['wgHooks']['LocalisationCacheRecache'][] = 'wfJsonI18nShim97d1b12e3cd5e296'; -} diff --git a/MultiUpload.php b/MultiUpload.php deleted file mode 100644 index 11560d2..0000000 --- a/MultiUpload.php +++ /dev/null @@ -1,165 +0,0 @@ -<?php -/** - * MultiUpload extension for MediaWiki 1.19 and later - * - * @file - * @ingroup Extensions - * @author Travis Derouin - * @author Lee Worden <worden....@gmail.com> - * @version 3.0 - * @date 19 August 2014 - * @copyright Copyright © Lee Worden <worden....@gmail.com> - * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - * http://www.gnu.org/copyleft/gpl.html - */ - -/** - * Protect against register_globals vulnerabilities. - * This line must be present before any global variable is referenced. - */ -if ( !defined( 'MEDIAWIKI' ) ) { - echo( "This is an extension to the MediaWiki package and cannot be run standalone.\n" ); - die( -1 ); -} - -# ===== Extension credits that will show up on Special:Version ===== -$wgExtensionCredits['specialpage'][] = array( - 'name' => 'MultiUpload', - 'version' => '3.0', - 'author' => array( 'Travis Derouin', 'Lee Worden' ), - 'url' => 'https://www.mediawiki.org/wiki/Extension:MultiUpload', - 'descriptionmsg' => 'multiupload-desc', -); - -# ===== Configuration variables ===== - -# start with 1 file selector shown -$wgMultiUploadInitialNumberOfImportRows = 1; - -# Can't have a huge number of files to upload on the single form - -# for instance, because of PHP restrictions on number -# and length of POST values -# $wgMultiUploadMaxImportFilesPerPage = 20; - -# where we unpack .tar and .zip files -$wgMultiUploadTempDir = '/tmp'; - -# ===== Register the special page ===== -$wgSpecialPages['MultiUpload'] = 'SpecialMultiUpload'; -$wgAutoloadClasses['SpecialMultiUpload'] = __DIR__ . '/SpecialMultiUpload.php'; -$wgAutoloadClasses['MultiUploadForm'] = __DIR__ . '/SpecialMultiUpload.php'; -$wgAutoloadClasses['FauxWebRequestUpload'] = __DIR__ . '/SpecialMultiUpload.php'; -$wgAutoloadClasses['DerivativeRequestWithFiles'] = __DIR__ . '/SpecialMultiUpload.php'; -$wgAutoloadClasses['UploadRow'] = __DIR__ . '/SpecialMultiUpload.php'; -$wgAutoloadClasses['UploadFormRow'] = __DIR__ . '/SpecialMultiUpload.php'; - -# ===== ResourceLoader ===== -$resourceModuleTemplate = array( - 'localBasePath' => __DIR__ . '/resources', -); - -$wgResourceModules['special.upload.patched'] = $resourceModuleTemplate + array( - 'scripts' => array( - 'mediawiki.special.upload.js.patched', - 'upload.js.patched', - ), - 'styles' => array( - 'ext.multiupload.mw1.19.compatibility.css', # actually useful in 1.23 as well - ), - 'messages' => array( - 'widthheight', - 'size-bytes', - 'size-kilobytes', - 'size-megabytes', - 'size-gigabytes', - 'largefileserver', - ), - 'dependencies' => array( - 'mediawiki.libs.jpegmeta', - 'mediawiki.api', - 'mediawiki.Title', - 'mediawiki.legacy.wikibits', - 'mediawiki.util', - 'jquery.spinner', - ), -); - -// slightly different patched code for older versions of MW -if ( version_compare( $wgVersion, '1.22', '<' ) ) { - $wgResourceModules['special.upload.patched']['scripts'] = array( - 'mediawiki.special.upload.js.patched.1.21.3', - 'upload.js.patched.1.21.3', - ); -} - -$wgResourceModules['ext.multiupload.top'] = $resourceModuleTemplate + array( - 'scripts' => array( 'ext.multiupload.top.js' ), - 'styles' => array( 'ext.multiupload.top.css' ), - 'position' => 'top', -); - -$wgResourceModules['ext.multiupload.unpack'] = $resourceModuleTemplate + array( - 'scripts' => array( - 'ext.multiupload.unpack.js', - 'mw.FormDataTransport.js', - ), - 'dependencies' => array( - 'mediawiki.api', - 'ext.multiupload.top', - ), - 'messages' => array( - 'multiupload-upload-package-error', - 'multiupload-unpack-error', - 'multiupload-http-error', - 'multiupload-file-unpacked-from', - ), -); - -$wgResourceModules['ext.multiupload.shared'] = $resourceModuleTemplate + array( - 'scripts' => array( - 'ext.multiupload.shared.js', - ), - 'styles' => array( - 'ext.multiupload.shared.css', - ), - 'dependencies' => array( - 'ext.multiupload.top', - 'special.upload.patched', - ), - 'messages' => array( - 'multiupload-row', # note: this message has to be named just so - it overlaps with id names in the DOM, created by HTMLForm - 'multiupload-unpack-button', - ), -); - -$wgResourceModules['ext.multiupload'] = $resourceModuleTemplate + array( - 'scripts' => array( 'ext.multiupload.js' ), - 'dependencies' => array( - 'ext.multiupload.shared', - ), -); - -# api.php actions -$wgAPIModules['multiupload-unpack'] = 'MultiUploadApiUnpack'; -$wgAutoloadClasses['MultiUploadApiUnpack'] = __DIR__ . '/MultiUploadApi.php'; - -# (potentially) multilingual messages -$wgExtensionMessagesFiles['MultiUpload'] = __DIR__ . '/MultiUpload.i18n.php'; -$wgMessagesDirs['MultiUpload'] = __DIR__ . '/i18n'; - -# special page aliases -$wgExtensionMessagesFiles['MultiUploadAlias'] = __DIR__ . '/MultiUpload.alias.php'; \ No newline at end of file diff --git a/MultiUploadApi.php b/MultiUploadApi.php index 7551506..d605c9f 100644 --- a/MultiUploadApi.php +++ b/MultiUploadApi.php @@ -13,6 +13,36 @@ } /** + * Get the temporary directory and the prefix in a platform-independent way, + * no matter what. + * + * @return array( temp dir (such as /tmp/MultiUpload_unpack_5pzFrr on *NIX), prefix (such as /tmp/MultiUpload_unpack_ on *NIX) ) + */ + public function getTempDirAndPrefix() { + global $wgMultiUploadTempDir; + if ( wfIsWindows() ) { + // tempnam(), or at least how this code uses it, is bugged on Windows + // @see http://php.net/manual/en/function.tempnam.php + // Quoth that page: "Note: Windows uses only the first three characters of prefix." + $ourTempDir = is_dir( $wgMultiUploadTempDir ) ? $wgMultiUploadTempDir : wfTempDir(); // C:\Users\<user name>\AppData\Local\Temp + $unpackLocation = realpath( tempnam( $ourTempDir, MultiUploadApiUnpack::$unpackDirBase ) ); // C:\Users\<user name>\AppData\Local\Temp\MulD6D8.tmp + $removeMe = end( explode( DIRECTORY_SEPARATOR, $unpackLocation ) ); // such as MulD6D8.tmp + $prefix = str_replace( str_replace( 'Mul', '', $removeMe ), '', $unpackLocation ); // should be something like C:\Users\<user name>\AppData\Local\Temp\Mul + } else { + // the end result is something like "/tmp/MultiUpload_unpack_Whcdu9" + // here (for the $unpackLocation var) and "/tmp/MultiUpload_unpack_" + // for the $prefix var + $unpackLocation = realpath( tempnam( + $wgMultiUploadTempDir, + MultiUploadApiUnpack::$unpackDirBase + ) ); + $prefix = realpath( $wgMultiUploadTempDir ) . '/' . + MultiUploadApiUnpack::$unpackDirBase; + } + return array( $unpackLocation, $prefix ); + } + + /** * Given a package (e.g. tar.gz or .zip file), unpack it into a temporary * location. * @@ -25,14 +55,8 @@ * In case of error, returns array(false, error_message). */ public function unpack( $pkgFile, $srcName ) { - global $wgMultiUploadTempDir; + list( $unpackLocation, $prefix ) = $this->getTempDirAndPrefix(); - $unpackLocation = realpath( tempnam( - $wgMultiUploadTempDir, - MultiUploadApiUnpack::$unpackDirBase - ) ); - $prefix = realpath( $wgMultiUploadTempDir ) . '/' . - MultiUploadApiUnpack::$unpackDirBase; wfDebugLog( 'MultiUploadApi', "unpackLocation is $unpackLocation, prefix is $prefix\n" ); if ( strncmp( $unpackLocation, $prefix, strlen( $prefix ) ) != 0 ) { @@ -63,20 +87,42 @@ # references. So I would need to check tar contents for ../ before extracting. In practice # it seems to be catching this case — but I should trap it explicitly anyway? if ( $this->suffixMatches( $srcName, '.tgz' ) || $this->suffixMatches( $srcName, '.tar.gz' ) ) { - $unpackCommand = 'tar -xz -C ' . escapeshellarg( $unpackLocation ) - . ' -f ' . escapeshellarg( $pkgFile ); + $unpackCommand = 'tar -xz -C ' . wfEscapeShellArg( $unpackLocation ) + . ' -f ' . wfEscapeShellArg( $pkgFile ); + system( $unpackCommand, $unpackSuccess ); } elseif ( $this->suffixMatches( $srcName, '.tar' ) ) { - $unpackCommand = 'tar -x -C ' . escapeshellarg( $unpackLocation ) - . ' -f ' . escapeshellarg( $pkgFile ); + $unpackCommand = 'tar -x -C ' . wfEscapeShellArg( $unpackLocation ) + . ' -f ' . wfEscapeShellArg( $pkgFile ); + system( $unpackCommand, $unpackSuccess ); } elseif ( $this->suffixMatches( $srcName, '.zip' ) ) { # unzip also rejects files outside of the extraction directory - $unpackCommand = 'unzip -q ' . escapeshellarg( $pkgFile ) - . ' -d ' . escapeshellarg( $unpackLocation ); + if ( wfIsWindows() ) { + // Windows is different + // @see https://phabricator.wikimedia.org/T124908 + $E_STRICT_IS_STUPID = explode( '/', $pkgFile ); + $removeMeToGetTheRealTempDirPathOnWindows = end( $E_STRICT_IS_STUPID ); // [sic] + $realUnpackLocation = str_replace( $removeMeToGetTheRealTempDirPathOnWindows, '', $pkgFile ); + + $zip = new ZipArchive; + $res = $zip->open( $pkgFile ); + if ( $res ) { + wfDebugLog( 'MultiUploadApi', 'On Windows; ZipArchive extraction OK, extracting to ' . print_r( $realUnpackLocation, true ) ); + $zip->extractTo( $realUnpackLocation ); + $zip->close(); + $unpackSuccess = 0; + } else { + wfDebugLog( 'MultiUploadApi', 'On Windows; ZipArchive extraction FAILED, status code = ' . print_r( $res, true ) ); + // something went wrong and extraction failed + $unpackSuccess = $res; + } + } else { + $unpackCommand = 'unzip -q ' . wfEscapeShellArg( $pkgFile ) + . ' -d ' . wfEscapeShellArg( $unpackLocation ); + system( $unpackCommand, $unpackSuccess ); + } } else { return array( false, "Unknown filetype $srcName" ); } - - system( $unpackCommand, $unpackSuccess ); if ( $unpackSuccess != 0 ) { return array( false, @@ -142,6 +188,8 @@ } public function execute() { + //global $wgMultiUploadTempDir; + $params = $this->extractRequestParams(); wfDebugLog( 'MultiUploadApi', 'MultiUploadApiUnpack, params is ' . json_encode( $params ) . "\n" ); @@ -159,6 +207,7 @@ // Second is to unpack it. Code for that is in ImportQueue. $packageFileName = $params['filename']; + wfDebugLog( 'MultiUploadApi', __METHOD__ . ': $packageFileName = ' . print_r( $packageFileName, true ) . ' and $path = ' . print_r( $path, true ) ); list( $success, $unpackCode ) = $this->unpack( $path, $packageFileName ); if ( !$success ) { $this->dieUsage( @@ -167,10 +216,15 @@ ); } - global $wgMultiUploadTempDir; + // $prefix is unused, but meh + list( $unpackDir, $prefix ) = $this->getTempDirAndPrefix(); + wfDebugLog( 'MultiUploadApi', 'in ' . __METHOD__ . ', raw $unpackDir is: ' . print_r( $unpackDir, true ) . ' and $prefix is : ' . print_r( $prefix, true ) ); + // $unpackDir nowadays contains $unpackCode already + /* $unpackDir = realpath( $wgMultiUploadTempDir ) . '/' . MultiUploadApiUnpack::$unpackDirBase . $unpackCode; + */ // done with the package - delete it $stash->removeFile( $sessionkey ); @@ -179,14 +233,34 @@ // now to traverse that directory, stash all the files and remember their // names - $filenames = $this->recursiveFindFiles( $unpackDir ); + if ( wfIsWindows() ) { + // $path is something like D:\xamppnew\htdocs\shoutwiki\trunk/images/temp/5/56/20161107221803!php8B81.tmp + // so we need to remove the last part + $dir = str_replace( end( explode( '/', $path ) ), '', $path ); + } else { + $dir = $unpackDir; + } + $filenames = $this->recursiveFindFiles( $dir ); + wfDebugLog( 'MultiUploadApi', 'filenames after finding them recursively: ' . print_r( $filenames, true ) ); natsort( $filenames ); + wfDebugLog( 'MultiUploadApi', 'filenames after natsort()ing: ' . print_r( $filenames, true ) ); $filedata = array(); + foreach ( $filenames as $filename ) { - $stashedFile = $stash->stashFile( $unpackDir . '/' . $filename, 'file' ); + if ( $filename == 'index.html' ) { + // I don't even... + continue; + } + if ( wfIsWindows() ) { + $fileToStash = $dir . $filename; + } else { + $fileToStash = $unpackDir . '/' . $filename; + } + wfDebugLog( 'MultiUploadApi', 'fileToStash: ' . print_r( $fileToStash, true ) . ', filename: ' . print_r( $filename, true ) ); + $stashedFile = $stash->stashFile( $fileToStash, 'file' ); $filedata[] = array( $stashedFile->getFileKey(), $filename ); } - $this->recursiveUnlink( $unpackDir, true ); + $this->recursiveUnlink( $dir, true ); $res = array( 'contents' => $filedata, @@ -221,4 +295,4 @@ public function getDescription() { return 'Unpack a zip or tar file before importing its contents.'; } -} +} \ No newline at end of file diff --git a/README b/README index 16b9677..667100e 100644 --- a/README +++ b/README @@ -7,7 +7,7 @@ === Requirements === * PHP 5.x -* MediaWiki 1.19 or later +* MediaWiki 1.27 or later === Installation === @@ -17,7 +17,7 @@ Add the line - require_once("extensions/MultiUpload/MultiUpload.php"); + wfLoadExtension( 'MultiUpload' ); to your LocalSettings.php file. diff --git a/SpecialMultiUpload.php b/SpecialMultiUpload.php index 49d40cd..a295210 100644 --- a/SpecialMultiUpload.php +++ b/SpecialMultiUpload.php @@ -25,13 +25,6 @@ die(); } -/* use the local, patched version of SpecialUpload.php for now */ -global $wgVersion; -$wgAutoloadLocalClasses['SpecialUpload'] - = $wgAutoloadLocalClasses['UploadForm'] - = $wgAutoloadLocalClasses['UploadSourceField'] - = __DIR__ . '/SpecialUpload.php'; - /** * Special page for uploading multiple files in one submission. */ @@ -55,9 +48,42 @@ return 'media'; } - protected function handleRequestData() { + /** + * Special page entry point + */ + public function execute( $par ) { + $this->setHeaders(); + $this->outputHeader(); + + # Check uploading enabled + if ( !UploadBase::isEnabled() ) { + throw new ErrorPageError( 'uploaddisabled', 'uploaddisabledtext' ); + } + + # Check permissions + $user = $this->getUser(); + $permissionRequired = UploadBase::isAllowed( $user ); + if ( $permissionRequired !== true ) { + throw new PermissionsError( $permissionRequired ); + } + + # Check blocks + if ( $user->isBlocked() ) { + throw new UserBlockedError( $user->getBlock() ); + } + + # Check whether we actually want to allow changing stuff + $this->checkReadOnly(); + + $this->handleRequestData(); + } + + public function handleRequestData() { $request = $this->getRequest(); - $this->checkToken( $request ); + + $token = $request->getVal( 'wpEditToken' ); + $this->mTokenOk = $this->getUser()->matchEditToken( $token ); + $this->mFrom = 1; # $request->getVal( 'from', 1 ); global $wgMultiUploadInitialNumberOfImportRows; $this->mTo = $request->getVal( 'wpLastRowIndex', @@ -128,10 +154,12 @@ $this->mSourceIds = array(); $this->mMessagePrefix = 'multiupload'; $this->setSubmitText( wfMessage( 'multiupload-submit' )->parse() ); + // ashley 28 October 2016: I have no idea what I'm doing... + $this->constructForm( $context ); } - protected function constructData( array $options = array(), IContextSource $context = null ) { - } +// protected function constructData( array $options = array(), IContextSource $context = null ) { +// } protected function constructForm( IContextSource $context ) { $descriptor = $this->getGlobalFormDescriptors() @@ -159,24 +187,22 @@ return wfMessage( "{$this->mMessagePrefix}-$msg", $parts )->parse(); } - protected function addJsConfigVars( $out ) { - parent::addJsConfigVars( $out ); + protected function addUploadJS() { + $out = $this->getOutput(); + parent::addUploadJS(); $jsConfig = array( 'wpFirstRowIndex' => $this->mPage->mFrom, 'wpLastRowIndex' => $this->mPage->mTo, 'wgMultiUploadMaxPhpUploadSize' => min( wfShorthandToInteger( ini_get( 'upload_max_filesize' ) ), wfShorthandToInteger( ini_get( 'post_max_size' ) ) - ), - + ) ); foreach ( $this->mRows as $row ) { $jsConfig = $jsConfig + $row->jsConfigVars(); } $out->addJsConfigVars( $jsConfig ); - } - protected function addRLModules( $out ) { $out->addModules( array( 'ext.multiupload.top', 'ext.multiupload', @@ -187,7 +213,7 @@ /** * Hoping this gets merged into core, won't have to do it here */ -if ( !class_exists( 'FauxWebRequestUpload' ) ) { +//if ( !class_exists( 'FauxWebRequestUpload' ) ) { /** * A WebRequestUpload that can be faked. */ @@ -222,12 +248,12 @@ } } } -} else { +//} else { /** * If the feature is in MW core, just use it */ - class DerivativeRequestWithFiles extends DerivativeRequest { } -} +// class DerivativeRequestWithFiles extends DerivativeRequest { } +//} class UploadRow extends SpecialUpload { public $mPage; @@ -290,19 +316,45 @@ return $this->mRequest; } - protected function handleRequestData() { + /** + * Respond to submitted form data and/or display upload form + * @since 1.23 + */ + public function handleRequestData() { $request = $this->getRequest(); $this->mUploadSuccessful = $request->getCheck( 'wpUploadSuccessful' ); - parent::handleRequestData(); - } - /** - * We don't do our own form output - we give all output to the - * page object to aggregate into a single form - */ - protected function showUploadForm( $form ) { - // it gets called - // wfDebug(" *** SHOWUPLOADFORM SHOULD NOT BE CALLED! *** \n"); + $this->loadRequest(); + + # 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; + } + } + + # Process upload or show a form + if ( $this->shouldProcessUpload() ) { + $this->processUpload(); + } else { + # Backwards compatibility hook + if ( !Hooks::run( 'UploadForm:initial', array( &$this ) ) ) { + wfDebug( "Hook 'UploadForm:initial' broke output of the upload form" ); + return; + } + // MediaWiki 1.28 fix -- either MW doesn't call the empty function + // below or then calling it has no effect, but for whatever reason + // MW tends to act like it calls the *parent* class' function w/ + // that name, which breaks stuff. Commenting this out doesn't appear + // to have any nasty side-effects, so... + //$this->showUploadForm( $this->getUploadForm() ); + } + + # Cleanup + if ( $this->mUpload ) { + $this->mUpload->cleanupTempFile(); + } } /** @@ -310,13 +362,35 @@ * this is called, wait and do it when the page object is ready to * assemble its full output form. * - * @param $message String: HTML string to add to the form - * @param $sessionKey String: session key in case this is a stashed upload - * @param $hideIgnoreWarning Boolean: whether to hide "ignore warning" check box - * @return UploadForm - * todo lots of redundancy here + * @param HTMLForm|string $form An HTMLForm instance or HTML string to show [unused] */ - public function getUploadForm( $message = '', $sessionKey = '', $hideIgnoreWarning = false ) { + public function showUploadForm( $form ) { + } + + /** + * Format an upload error message for display. + * + * @param string $message HTML string + * @return string HTML message + * @since 1.23 + */ + protected function getUploadError( $message ) { + return '<h2>' . $this->msg( 'uploadwarning' )->escaped() . "</h2>\n" . + '<div class="error">' . $message . "</div>\n"; + } + + /** + * Construct a recoverable error message. + * + * See showRecoverableUploadError below. + * + * @param string $message HTML message to be passed to mainUploadForm + * @return string formatted HTML + * @since 1.23 + */ + protected function getRecoverableUploadError( $message ) { + return '<h2>' . $this->msg( 'uploaderror' )->escaped() . "</h2>\n" . + '<div class="error">' . $message . "</div>\n"; } public function showUploadError( $message ) { @@ -328,6 +402,87 @@ $this->mFormMessage .= $this->getRecoverableUploadError( $message ); } + /** + * Construct a formatted list of upload warnings. + * + * @todo FIXME: filthy copypasta to work around a fatal --ashley 29 October 2016 + * + * @param array $warnings + * @return string|bool A string if there are warnings to display, false if there are no + * warnings and it should continue processing + * @return string Formatted HTML + * @since 1.23 + */ + protected function getUploadWarning( $warnings ) { + # If there are no warnings, or warnings we can ignore, return early. + # mDestWarningAck is set when some javascript has shown the warning + # to the user. mForReUpload is set when the user clicks the "upload a + # new version" link. + if ( !$warnings || ( count( $warnings ) == 1 + && isset( $warnings['exists'] ) + && ( $this->mDestWarningAck || $this->mForReUpload ) ) + ) { + return false; + } + + $warningHtml = '<h2>' . $this->msg( 'uploadwarning' )->escaped() . "</h2>\n" + . '<ul class="warning">'; + foreach ( $warnings as $warning => $args ) { + if ( $warning == 'badfilename' ) { + $this->mDesiredDestName = Title::makeTitle( NS_FILE, $args )->getText(); + } + if ( $warning == 'exists' ) { + $msg = "\t<li>" . self::getExistsWarning( $args ) . "</li>\n"; + } elseif ( $warning == 'duplicate' ) { + $msg = $this->getDupeWarning( $args ); + } elseif ( $warning == 'duplicate-archive' ) { + $msg = "\t<li>" . $this->msg( 'file-deleted-duplicate', + Title::makeTitle( NS_FILE, $args )->getPrefixedText() )->parse() + . "</li>\n"; + } else { + if ( $args === true ) { + $args = array(); + } elseif ( !is_array( $args ) ) { + $args = array( $args ); + } + $msg = "\t<li>" . $this->msg( $warning, $args )->parse() . "</li>\n"; + } + $warningHtml .= $msg; + } + $warningHtml .= "</ul>\n"; + $warningHtml .= $this->msg( 'uploadwarning-text' )->parseAsBlock(); + + return $warningHtml; + } + + /** + * Stash the upload, show the main form, but add a "continue anyway" button. + * Also check whether there are actually warnings to display. + * + * @param array $warnings + * @return bool True if warnings were displayed, false if there are no + * warnings and it should continue processing + */ + protected function showUploadWarning( $warnings ) { + $warningHtml = $this->getUploadWarning( $warnings ); + if ( $warningHtml === false ) { + return false; + } + + $sessionKey = $this->mUpload->stashSession(); + + $form = $this->getUploadForm( $warningHtml, $sessionKey, /* $hideIgnoreWarning */ true ); + $form->setSubmitText( $this->msg( 'upload-tryagain' )->text() ); + $form->addButton( 'wpUploadIgnoreWarning', $this->msg( 'ignorewarning' )->text() ); + $form->addButton( 'wpCancelUpload', $this->msg( 'reuploaddesc' )->text() ); + + $this->showUploadForm( $form ); + + # Indicate that we showed a form + return true; + } + + /* OLD VERSION: protected function showUploadWarning( $warnings ) { $warningHtml = $this->getUploadWarning( $warnings ); if ( $warningHtml === false ) { @@ -349,6 +504,7 @@ return true; } + */ /** * This is apparently a pretty bad one. @@ -382,12 +538,50 @@ protected function createFormRow() { return new UploadFormRow( $this, - $this->getFormOptions( - $this->mSessionKey, - $this->mHideIgnoreWarning + array( + 'watch' => $this->getWatchCheck(), + 'forreupload' => $this->mForReUpload, + 'sessionkey' => $this->mSessionKey, + 'hideignorewarning' => $this->mHideIgnoreWarning, + 'destwarningack' => (bool)$this->mDestWarningAck, + + 'description' => $this->mComment, + 'texttop' => $this->uploadFormTextTop, + 'textaftersummary' => $this->uploadFormTextAfterSummary, + 'destfile' => $this->mDesiredDestName, + 'sourcetype' => $this->mSourceType, ), $this->getContext() ); + } + + /** + * Assemble the text of the "view X deleted revisions" link + * + * @todo FIXME: yet another filthy hack since this returns a string + * instead of using core's OutputPage directly as SpecialUpload::showViewDeletedLinks() + * does. --ashley 29 October 2016 + * + * @return string + * @since 1.23 + */ + protected function getViewDeletedLinks() { + $title = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName ); + $user = $this->getUser(); + // Show a subtitle link to deleted revisions (to sysops et al only) + if ( $title instanceof Title ) { + $count = $title->isDeleted(); + if ( $count > 0 && $user->isAllowed( 'deletedhistory' ) ) { + $restorelink = Linker::linkKnown( + SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedText() ), + $this->msg( 'restorelink' )->numParams( $count )->escaped() + ); + $link = $this->msg( $user->isAllowed( 'delete' ) ? 'thisisdeleted' : 'viewdeleted' ) + ->rawParams( $restorelink )->parseAsBlock(); + return "<div id=\"contentSub2\">{$link}</div>"; + } + } + return ''; } public function getFormDescriptors() { @@ -396,6 +590,7 @@ $preText = ''; + # Add links if file was previously deleted if ( $this->mDesiredDestName ) { $preText .= $this->getViewDeletedLinks(); } @@ -427,6 +622,8 @@ $this->mUploadSuccessful ); } + // @todo FIXME: <s>not called anymore</s>, core SpecialUpload::execute() used to + // call this right above the 'UploadForm:initial' hook --ashley 28 October 2016 protected function shouldProcessUpload() { return ( !$this->mUploadSuccessful && $this->mPage->mTokenOk && !$this->mCancelUpload && @@ -434,11 +631,17 @@ $this->mUploadClicked ) ); } + // @todo FIXME: doesn't exist in core anymore as of 1.27. remove? --ashley 28 October 2016 protected function uploadSucceeded() { $this->mDesiredDestName = $this->mLocalFile->getTitle()->getDBkey(); } /** + * @todo FIXME: probably should be something like this: + * @code + $this->getOutput()->addJsConfigVars( array( ... ) ); + * @endcode + * but where would it then be called from? Hmm... --ashley 28 October 2016 * @return array */ public function jsConfigVars() { @@ -450,6 +653,17 @@ $this->mDesiredDestName === '' ), ); } + + protected function addUploadJS() { + $out = $this->getOutput(); + $out->addJsConfigVars( array( + 'wgMultiUploadAutoFill' . $this->mRowNumber => + ( !$this->mForReUpload && + // if mDestFile was provided in the request, + // don't overwrite it by autofilling + $this->mDesiredDestName === '' ), + ) ); + } } class UploadFormRow extends UploadForm { @@ -457,7 +671,29 @@ function __construct( $row, array $options = array(), IContextSource $context = null ) { $this->mRow = $row; - $this->constructData( $options, $context ); + # setContext is called in the constructor, but it's needed + # before we get there + $this->setContext( $context ); + + $this->mWatch = !empty( $options['watch'] ); + $this->mForReUpload = !empty( $options['forreupload'] ); + $this->mSessionKey = isset( $options['sessionkey'] ) + ? $options['sessionkey'] : ''; + $this->mHideIgnoreWarning = !empty( $options['hideignorewarning'] ); + $this->mDestWarningAck = !empty( $options['destwarningack'] ); + $this->mDestFile = isset( $options['destfile'] ) ? $options['destfile'] : ''; + + $this->mComment = isset( $options['description'] ) ? + $options['description'] : ''; + + $this->mTextTop = isset( $options['texttop'] ) + ? $options['texttop'] : ''; + + $this->mTextAfterSummary = isset( $options['textaftersummary'] ) + ? $options['textaftersummary'] : ''; + + $this->mSourceType = isset( $options['sourcetype'] ) + ? $options['sourcetype'] : ''; # HTMLForm::__construct( array(), $context, 'upload' ); # $this->mSourceIds = array(); } diff --git a/SpecialUpload.php b/SpecialUpload.php deleted file mode 100644 index 7c151a3..0000000 --- a/SpecialUpload.php +++ /dev/null @@ -1,1288 +0,0 @@ -<?php -/** - * Implements Special:Upload - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - * @ingroup SpecialPage - * @ingroup Upload - */ - -/** - * Form for handling uploads and special page. - * - * @ingroup SpecialPage - * @ingroup Upload - */ -class SpecialUpload extends SpecialPage { - /** - * Constructor : initialise object - * Get data POSTed through the form and assign them to the object - * @param $request WebRequest : data posted. - */ - public function __construct( $request = null ) { - parent::__construct( 'Upload', 'upload' ); - } - - /** Misc variables **/ - public $mRequest; // The WebRequest or FauxRequest this form is supposed to handle - public $mSourceType; - - /** - * @var UploadBase - */ - public $mUpload; - - /** - * @var LocalFile - */ - public $mLocalFile; - public $mUploadClicked; - - /** User input variables from the "description" section **/ - public $mDesiredDestName; // The requested target file name - public $mComment; - public $mLicense; - - /** User input variables from the root section **/ - public $mIgnoreWarning; - public $mWatchthis; - public $mCopyrightStatus; - public $mCopyrightSource; - - /** Hidden variables **/ - public $mDestWarningAck; - public $mForReUpload; // The user followed an "overwrite this file" link - public $mCancelUpload; // The user clicked "Cancel and return to upload form" button - public $mTokenOk; - public $mUploadSuccessful = false; // Subclasses can use this to determine whether a file was uploaded - - /** Text injection points for hooks not using HTMLForm **/ - public $uploadFormTextTop; - public $uploadFormTextAfterSummary; - - /** - * Initialize instance variables from request and create an Upload handler - */ - protected function loadRequest() { - $this->mRequest = $request = $this->getRequest(); - $this->mSourceType = $request->getVal( 'wpSourceType', 'file' ); - $this->mUpload = UploadBase::createFromRequest( $request ); - $this->mUploadClicked = $request->wasPosted() - && ( $request->getCheck( 'wpUpload' ) - || $request->getCheck( 'wpUploadIgnoreWarning' ) ); - - // Guess the desired name from the filename if not provided - $this->mDesiredDestName = $request->getText( 'wpDestFile' ); - if ( !$this->mDesiredDestName && $request->getFileName( 'wpUploadFile' ) !== null ) { - $this->mDesiredDestName = $request->getFileName( 'wpUploadFile' ); - } - $this->mLicense = $request->getText( 'wpLicense' ); - - $this->mDestWarningAck = $request->getText( 'wpDestFileWarningAck' ); - $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' ) - || $request->getCheck( 'wpUploadIgnoreWarning' ); - $this->mWatchthis = $request->getBool( 'wpWatchthis' ) && $this->getUser()->isLoggedIn(); - $this->mCopyrightStatus = $request->getText( 'wpUploadCopyStatus' ); - $this->mCopyrightSource = $request->getText( 'wpUploadSource' ); - - $this->mForReUpload = $request->getBool( 'wpForReUpload' ); // updating a file - - $commentDefault = ''; - $commentMsg = wfMessage( 'upload-default-description' )->inContentLanguage(); - if ( !$this->mForReUpload && !$commentMsg->isDisabled() ) { - $commentDefault = $commentMsg->plain(); - } - $this->mComment = $request->getText( 'wpUploadDescription', $commentDefault ); - - $this->mCancelUpload = $request->getCheck( 'wpCancelUpload' ) - || $request->getCheck( 'wpReUpload' ); // b/w compat - - $this->checkToken( $request ); - - $this->uploadFormTextTop = ''; - $this->uploadFormTextAfterSummary = ''; - } - - /** - * Check for valid edit token, to protect against remote posting - * with user credentials. - * - * @param $request WebRequest object - * @since 1.23 - */ - protected function checkToken( $request ) { - $token = $request->getVal( 'wpEditToken' ); - $this->mTokenOk = $this->getUser()->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 object - * @return Boolean - */ - public function userCanExecute( User $user ) { - return UploadBase::isEnabled() && parent::userCanExecute( $user ); - } - - /** - * Special page entry point - */ - public function execute( $par ) { - $this->setHeaders(); - $this->outputHeader(); - - # Check uploading enabled - if ( !UploadBase::isEnabled() ) { - throw new ErrorPageError( 'uploaddisabled', 'uploaddisabledtext' ); - } - - # Check permissions - $user = $this->getUser(); - $permissionRequired = UploadBase::isAllowed( $user ); - if ( $permissionRequired !== true ) { - throw new PermissionsError( $permissionRequired ); - } - - # Check blocks - if ( $user->isBlocked() ) { - throw new UserBlockedError( $user->getBlock() ); - } - - # Check whether we actually want to allow changing stuff - $this->checkReadOnly(); - - $this->handleRequestData(); - } - - /** - * Respond to submitted form data and/or display upload form - * @since 1.23 - */ - protected function handleRequestData() { - $this->loadRequest(); - - # 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; - } - } - - # Process upload or show a form - if ( $this->shouldProcessUpload() ) { - $this->processUpload(); - } else { - # Backwards compatibility hook - if ( !Hooks::run( 'UploadForm:initial', array( &$this ) ) ) { - wfDebug( "Hook 'UploadForm:initial' broke output of the upload form" ); - return; - } - $this->showUploadForm( $this->getUploadForm() ); - } - - # Cleanup - if ( $this->mUpload ) { - $this->mUpload->cleanupTempFile(); - } - } - - /** - * Decide whether an upload has been requested - * @return Bool - * @since 1.23 - */ - protected function shouldProcessUpload() { - return ( - $this->mTokenOk && !$this->mCancelUpload && - ( $this->mUpload && $this->mUploadClicked ) - ); - } - - /** - * Show the main upload form - * - * @param $form Mixed: an HTMLForm instance or HTML string to show - */ - protected function showUploadForm( $form ) { - # Add links if file was previously deleted - if ( $this->mDesiredDestName ) { - $this->showViewDeletedLinks(); - } - - if ( $form instanceof HTMLForm ) { - $form->show(); - } else { - $this->getOutput()->addHTML( $form ); - } - - } - - /** - * Assemble options array for constructing form data. - * - * @param string $sessionKey session key in case this is a stashed upload - * @param $hideIgnoreWarning Boolean: Whether to hide "ignore warning" check box - * @return Array - */ - protected function getFormOptions( $sessionKey = '', $hideIgnoreWarning = false ) { - return array( - 'watch' => $this->getWatchCheck(), - 'forreupload' => $this->mForReUpload, - 'sessionkey' => $sessionKey, - 'hideignorewarning' => $hideIgnoreWarning, - 'destwarningack' => (bool)$this->mDestWarningAck, - - 'description' => $this->mComment, - 'texttop' => $this->uploadFormTextTop, - 'textaftersummary' => $this->uploadFormTextAfterSummary, - 'destfile' => $this->mDesiredDestName, - 'sourcetype' => $this->mSourceType, - ); - } - - /** - * 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 - * @param $hideIgnoreWarning Boolean: whether to hide "ignore warning" check box - * @return UploadForm - */ - protected function getUploadForm( $message = '', $sessionKey = '', $hideIgnoreWarning = false ) { - $context = new DerivativeContext( $this->getContext() ); - $context->setTitle( $this->getTitle() ); // Remove subpage - # Initialize form - $form = new UploadForm( - $this->getFormOptions( $sessionKey, $hideIgnoreWarning ), - $context - ); - $form->setTitle( $this->getTitle() ); - - # Check the token, but only if necessary - if ( - !$this->mTokenOk && !$this->mCancelUpload && - ( $this->mUpload && $this->mUploadClicked ) - ) { - $form->addPreText( $this->msg( 'session_fail_preview' )->parse() ); - } - - # Give a notice if the user is uploading a file that has been deleted or moved - # Note that this is independent from the message 'filewasdeleted' that requires JS - $desiredTitleObj = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName ); - $delNotice = ''; // empty by default - if ( $desiredTitleObj instanceof Title && !$desiredTitleObj->exists() ) { - LogEventsList::showLogExtract( $delNotice, array( 'delete', 'move' ), - $desiredTitleObj, - '', array( 'lim' => 10, - 'conds' => array( "log_action != 'revision'" ), - 'showIfEmpty' => false, - 'msgKey' => array( 'upload-recreate-warning' ) ) - ); - } - $form->addPreText( $delNotice ); - - # Add text to form - $form->addPreText( '<div id="uploadtext">' . - $this->msg( 'uploadtext', array( $this->mDesiredDestName ) )->parseAsBlock() . - '</div>' ); - # Add upload error message - $form->addPreText( $message ); - - # Add footer to form - $uploadFooter = $this->msg( 'uploadfooter' ); - if ( !$uploadFooter->isDisabled() ) { - $form->addPostText( '<div id="mw-upload-footer-message">' - . $uploadFooter->parseAsBlock() . "</div>\n" ); - } - - return $form; - } - - /** - * Assemble the text of the "view X deleted revisions" link - * @return string - * @since 1.23 - */ - protected function getViewDeletedLinks() { - $title = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName ); - $user = $this->getUser(); - // Show a subtitle link to deleted revisions (to sysops et al only) - if ( $title instanceof Title ) { - $count = $title->isDeleted(); - if ( $count > 0 && $user->isAllowed( 'deletedhistory' ) ) { - $restorelink = Linker::linkKnown( - SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedText() ), - $this->msg( 'restorelink' )->numParams( $count )->escaped() - ); - $link = $this->msg( $user->isAllowed( 'delete' ) ? 'thisisdeleted' : 'viewdeleted' ) - ->rawParams( $restorelink )->parseAsBlock(); - return "<div id=\"contentSub2\">{$link}</div>"; - } - } - return ''; - } - - /* - * Show the "view X deleted revisions" link - */ - protected function showViewDeletedLinks() { - $html = $this->getViewDeletedLinks(); - if ( $html !== '' ) { - $this->getOutput()->addHTML( $html ); - } - } - - /** - * Construct a recoverable error message. - * - * See showRecoverableUploaderror, below. - * - * @param string $message HTML message to be passed to mainUploadForm - * @return string formatted HTML - * @since 1.23 - */ - protected function getRecoverableUploadError( $message ) { - return '<h2>' . $this->msg( 'uploaderror' )->escaped() . "</h2>\n" . - '<div class="error">' . $message . "</div>\n"; - } - - /** - * Stash the upload and show 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 showRecoverableUploadError( $message ) { - $sessionKey = $this->mUpload->stashSession(); - $form = $this->getUploadForm( - $this->getRecoverableUploadError( $message ), - $sessionKey - ); - $form->setSubmitText( $this->msg( 'upload-tryagain' )->escaped() ); - $this->showUploadForm( $form ); - } - - /** - * Construct a formatted list of upload warnings. - * - * @param $warnings Array - * @return mixed: a string if there are warnings to display, false if there are no - * warnings and it should continue processing - * @param Array $warnings - * @return string formatted HTML - * @since 1.23 - */ - protected function getUploadWarning( $warnings ) { - # If there are no warnings, or warnings we can ignore, return early. - # mDestWarningAck is set when some javascript has shown the warning - # to the user. mForReUpload is set when the user clicks the "upload a - # new version" link. - if ( !$warnings || ( count( $warnings ) == 1 - && isset( $warnings['exists'] ) - && ( $this->mDestWarningAck || $this->mForReUpload ) ) - ) { - return false; - } - - $warningHtml = '<h2>' . $this->msg( 'uploadwarning' )->escaped() . "</h2>\n" - . '<ul class="warning">'; - foreach ( $warnings as $warning => $args ) { - if ( $warning == 'badfilename' ) { - $this->mDesiredDestName = Title::makeTitle( NS_FILE, $args )->getText(); - } - if ( $warning == 'exists' ) { - $msg = "\t<li>" . self::getExistsWarning( $args ) . "</li>\n"; - } elseif ( $warning == 'duplicate' ) { - $msg = $this->getDupeWarning( $args ); - } elseif ( $warning == 'duplicate-archive' ) { - $msg = "\t<li>" . $this->msg( 'file-deleted-duplicate', - Title::makeTitle( NS_FILE, $args )->getPrefixedText() )->parse() - . "</li>\n"; - } else { - if ( $args === true ) { - $args = array(); - } elseif ( !is_array( $args ) ) { - $args = array( $args ); - } - $msg = "\t<li>" . $this->msg( $warning, $args )->parse() . "</li>\n"; - } - $warningHtml .= $msg; - } - $warningHtml .= "</ul>\n"; - $warningHtml .= $this->msg( 'uploadwarning-text' )->parseAsBlock(); - - return $warningHtml; - } - - /** - * Stash the upload, show the main form, but add a "continue anyway" button. - * Also check whether there are actually warnings to display. - * - * @param $warnings Array - * @return boolean true if warnings were displayed, false if there are no - * warnings and it should continue processing - */ - protected function showUploadWarning( $warnings ) { - $warningHtml = $this->getUploadWarning( $warnings ); - if ( $warningHtml === false ) { - return false; - } - - $sessionKey = $this->mUpload->stashSession(); - - $form = $this->getUploadForm( $warningHtml, $sessionKey, /* $hideIgnoreWarning */ true ); - $form->setSubmitText( $this->msg( 'upload-tryagain' )->text() ); - $form->addButton( 'wpUploadIgnoreWarning', $this->msg( 'ignorewarning' )->text() ); - $form->addButton( 'wpCancelUpload', $this->msg( 'reuploaddesc' )->text() ); - - $this->showUploadForm( $form ); - - # Indicate that we showed a form - return true; - } - - /** - * Format an upload error message for display. - * - * @param string $message HTML string - * @return string HTML message - * @since 1.23 - */ - protected function getUploadError( $message ) { - return '<h2>' . $this->msg( 'uploadwarning' )->escaped() . "</h2>\n" . - '<div class="error">' . $message . "</div>\n"; - } - - /** - * Show the upload form with error message, but do not stash the file. - * - * @param string $message HTML string - */ - protected function showUploadError( $message ) { - $this->showUploadForm( $this->getUploadForm( - $this->getUploadError( $message ) - ) ); - } - - /** - * Check various conditions and do the upload. - * Other checks are made in SpecialUpload::execute() - */ - protected function processUpload() { - // Fetch the file if required - $status = $this->mUpload->fetchFile(); - if ( !$status->isOK() ) { - $this->showUploadError( $this->getOutput()->parse( $status->getWikiText() ) ); - return; - } - - if ( !Hooks::run( 'UploadForm:BeforeProcessing', array( &$this ) ) ) { - wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file.\n" ); - // This code path is deprecated. If you want to break upload processing - // do so by hooking into the appropriate hooks in UploadBase::verifyUpload - // and UploadBase::verifyFile. - // If you use this hook to break uploading, the user will be returned - // an empty form with no error message whatsoever. - return; - } - - // Upload verification - $details = $this->mUpload->verifyUpload(); - if ( $details['status'] != UploadBase::OK ) { - $this->processVerificationError( $details ); - return; - } - - // Verify permissions for this title - $permErrors = $this->mUpload->verifyTitlePermissions( $this->getUser() ); - if ( $permErrors !== true ) { - $code = array_shift( $permErrors[0] ); - $this->showRecoverableUploadError( $this->msg( $code, $permErrors[0] )->parse() ); - return; - } - - $this->mLocalFile = $this->mUpload->getLocalFile(); - - // Check warnings if necessary - if ( !$this->mIgnoreWarning ) { - $warnings = $this->mUpload->checkWarnings(); - if ( $this->showUploadWarning( $warnings ) ) { - return; - } - } - - // Get the page text if this is not a reupload - if ( !$this->mForReUpload ) { - $pageText = self::getInitialPageText( $this->mComment, $this->mLicense, - $this->mCopyrightStatus, $this->mCopyrightSource ); - } else { - $pageText = false; - } - $status = $this->mUpload->performUpload( $this->mComment, $pageText, $this->mWatchthis, $this->getUser() ); - if ( !$status->isGood() ) { - $this->showUploadError( $this->getOutput()->parse( $status->getWikiText() ) ); - return; - } - - // Success, redirect to description page - $this->mUploadSuccessful = true; - Hooks::run( 'SpecialUploadComplete', array( &$this ) ); - $this->uploadSucceeded(); - } - - /** - * Once upload is successful, redirect to the updated File: page - * @since 1.23 - */ - protected function uploadSucceeded() { - $this->getOutput()->redirect( $this->mLocalFile->getTitle()->getFullURL() ); - } - - /** - * Get the initial image page text based on a comment and optional file status information - * @param $comment string - * @param $license string - * @param $copyStatus string - * @param $source string - * @return string - */ - public static function getInitialPageText( $comment = '', $license = '', $copyStatus = '', $source = '' ) { - global $wgUseCopyrightUpload, $wgForceUIMsgAsContentMsg; - - $msg = array(); - /* These messages are transcluded into the actual text of the description page. - * Thus, forcing them as content messages makes the upload to produce an int: template - * instead of hardcoding it there in the uploader language. - */ - foreach ( array( 'license-header', 'filedesc', 'filestatus', 'filesource' ) as $msgName ) { - if ( in_array( $msgName, (array)$wgForceUIMsgAsContentMsg ) ) { - $msg[$msgName] = "{{int:$msgName}}"; - } else { - $msg[$msgName] = wfMessage( $msgName )->inContentLanguage()->text(); - } - } - - if ( $wgUseCopyrightUpload ) { - $licensetxt = ''; - if ( $license != '' ) { - $licensetxt = '== ' . $msg['license-header'] . " ==\n" . '{{' . $license . '}}' . "\n"; - } - $pageText = '== ' . $msg['filedesc'] . " ==\n" . $comment . "\n" . - '== ' . $msg['filestatus'] . " ==\n" . $copyStatus . "\n" . - "$licensetxt" . - '== ' . $msg['filesource'] . " ==\n" . $source; - } else { - if ( $license != '' ) { - $filedesc = $comment == '' ? '' : '== ' . $msg['filedesc'] . " ==\n" . $comment . "\n"; - $pageText = $filedesc . - '== ' . $msg['license-header'] . " ==\n" . '{{' . $license . '}}' . "\n"; - } else { - $pageText = $comment; - } - } - return $pageText; - } - - /** - * See if we should check the 'watch this page' checkbox on the form - * based on the user's preferences and whether we're being asked - * to create a new file or update an existing one. - * - * In the case where 'watch edits' is off but 'watch creations' is on, - * we'll leave the box unchecked. - * - * Note that the page target can be changed *on the form*, so our check - * state can get out of sync. - * @return Bool|String - */ - protected function getWatchCheck() { - if ( $this->getUser()->getOption( 'watchdefault' ) ) { - // Watch all edits! - return true; - } - - $desiredTitleObj = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName ); - if ( $desiredTitleObj instanceof Title && $this->getUser()->isWatched( $desiredTitleObj ) ) { - // Already watched, don't change that - return true; - } - - $local = wfLocalFile( $this->mDesiredDestName ); - if ( $local && $local->exists() ) { - // We're uploading a new version of an existing file. - // No creation, so don't watch it if we're not already. - return false; - } else { - // New page should get watched if that's our option. - return $this->getUser()->getOption( 'watchcreations' ); - } - } - - /** - * Provides output to the user for a result of UploadBase::verifyUpload - * - * @param array $details result of UploadBase::verifyUpload - * @throws MWException - */ - protected function processVerificationError( $details ) { - global $wgFileExtensions; - - switch ( $details['status'] ) { - - /** Statuses that only require name changing **/ - case UploadBase::MIN_LENGTH_PARTNAME: - $this->showRecoverableUploadError( $this->msg( 'minlength1' )->escaped() ); - break; - case UploadBase::ILLEGAL_FILENAME: - $this->showRecoverableUploadError( $this->msg( 'illegalfilename', - $details['filtered'] )->parse() ); - break; - case UploadBase::FILENAME_TOO_LONG: - $this->showRecoverableUploadError( $this->msg( 'filename-toolong' )->escaped() ); - break; - case UploadBase::FILETYPE_MISSING: - $this->showRecoverableUploadError( $this->msg( 'filetype-missing' )->parse() ); - break; - case UploadBase::WINDOWS_NONASCII_FILENAME: - $this->showRecoverableUploadError( $this->msg( 'windows-nonascii-filename' )->parse() ); - break; - - /** Statuses that require reuploading **/ - case UploadBase::EMPTY_FILE: - $this->showUploadError( $this->msg( 'emptyfile' )->escaped() ); - break; - case UploadBase::FILE_TOO_LARGE: - $this->showUploadError( $this->msg( 'largefileserver' )->escaped() ); - break; - case UploadBase::FILETYPE_BADTYPE: - $msg = $this->msg( 'filetype-banned-type' ); - if ( isset( $details['blacklistedExt'] ) ) { - $msg->params( $this->getLanguage()->commaList( $details['blacklistedExt'] ) ); - } else { - $msg->params( $details['finalExt'] ); - } - $extensions = array_unique( $wgFileExtensions ); - $msg->params( $this->getLanguage()->commaList( $extensions ), - count( $extensions ) ); - - // Add PLURAL support for the first parameter. This results - // in a bit unlogical parameter sequence, but does not break - // old translations - if ( isset( $details['blacklistedExt'] ) ) { - $msg->params( count( $details['blacklistedExt'] ) ); - } else { - $msg->params( 1 ); - } - - $this->showUploadError( $msg->parse() ); - break; - case UploadBase::VERIFICATION_ERROR: - unset( $details['status'] ); - $code = array_shift( $details['details'] ); - $this->showUploadError( $this->msg( $code, $details['details'] )->parse() ); - break; - case UploadBase::HOOK_ABORTED: - if ( is_array( $details['error'] ) ) { # allow hooks to return error details in an array - $args = $details['error']; - $error = array_shift( $args ); - } else { - $error = $details['error']; - $args = null; - } - - $this->showUploadError( $this->msg( $error, $args )->parse() ); - break; - default: - throw new MWException( __METHOD__ . ": Unknown value `{$details['status']}`" ); - } - } - - /** - * Remove a temporarily kept file stashed by saveTempUploadedFile(). - * - * @return Boolean: success - */ - protected function unsaveUploadedFile() { - if ( !( $this->mUpload instanceof UploadFromStash ) ) { - return true; - } - $success = $this->mUpload->unsaveUploadedFile(); - if ( !$success ) { - $this->showFileDeleteError(); - return false; - } else { - return true; - } - } - - /** - * Produce error output if uploaded file can't be unsaved. - * @since 1.23 - */ - protected function showFileDeleteError() { - $this->getOutput()->showFileDeleteError( $this->mUpload->getTempPath() ); - } - - /*** Functions for formatting warnings ***/ - - /** - * Formats a result of UploadBase::getExistsWarning as HTML - * This check is static and can be done pre-upload via AJAX - * - * @param array $exists the result of UploadBase::getExistsWarning - * @return String: empty string if there is no warning or an HTML fragment - */ - public static function getExistsWarning( $exists ) { - if ( !$exists ) { - return ''; - } - - $file = $exists['file']; - $filename = $file->getTitle()->getPrefixedText(); - $warning = ''; - - if ( $exists['warning'] == 'exists' ) { - // Exact match - $warning = wfMessage( 'fileexists', $filename )->parse(); - } elseif ( $exists['warning'] == 'page-exists' ) { - // Page exists but file does not - $warning = wfMessage( 'filepageexists', $filename )->parse(); - } elseif ( $exists['warning'] == 'exists-normalized' ) { - $warning = wfMessage( 'fileexists-extension', $filename, - $exists['normalizedFile']->getTitle()->getPrefixedText() )->parse(); - } elseif ( $exists['warning'] == 'thumb' ) { - // Swapped argument order compared with other messages for backwards compatibility - $warning = wfMessage( 'fileexists-thumbnail-yes', - $exists['thumbFile']->getTitle()->getPrefixedText(), $filename )->parse(); - } elseif ( $exists['warning'] == 'thumb-name' ) { - // Image w/o '180px-' does not exists, but we do not like these filenames - $name = $file->getName(); - $badPart = substr( $name, 0, strpos( $name, '-' ) + 1 ); - $warning = wfMessage( 'file-thumbnail-no', $badPart )->parse(); - } elseif ( $exists['warning'] == 'bad-prefix' ) { - $warning = wfMessage( 'filename-bad-prefix', $exists['prefix'] )->parse(); - } elseif ( $exists['warning'] == 'was-deleted' ) { - # If the file existed before and was deleted, warn the user of this - $ltitle = SpecialPage::getTitleFor( 'Log' ); - $llink = Linker::linkKnown( - $ltitle, - wfMessage( 'deletionlog' )->escaped(), - array(), - array( - 'type' => 'delete', - 'page' => $filename - ) - ); - $warning = wfMessage( 'filewasdeleted' )->rawParams( $llink )->parseAsBlock(); - } - - return $warning; - } - - /** - * Construct a warning and a gallery from an array of duplicate files. - * @param $dupes array - * @return string - */ - public function getDupeWarning( $dupes ) { - if ( !$dupes ) { - return ''; - } - - $gallery = ImageGalleryBase::factory(); - $gallery->setContext( $this->getContext() ); - $gallery->setShowBytes( false ); - foreach ( $dupes as $file ) { - $gallery->add( $file->getTitle() ); - } - return '<li>' . - wfMessage( 'file-exists-duplicate' )->numParams( count( $dupes ) )->parse() . - $gallery->toHtml() . "</li>\n"; - } - - protected function getGroupName() { - return 'media'; - } -} - -/** - * Sub class of HTMLForm that provides the form section of SpecialUpload - */ -class UploadForm extends HTMLForm { - protected $mWatch; - protected $mForReUpload; - protected $mSessionKey; - protected $mHideIgnoreWarning; - protected $mDestWarningAck; - protected $mDestFile; - - protected $mComment; - protected $mTextTop; - protected $mTextAfterSummary; - - protected $mSourceType; - - protected $mSourceIds; - - protected $mMaxFileSize = array(); - - protected $mMaxUploadSize = array(); - - /** - * constructor: make upload form using options provided by SpecialUpload - */ - public function __construct( array $options = array(), IContextSource $context = null ) { - $this->constructData( $options, $context ); - $this->constructForm( $context ); - - # Set some form properties - $this->setSubmitText( $this->msg( 'uploadbtn' )->text() ); - $this->setSubmitName( 'wpUpload' ); - # Used message keys: 'accesskey-upload', 'tooltip-upload' - $this->setSubmitTooltip( 'upload' ); - $this->setId( 'mw-upload-form' ); - } - - /** - * Initialize member data and form descriptors using options provided by SpecialUpload - * @since 1.23 - */ - protected function constructData( array $options = array(), IContextSource $context = null ) { - # setContext is called in the constructor, but it's needed - # before we get there - $this->setContext( $context ); - - $this->mWatch = !empty( $options['watch'] ); - $this->mForReUpload = !empty( $options['forreupload'] ); - $this->mSessionKey = isset( $options['sessionkey'] ) - ? $options['sessionkey'] : ''; - $this->mHideIgnoreWarning = !empty( $options['hideignorewarning'] ); - $this->mDestWarningAck = !empty( $options['destwarningack'] ); - $this->mDestFile = isset( $options['destfile'] ) ? $options['destfile'] : ''; - - $this->mComment = isset( $options['description'] ) ? - $options['description'] : ''; - - $this->mTextTop = isset( $options['texttop'] ) - ? $options['texttop'] : ''; - - $this->mTextAfterSummary = isset( $options['textaftersummary'] ) - ? $options['textaftersummary'] : ''; - - $this->mSourceType = isset( $options['sourcetype'] ) - ? $options['sourcetype'] : ''; - } - - protected function constructForm( IContextSource $context ) { - $sourceDescriptor = $this->getSourceSection(); - $descriptor = $sourceDescriptor - + $this->getDescriptionSection() - + $this->getOptionsSection(); - - Hooks::run( 'UploadFormInitDescriptor', array( &$descriptor ) ); - parent::__construct( $descriptor, $context, 'upload' ); - - # Build a list of IDs for javascript insertion - $this->mSourceIds = array(); - foreach ( $sourceDescriptor as $field ) { - if ( !empty( $field['id'] ) ) { - $this->mSourceIds[] = $field['id']; - } - } - } - - /** - * Get the descriptor of the fieldset that contains the file source - * selection. The section is 'source' - * - * @return Array: descriptor array - */ - protected function getSourceSection() { - global $wgCopyUploadsFromSpecialUpload; - - if ( $this->mSessionKey ) { - return array( - 'SessionKey' => array( - 'type' => 'hidden', - 'default' => $this->mSessionKey, - ), - 'SourceType' => array( - 'type' => 'hidden', - 'default' => 'Stash', - ), - ); - } - - $canUploadByUrl = UploadFromUrl::isEnabled() - && UploadFromUrl::isAllowed( $this->getUser() ) - && $wgCopyUploadsFromSpecialUpload; - $radio = $canUploadByUrl; - $selectedSourceType = strtolower( $this->mSourceType ); - - $descriptor = array(); - if ( $this->mTextTop ) { - $descriptor['UploadFormTextTop'] = array( - 'type' => 'info', - 'section' => 'source', - 'default' => $this->mTextTop, - 'raw' => true, - ); - } - - $this->mMaxUploadSize['file'] = UploadBase::getMaxUploadSize( 'file' ); - # Limit to upload_max_filesize unless we are running under HipHop and - # that setting doesn't exist - if ( ! ( ( function_exists( 'wfIsHipHop' ) and wfIsHipHop() ) - || ( function_exists( 'wfIsHHVM()' ) and wfIsHHVM() ) ) ) { - $this->mMaxUploadSize['file'] = min( $this->mMaxUploadSize['file'], - wfShorthandToInteger( ini_get( 'upload_max_filesize' ) ), - wfShorthandToInteger( ini_get( 'post_max_size' ) ) - ); - } - - $descriptor['UploadFile'] = array( - 'class' => 'UploadSourceField', - 'section' => 'source', - 'type' => 'file', - 'id' => 'wpUploadFile', - 'label-message' => 'sourcefilename', - 'upload-type' => 'File', - 'radio' => &$radio, - 'radio-name' => 'wpSourceType', - 'help' => $this->msg( 'upload-maxfilesize', - $this->getContext()->getLanguage()->formatSize( $this->mMaxUploadSize['file'] ) ) - ->parse() . - $this->msg( 'word-separator' )->escaped() . - $this->msg( 'upload_source_file' )->escaped(), - 'checked' => $selectedSourceType == 'file', - ); - - if ( $canUploadByUrl ) { - $this->mMaxUploadSize['url'] = UploadBase::getMaxUploadSize( 'url' ); - $descriptor['UploadFileURL'] = array( - 'class' => 'UploadSourceField', - 'section' => 'source', - 'id' => 'wpUploadFileURL', - 'label-message' => 'sourceurl', - 'upload-type' => 'url', - 'radio' => &$radio, - 'radio-name' => 'wpSourceType', - 'help' => $this->msg( 'upload-maxfilesize', - $this->getContext()->getLanguage()->formatSize( $this->mMaxUploadSize['url'] ) ) - ->parse() . - $this->msg( 'word-separator' )->escaped() . - $this->msg( 'upload_source_url' )->escaped(), - 'checked' => $selectedSourceType == 'url', - ); - } - Hooks::run( 'UploadFormSourceDescriptors', array( &$descriptor, &$radio, $selectedSourceType ) ); - - $descriptor['Extensions'] = array( - 'type' => 'info', - 'section' => 'source', - 'default' => $this->getExtensionsMessage(), - 'raw' => true, - ); - return $descriptor; - } - - /** - * Get the messages indicating which extensions are preferred and prohibitted. - * - * @return String: HTML string containing the message - */ - protected function getExtensionsMessage() { - # Print a list of allowed file extensions, if so configured. We ignore - # MIME type here, it's incomprehensible to most people and too long. - global $wgCheckFileExtensions, $wgStrictFileExtensions, - $wgFileExtensions, $wgFileBlacklist; - - if ( $wgCheckFileExtensions ) { - if ( $wgStrictFileExtensions ) { - # Everything not permitted is banned - $extensionsList = - '<div id="mw-upload-permitted">' . - $this->msg( 'upload-permitted', $this->getContext()->getLanguage()->commaList( array_unique( $wgFileExtensions ) ) )->parseAsBlock() . - "</div>\n"; - } else { - # We have to list both preferred and prohibited - $extensionsList = - '<div id="mw-upload-preferred">' . - $this->msg( 'upload-preferred', $this->getContext()->getLanguage()->commaList( array_unique( $wgFileExtensions ) ) )->parseAsBlock() . - "</div>\n" . - '<div id="mw-upload-prohibited">' . - $this->msg( 'upload-prohibited', $this->getContext()->getLanguage()->commaList( array_unique( $wgFileBlacklist ) ) )->parseAsBlock() . - "</div>\n"; - } - } else { - # Everything is permitted. - $extensionsList = ''; - } - return $extensionsList; - } - - /** - * Get the descriptor of the fieldset that contains the file description - * input. The section is 'description' - * - * @return Array: descriptor array - */ - protected function getDescriptionSection() { - if ( $this->mSessionKey ) { - $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash(); - try { - $file = $stash->getFile( $this->mSessionKey ); - } catch ( MWException $e ) { - $file = null; - } - if ( $file ) { - global $wgContLang; - - $mto = $file->transform( array( 'width' => 120 ) ); - $this->addHeaderText( - '<div class="thumb t' . $wgContLang->alignEnd() . '">' . - Html::element( 'img', array( - 'src' => $mto->getUrl(), - 'class' => 'thumbimage', - ) ) . '</div>', 'description' ); - } - } - - $descriptor = array( - 'DestFile' => array( - 'type' => 'text', - 'section' => 'description', - 'id' => 'wpDestFile', - 'label-message' => 'destfilename', - 'size' => 60, - 'default' => $this->mDestFile, - # @todo FIXME: Hack to work around poor handling of the 'default' option in HTMLForm - 'nodata' => strval( $this->mDestFile ) !== '', - ), - 'UploadDescription' => array( - 'type' => 'textarea', - 'section' => 'description', - 'id' => 'wpUploadDescription', - 'label-message' => $this->mForReUpload - ? 'filereuploadsummary' - : 'fileuploadsummary', - 'default' => $this->mComment, - 'cols' => 80, - 'rows' => 8, - ) - ); - if ( $this->mTextAfterSummary ) { - $descriptor['UploadFormTextAfterSummary'] = array( - 'type' => 'info', - 'section' => 'description', - 'default' => $this->mTextAfterSummary, - 'raw' => true, - ); - } - - $descriptor += array( - 'EditTools' => array( - 'type' => 'edittools', - 'section' => 'description', - 'message' => 'edittools-upload', - ) - ); - - if ( $this->mForReUpload ) { - $descriptor['DestFile']['readonly'] = true; - } else { - $descriptor['License'] = array( - 'type' => 'select', - 'class' => 'Licenses', - 'section' => 'description', - 'id' => 'wpLicense', - 'label-message' => 'license', - ); - } - - global $wgUseCopyrightUpload; - if ( $wgUseCopyrightUpload ) { - $descriptor['UploadCopyStatus'] = array( - 'type' => 'text', - 'section' => 'description', - 'id' => 'wpUploadCopyStatus', - 'label-message' => 'filestatus', - ); - $descriptor['UploadSource'] = array( - 'type' => 'text', - 'section' => 'description', - 'id' => 'wpUploadSource', - 'label-message' => 'filesource', - ); - } - - return $descriptor; - } - - /** - * Get the descriptor of the fieldset that contains the upload options, - * such as "watch this file". The section is 'options' - * - * @return Array: descriptor array - */ - protected function getOptionsSection() { - $user = $this->getUser(); - if ( $user->isLoggedIn() ) { - $descriptor = array( - 'Watchthis' => array( - 'type' => 'check', - 'id' => 'wpWatchthis', - 'label-message' => 'watchthisupload', - 'section' => 'options', - 'default' => $this->mWatch, - ) - ); - } - if ( !$this->mHideIgnoreWarning ) { - $descriptor['IgnoreWarning'] = array( - 'type' => 'check', - 'id' => 'wpIgnoreWarning', - 'label-message' => 'ignorewarnings', - 'section' => 'options', - ); - } - - $descriptor['DestFileWarningAck'] = array( - 'type' => 'hidden', - 'id' => 'wpDestFileWarningAck', - 'default' => $this->mDestWarningAck ? '1' : '', - ); - - if ( $this->mForReUpload ) { - $descriptor['ForReUpload'] = array( - 'type' => 'hidden', - 'id' => 'wpForReUpload', - 'default' => '1', - ); - } - - return $descriptor; - } - - /** - * Add the upload JS and show the form. - */ - public function show() { - $this->addUploadJs(); - parent::show(); - } - - /** - * Add upload JS to the OutputPage - */ - protected function addUploadJs() { - $out = $this->getOutput(); - $this->addJsConfigVars( $out ); - $this->addRLModules( $out ); - } - - protected function addJsConfigVars( $out ) { - global $wgUseAjax, $wgAjaxUploadDestCheck, $wgAjaxLicensePreview, $wgEnableAPI, $wgStrictFileExtensions; - - $useAjaxDestCheck = $wgUseAjax && $wgAjaxUploadDestCheck; - $useAjaxLicensePreview = $wgUseAjax && $wgAjaxLicensePreview && $wgEnableAPI; - $this->mMaxUploadSize['*'] = UploadBase::getMaxUploadSize(); - - $scriptVars = array( - 'wgAjaxUploadDestCheck' => $useAjaxDestCheck, - 'wgAjaxLicensePreview' => $useAjaxLicensePreview, - 'wgUploadAutoFill' => !$this->mForReUpload && - // If we received mDestFile from the request, don't autofill - // the wpDestFile textbox - $this->mDestFile === '', - 'wgUploadSourceIds' => $this->mSourceIds, - 'wgStrictFileExtensions' => $wgStrictFileExtensions, - 'wgCapitalizeUploads' => MWNamespace::isCapitalized( NS_FILE ), - 'wgMaxUploadSize' => $this->mMaxUploadSize, - ); - - $out->addJsConfigVars( $scriptVars ); - } - - protected function addRLModules( $out ) { - $out->addModules( array( - 'mediawiki.action.edit', // For <charinsert> support - 'mediawiki.legacy.upload', // Old form stuff... - 'mediawiki.special.upload', // Newer extras for thumbnail preview. - ) ); - } - - /** - * Empty function; submission is handled elsewhere. - * - * @return bool false - */ - function trySubmit() { - return false; - } - -} - -/** - * A form field that contains a radio box in the label - */ -class UploadSourceField extends HTMLTextField { - - /** - * @param $cellAttributes array - * @return string - */ - function getLabelHtml( $cellAttributes = array() ) { - if ( !empty( $this->mParams['radio'] ) ) { - $id = "wpSourceType{$this->mParams['upload-type']}"; - $attribs = array( - 'name' => $this->mParams['radio-name'], - 'type' => 'radio', - 'id' => $this->mParams['radio-name'] . $this->mParams['upload-type'], - 'value' => $this->mParams['upload-type'], - ); - if ( !empty( $this->mParams['checked'] ) ) { - $attribs['checked'] = 'checked'; - } - $label = Html::rawElement( 'label', array( 'for' => $id ), $this->mLabel ); - $label .= Html::element( 'input', $attribs ); - } else { - $id = $this->mParams['id']; - $label = Html::rawElement( 'label', array( 'for' => $id ), $this->mLabel ); - } - return Html::rawElement( 'td', array( 'class' => 'mw-label' ) + $cellAttributes, $label ); - } - - /** - * @return int - */ - function getSize() { - return isset( $this->mParams['size'] ) - ? $this->mParams['size'] - : 60; - } -} diff --git a/extension.json b/extension.json index f2cc82d..db561bb 100644 --- a/extension.json +++ b/extension.json @@ -1,6 +1,6 @@ { "name": "MultiUpload", - "version": "3.0", + "version": "3.1-SW", "author": [ "Travis Derouin", "Lee Worden" @@ -75,6 +75,7 @@ "mw.FormDataTransport.js" ], "dependencies": [ + "jquery.client", "mediawiki.api", "ext.multiupload.top" ], diff --git a/resources/ext.multiupload.shared.js b/resources/ext.multiupload.shared.js index c7a12f0..661601f 100644 --- a/resources/ext.multiupload.shared.js +++ b/resources/ext.multiupload.shared.js @@ -140,7 +140,15 @@ } $fieldset.addClass( 'row' ); $fieldset.data( 'row-index', i ); - if ( $.isEmpty( $fieldset.children( 'div.row' ) ) ) { + if ( + $fieldset.children( 'div.row' ) === '' || + $fieldset.children( 'div.row' ) === 0 || + $fieldset.children( 'div.row' ) === "0" || + $fieldset.children( 'div.row' ) === null || + $fieldset.children( 'div.row' ) === false || + $fieldset.children( 'div.row' ) === undefined + ) + { $( '<div>' ).addClass( 'row' ) .append( $fieldset.children().not( 'legend' ) ) .appendTo( $fieldset ); @@ -189,7 +197,11 @@ // give it a close button if it doesn't have one var $cb = $fieldset.find( '.multiupload-close-button-container' ); - if ( $.isEmpty( $cb ) ) { + if ( + $cb === '' || $cb === 0 || $cb === "0" || $cb === null || + $cb === false || $cb === undefined + ) + { $fieldset.children( 'div.row' ).prepend( $( '<div/>' ).addClass( 'multiupload-close-button-container' ).append( $( '<span/>' ) @@ -271,10 +283,19 @@ // append the row after the existing rows. $fieldset.hide(); // TODO: make the animation happen sometime after this returns - if ( !$lastfs || $.isEmpty( $lastfs ) ) { + if ( + !$lastfs || + $lastfs === '' || $lastfs === 0 || $lastfs === "0" || $lastfs === null || + $lastfs === false || $lastfs === undefined + ) + { $lastfs = mw.libs.ext.multiupload.findLastRow(); } - if ( $.isEmpty( $lastfs ) ) { + if ( + $lastfs === '' || $lastfs === 0 || $lastfs === "0" || $lastfs === null || + $lastfs === false || $lastfs === undefined + ) + { $( 'form#mw-upload-form' ).prepend( $fieldset ); } else { $fieldset.insertAfter( $lastfs ); diff --git a/resources/ext.multiupload.unpack.js b/resources/ext.multiupload.unpack.js index 61e5cd8..de007b0 100644 --- a/resources/ext.multiupload.unpack.js +++ b/resources/ext.multiupload.unpack.js @@ -1,30 +1,6 @@ /* global $, mw */ ( function ( $, mw ) { -function notify( message ) { - // message could be a jQuery object, or just a string. - try { - // if wiki is new enough to have mw.notify(), use it. - mw.loader.using( [ 'mediawiki.notification', 'mediawiki.notify' ], function() { - mw.notify( message ); - } ); - } catch ( e ) { - // otherwise, use dialog(). - mw.loader.using( 'jquery.ui.dialog', function() { - var dialogOpts = { - buttons: [ { - text: mw.message( 'multiupload-notify-ok' ).plain(), - click: function () { - $( this ).dialog( 'close' ); - } - } ] - }; - $( '<div/>' ).append( message ).dialog( dialogOpts ); - // TODO make it stretch to width of message - } ); - } -} - function apiErr( code, result, message ) { var $errdiv = $( '<div/>' ).append( message ); var errtxt = code; @@ -41,7 +17,9 @@ if ( result && result.error && result.error.messages ) { $errdiv.append( '<br/>' + result.error.messages ); } - notify( $errdiv ); + mw.loader.using( [ 'mediawiki.notification', 'mediawiki.notify' ], function() { + mw.notify( $errdiv ); + } ); } function unpackOnServer( input, sessionkey, spinnerName ) { @@ -50,20 +28,19 @@ action: 'multiupload-unpack', key: sessionkey, filename: input.files[0].name - }, { - ok: function ( data ) { - mw.libs.ext.multiupload.removeTinySpinner( spinnerName ); - if ( 'multiupload-unpack' in data && 'contents' in data['multiupload-unpack'] ) { - reloadForm( input, data['multiupload-unpack'].contents ); - } else { - // TODO: if invalid token, get a new one and redo - notify( 'Error unpacking ' + input.files[0].name ); - } - }, - err: function ( code, result ) { - mw.libs.ext.multiupload.removeTinySpinner( spinnerName ); - apiErr( code, result, mw.message( 'multiupload-unpack-error' ).parse() ); + } ).done( function ( data ) { + mw.libs.ext.multiupload.removeTinySpinner( spinnerName ); + if ( 'multiupload-unpack' in data && 'contents' in data['multiupload-unpack'] ) { + reloadForm( input, data['multiupload-unpack'].contents ); + } else { + // TODO: if invalid token, get a new one and redo + mw.loader.using( [ 'mediawiki.notification', 'mediawiki.notify' ], function() { + mw.notify( 'Error unpacking ' + input.files[0].name ); + } ); } + } ).fail( function ( code, result ) { + mw.libs.ext.multiupload.removeTinySpinner( spinnerName ); + apiErr( code, result, mw.message( 'multiupload-unpack-error' ).parse() ); } ); } ); } @@ -118,7 +95,7 @@ } var enableChunked = ( mw.config.get( 'wgVersion' ).match( /^1\.2[0-9]\./ ) ? true : false ); -$.extend( mw.UploadWizard.config, { +$.extend( mw.UploadWizard.config, { chunkSize: 5 * 1024 * 1024, enableChunked: enableChunked, maxPhpUploadSize: mw.config.get( 'wgMultiUploadMaxPhpUploadSize' ) diff --git a/resources/mediawiki.special.upload.js.patched.1.21.3 b/resources/mediawiki.special.upload.js.patched.1.21.3 deleted file mode 100644 index 169f36b..0000000 --- a/resources/mediawiki.special.upload.js.patched.1.21.3 +++ /dev/null @@ -1,379 +0,0 @@ -/** - * JavaScript for Special:Upload - * Note that additional code still lives in skins/common/upload.js - */ -( function ( mw, $ ) { - /** - * Add a preview to the upload form - */ - $( document ).ready( function () { - /** - * Is the FileAPI available with sufficient functionality? - */ - function hasFileAPI() { - return window.FileReader !== undefined; - } - - /** - * Check if this is a recognizable image type... - * Also excludes files over 10M to avoid going insane on memory usage. - * - * @todo is there a way we can ask the browser what's supported in <img>s? - * @todo put SVG back after working around Firefox 7 bug <https://bugzilla.wikimedia.org/show_bug.cgi?id=31643> - * - * @param {File} file - * @return boolean - */ - function fileIsPreviewable( file ) { - var known = ['image/png', 'image/gif', 'image/jpeg', 'image/svg+xml'], - tooHuge = 10 * 1024 * 1024; - return ( $.inArray( file.type, known ) !== -1 ) && file.size > 0 && file.size < tooHuge; - } - - /** - * Show a thumbnail preview of PNG, JPEG, GIF, and SVG files prior to upload - * in browsers supporting HTML5 FileAPI. - * - * As of this writing, known good: - * - Firefox 3.6+ - * - Chrome 7.something - * - * tableId and thumbnailId parameters must be properly escaped - * HTML id attribute values. - * - * @todo check file size limits and warn of likely failures - * - * @param {File} file - * @param {string} tableId ID of table element above which to insert the thumbnail - * @param {string} thumbnailId ID to be given to thumbnail element - */ - function showPreview( file, tableId, thumbnailId ) { - var $canvas, - ctx, - meta, - previewSize = 180, - $thumb = $( '<div id="' + thumbnailId + '" class="thumb tright">' + - '<div class="thumbinner">' + - '<div class="mw-small-spinner" style="width: 180px; height: 180px"></div>' + - '<div class="thumbcaption"><div class="filename"></div><div class="fileinfo"></div></div>' + - '</div>' + - '</div>' ); - - $thumb.find( '.filename' ).text( file.name ).end() - .find( '.fileinfo' ).text( prettySize( file.size ) ).end(); - - $canvas = $('<canvas width="' + previewSize + '" height="' + previewSize + '" ></canvas>'); - ctx = $canvas[0].getContext( '2d' ); - $( '#' + tableId ).parent().prepend( $thumb ); - - fetchPreview( file, function ( dataURL ) { - var img = new Image(), - rotation = 0; - - if ( meta && meta.tiff && meta.tiff.Orientation ) { - rotation = ( 360 - ( function () { - // See includes/media/Bitmap.php - switch ( meta.tiff.Orientation.value ) { - case 8: - return 90; - case 3: - return 180; - case 6: - return 270; - default: - return 0; - } - }() ) ) % 360; - } - - img.onload = function () { - var info, width, height, x, y, dx, dy, logicalWidth, logicalHeight; - - // Fit the image within the previewSizexpreviewSize box - if ( img.width > img.height ) { - width = previewSize; - height = img.height / img.width * previewSize; - } else { - height = previewSize; - width = img.width / img.height * previewSize; - } - // Determine the offset required to center the image - dx = (180 - width) / 2; - dy = (180 - height) / 2; - switch ( rotation ) { - // If a rotation is applied, the direction of the axis - // changes as well. You can derive the values below by - // drawing on paper an axis system, rotate it and see - // where the positive axis direction is - case 0: - x = dx; - y = dy; - logicalWidth = img.width; - logicalHeight = img.height; - break; - case 90: - - x = dx; - y = dy - previewSize; - logicalWidth = img.height; - logicalHeight = img.width; - break; - case 180: - x = dx - previewSize; - y = dy - previewSize; - logicalWidth = img.width; - logicalHeight = img.height; - break; - case 270: - x = dx - previewSize; - y = dy; - logicalWidth = img.height; - logicalHeight = img.width; - break; - } - - ctx.clearRect( 0, 0, 180, 180 ); - ctx.rotate( rotation / 180 * Math.PI ); - ctx.drawImage( img, x, y, width, height ); - $thumb.find('.mw-small-spinner').replaceWith($canvas); - - // Image size - info = mw.msg( 'widthheight', logicalWidth, logicalHeight ) + - ', ' + prettySize( file.size ); - - $( '#' + thumbnailId + ' .fileinfo' ).text( info ); - }; - img.src = dataURL; - }, mw.config.get( 'wgFileCanRotate' ) ? function ( data ) { - /*jshint camelcase: false, nomen: false */ - try { - meta = mw.libs.jpegmeta( data, file.fileName ); - meta._binary_data = null; - } catch ( e ) { - meta = null; - } - } : null ); - } - - /** - * Start loading a file into memory; when complete, pass it as a - * data URL to the callback function. If the callbackBinary is set it will - * first be read as binary and afterwards as data URL. Useful if you want - * to do preprocessing on the binary data first. - * - * @param {File} file - * @param {function} callback - * @param {function} callbackBinary - */ - function fetchPreview( file, callback, callbackBinary ) { - var reader = new FileReader(); - if ( callbackBinary && 'readAsBinaryString' in reader ) { - // To fetch JPEG metadata we need a binary string; start there. - // todo: - reader.onload = function () { - callbackBinary( reader.result ); - - // Now run back through the regular code path. - fetchPreview( file, callback ); - }; - reader.readAsBinaryString( file ); - } else if ( callbackBinary && 'readAsArrayBuffer' in reader ) { - // readAsArrayBuffer replaces readAsBinaryString - // However, our JPEG metadata library wants a string. - // So, this is going to be an ugly conversion. - reader.onload = function () { - var i, - buffer = new Uint8Array( reader.result ), - string = ''; - for ( i = 0; i < buffer.byteLength; i++ ) { - string += String.fromCharCode( buffer[i] ); - } - callbackBinary( string ); - - // Now run back through the regular code path. - fetchPreview( file, callback ); - }; - reader.readAsArrayBuffer( file ); - } else if ( 'URL' in window && 'createObjectURL' in window.URL ) { - // Supported in Firefox 4.0 and above <https://developer.mozilla.org/en/DOM/window.URL.createObjectURL> - // WebKit has it in a namespace for now but that's ok. ;) - // - // Lifetime of this URL is until document close, which is fine - // for Special:Upload -- if this code gets used on longer-running - // pages, add a revokeObjectURL() when it's no longer needed. - // - // Prefer this over readAsDataURL for Firefox 7 due to bug reading - // some SVG files from data URIs <https://bugzilla.mozilla.org/show_bug.cgi?id=694165> - callback( window.URL.createObjectURL( file ) ); - } else { - // This ends up decoding the file to base-64 and back again, which - // feels horribly inefficient. - reader.onload = function () { - callback( reader.result ); - }; - reader.readAsDataURL( file ); - } - } - - /** - * Format a file size attractively. - * @todo match numeric formatting - * - * @param {number} s - * @return string - */ - function prettySize( s ) { - var sizeMsgs = ['size-bytes', 'size-kilobytes', 'size-megabytes', 'size-gigabytes']; - while ( s >= 1024 && sizeMsgs.length > 1 ) { - s /= 1024; - sizeMsgs = sizeMsgs.slice( 1 ); - } - return mw.msg( sizeMsgs[0], Math.round( s ) ); - } - - /** - * Clear the file upload preview area. - * - * thumbnailId parameter must be a properly escaped - * HTML id attribute value. - * - * @param {string} thumbnailId ID of thumbnail element to be removed - */ - function clearPreview( thumbnailId ) { - $( '#' + thumbnailId ).remove(); - } - - /** - * Check if the file does not exceed the maximum size - * - * fileId and errorId parameters must be properly escaped - * HTML id attribute values. - * - * @param {File} file - * @param {string} fileId ID of upload form field - * @param {string} errorId ID of element to contain error output - */ - function checkMaxUploadSize( file, fileId, errorId ) { - var maxSize, $error; - - function getMaxUploadSize( type ) { - var sizes = mw.config.get( 'wgMaxUploadSize' ); - - if ( sizes[type] !== undefined ) { - return sizes[type]; - } - return sizes['*']; - } - - $( '.' + errorId ).remove(); - - maxSize = getMaxUploadSize( 'file' ); - if ( file.size > maxSize ) { - $error = $( '<p class="error mw-upload-source-error" id="' + errorId + '">' + - mw.message( 'largefileserver', file.size, maxSize ).escaped() + '</p>' ); - - $( '#' + fileId ).after( $error ); - - return false; - } - - return true; - } - - /** - * Initialization - * - * All parameters must be properly escaped - * HTML id attribute values. - * - * @param {string} fileId ID of upload field - * @param {string} thumbnailId ID of thumbnail element - * @param {string} errorId ID of element where error output will appear - * @param {string} tableId ID of table element above which thumbnail will be added - */ - window.setupThumbnail = function ( fileId, thumbnailId, errorId, tableId ) { - if ( hasFileAPI() ) { - // Update thumbnail when the file selection control is updated. - $( '#' + fileId ) - .change( function () { - clearPreview( thumbnailId ); - if ( this.files && this.files.length ) { - // Note: would need to be updated to handle multiple files. - var file = this.files[0]; - - if ( ! checkMaxUploadSize( - file, - fileId, - errorId - ) ) { - return; - } - - if ( fileIsPreviewable( file ) ) { - showPreview( - file, - tableId, - thumbnailId - ); - } - } - } ) - .change(); - } - }; - window.setupThumbnail( - 'wpUploadFile', - 'mw-upload-thumbnail', - 'wpSourceTypeFile-error', - 'mw-htmlform-source' - ); - } ); - - /** - * When there are multiple source fields, allow only one enabled at once - * - * Parameters errorId and radioName must be appropriately escaped for - * insertion directly into jQuery selector strings. - * - * @param rows selected table rows within which to operate - * @param {string} errorId ID of element where source errors go - * @param {string} radioName name of radio button selecting source fields - */ - window.setupSourceFields = function ( $rows, errorId, radioName ) { - var i, $row; - - function createHandler( $currentRow ) { - /** - * @param {jQuery.Event} - */ - return function () { - $( '#' + errorId ).remove(); - if ( this.checked ) { - // Disable all inputs - $rows.find( 'input[name!="' + radioName + '"]' ).prop( 'disabled', true ); - // Re-enable the current one - $currentRow.find( 'input' ).prop( 'disabled', false ); - } - }; - } - - for ( i = $rows.length; i; i-- ) { - $row = $rows.eq(i - 1); - $row - .find( 'input[name="' + radioName + '"]' ) - .change( createHandler( $row ) ); - } - }; - - /** - * Disable all upload source fields except the selected one - */ - $( document ).ready( function() { - window.setupSourceFields( - $( '.mw-htmlform-field-UploadSourceField' ), - 'wpSourceTypeFile-error', - 'wpSourceType' - ); - } ); - -}( mediaWiki, jQuery ) ); diff --git a/resources/mw.FormDataTransport.js b/resources/mw.FormDataTransport.js index 0730974..b3aafc2 100644 --- a/resources/mw.FormDataTransport.js +++ b/resources/mw.FormDataTransport.js @@ -15,6 +15,8 @@ mw.FormDataTransport = function( postUrl, formData, uploadObject, progressCb, transportedCb ) { + var profile = $.client.profile(); + this.formData = formData; this.progressCb = progressCb; this.transportedCb = transportedCb; @@ -32,7 +34,12 @@ // Workaround for Firefox < 7.0 sending an empty string // as filename for Blobs in FormData requests, something PHP does not like // https://bugzilla.mozilla.org/show_bug.cgi?id=649150 - this.gecko = $.browser.mozilla && $.browser.version < '7.0'; + // From version 7.0 to 22.0, Firefox sends "blob" as the file name + // which seems to be accepted by the server + // https://bugzilla.mozilla.org/show_bug.cgi?id=690659 + // https://developer.mozilla.org/en-US/docs/Web/API/FormData#Browser_compatibility + + this.insufficientFormDataSupport = profile.name === 'firefox' && profile.versionNumber < 7; }; mw.FormDataTransport.prototype = { @@ -174,7 +181,7 @@ transport.parseResponse(evt, transport.transportedCb); }, false); - if(this.gecko) { + if(this.insufficientFormDataSupport) { formData = this.geckoFormData(); } else { formData = new FormData(); @@ -196,13 +203,13 @@ formData.append('filekey', this.filekey); } formData.append('filesize', bytesAvailable); - if(this.gecko) { + if(this.insufficientFormDataSupport) { formData.appendBlob('chunk', chunk, 'chunk.bin'); } else { formData.append('chunk', chunk); } this.xhr.open('POST', this.postUrl, true); - if(this.gecko) { + if(this.insufficientFormDataSupport) { formData.send(this.xhr); } else { this.xhr.send(formData); diff --git a/resources/upload.js.patched.1.21.3 b/resources/upload.js.patched.1.21.3 deleted file mode 100644 index 231054c..0000000 --- a/resources/upload.js.patched.1.21.3 +++ /dev/null @@ -1,518 +0,0 @@ -( function ( mw, $ ) { -var licenseSelectorCheck, wgUploadWarningObj, wgUploadLicenseObj, fillDestFilename, - ajaxUploadDestCheck = mw.config.get( 'wgAjaxUploadDestCheck' ), - fileExtensions = mw.config.get( 'wgFileExtensions' ); - -/** - * onchange function to show a preview when license selector is changed - * - * All parameters must be properly escaped - * HTML id attribute values. - * - * @param {string} licenseId ID of license selector field - * @param {string} destFileId ID of destination filename field - * @param {string} previewId ID of element where license preview goes - */ -licenseSelectorCheck = window.licenseSelectorCheck = function( licenseId, destFileId, previewId ) { - return function() { - var selector = document.getElementById( licenseId ), - selection = selector.options[selector.selectedIndex].value; - if( selector.selectedIndex > 0 ) { - if ( !selection ) { - // Option disabled, but browser is broken and doesn't respect this - selector.selectedIndex = 0; - } - } - // We might show a preview - wgUploadLicenseObj.fetchPreview( selection, licenseId, destFileId, previewId ); - }; -}; - -/** - * Insert event handlers, empty elements, generally get form ready for - * the dynamics operations we're going to do when data is entered. - * - * All parameters must be properly escaped - * HTML id attribute values. - * - * @param {string} sourceTypeUrlId ID of the radio button for the copy-from-URL field - * @param {string} uploadUrlId ID of copy-from-URL text field - * @param {string} licenseId ID of license selector field - * @param {string} warningId ID to attach to table row where warnings will go - * @param {string} ackId Id of hidden field recording that warning was acknowledged - * @param {string} destFileId ID of destination filename field - * @param {string} descriptionId unused - * @param {string} previewId ID of element where license preview goes - */ -/** TODO: move away from using IDs as args? */ -window.uploadSetupByIds = function( sourceTypeUrlId, uploadUrlId, licenseId, warningId, ackId, destFileId, descriptionId, previewId ) { - // Disable URL box if the URL copy upload source type is not selected - var ein, - selector, ua, isMacIe, i, - destElt, destFileRow, destFileTbody, row, td, ackElt, - wpLicense, wpLicenseRow, wpLicenseTbody, - e = document.getElementById( sourceTypeUrlId ); - if ( e ) { - if ( !e.checked ) { - ein = document.getElementById( uploadUrlId ); - if ( ein ) { - ein.disabled = true; - } - } - } - - // For MSIE/Mac: non-breaking spaces cause the <option> not to render. - // But for some reason, setting the text to itself works - selector = document.getElementById( licenseId ); - if ( selector ) { - ua = navigator.userAgent; - isMacIe = ua.indexOf( 'MSIE' ) !== -1 && ua.indexOf( 'Mac' ) !== -1; - if ( isMacIe ) { - for ( i = 0; i < selector.options.length; i++ ) { - selector.options[i].text = selector.options[i].text; - } - } - } - - // AJAX wpDestFile warnings - if ( ajaxUploadDestCheck ) { - // Insert event handlers to fetch upload warnings when wpDestFile - // has been changed - destElt = document.getElementById( destFileId ); - if ( destElt ) { - ackElt = document.getElementsByName( ackId ); - destElt.onchange = function () { - wgUploadWarningObj.checkNow( this, warningId, ackElt ); - }; - destElt.onkeyup = function () { - wgUploadWarningObj.keypress( this, warningId, ackElt ); - }; - if ( ! document.getElementById( warningId ) ) { - // Insert a row where the warnings will be displayed just below the - // wpDestFile row - destFileRow = destElt.parentNode.parentNode; - destFileTbody = destFileRow.parentNode; - - row = document.createElement( 'tr' ); - td = document.createElement( 'td' ); - td.id = warningId; - td.colSpan = 2; - - row.appendChild( td ); - destFileTbody.insertBefore( row, destFileRow.nextSibling ); - } - } - } - - wpLicense = document.getElementById( licenseId ); - if ( mw.config.get( 'wgAjaxLicensePreview' ) && wpLicense ) { - if ( ! document.getElementById( previewId ) ) { - // License selector table row - var wpLicenseRow = wpLicense.parentNode.parentNode; - var wpLicenseTbody = wpLicenseRow.parentNode; - - var row = document.createElement( 'tr' ); - var td = document.createElement( 'td' ); - row.appendChild( td ); - td = document.createElement( 'td' ); - td.id = previewId; - row.appendChild( td ); - - wpLicenseTbody.insertBefore( row, wpLicenseRow.nextSibling ); - } - - // License selector check - wpLicense.onchange = licenseSelectorCheck( licenseId, destFileId, previewId ); - // Something might already be selected - wpLicense.onchange(); - } -}; - -/** - * call uploadSetupByIds and do a bit of extra setup - */ -function uploadSetup() { - window.uploadSetupByIds( 'wpSourceTypeurl', 'wpUploadFileURL', 'wpLicense', 'wpDestFile-warning', - 'wpDestFileWarningAck', 'wpDestFile', 'mw-htmlform-description', 'mw-license-preview' ); - - // fillDestFile setup - var uploadSourceIds = mw.config.get( 'wgUploadSourceIds' ), - i, uploadElt, - len = Array.isArray(uploadSourceIds) ? uploadSourceIds.length : 0, - upUrl = document.getElementById( 'wpUploadFileURL' ), - destFile = document.getElementById( 'wpDestFile' ), - upperm = document.getElementById( 'mw-upload-permitted' ), - uppro = document.getElementById( 'mw-upload-prohibited' ), - warningId = 'wpDestFile-warning', - ackElt = document.getElementsByName( 'wpDestFileWarningAck' ), - uploadChange = function () { - fillDestFilename( this, upUrl, destFile, upperm, uppro, warningId, ackElt, 'wgUploadAutoFill' ); - }; - for ( i = 0; i < len; i += 1 ) { - uploadElt = document.getElementById( uploadSourceIds[i] ); - uploadElt.onchange = uploadChange; - uploadElt.onchange(); - } -} - -/** - * object to manage Ajax call comparing destination filename to existing - * file pages - */ -wgUploadWarningObj = window.wgUploadWarningObj = { - responseCache: { '' : '' }, - delay: 500, // ms - timeoutID: false, - - /* - * keypress handler for destination filename field. - * set a timer to check the destination against existing File: - * pages when the uploader stops typing. - * - * warningId parameter must be a properly escaped - * HTML id attribute value. - * - * @param {HTMLElement} destFile destination filename field - * @param {string} warningId ID of element where warning will go - * @param {HTMLElement} ackElt hidden field element for warning acknowledgement - */ - keypress: function ( destFile, warningId, ackElt ) { - var cached; - - if ( !ajaxUploadDestCheck || !sajax_init_object() ) return; - - // Clear timer - if ( this.timeoutID ) { - window.clearTimeout( this.timeoutID ); - } - // Check response cache - for ( cached in this.responseCache ) { - if ( cached === destFile.value ) { - this.setWarning( this.responseCache[cached], warningId, ackElt ); - return; - } - } - - this.timeoutID = setTimeout( function () { - wgUploadWarningObj.timeout( destFile, warningId, ackElt ); - }, this.delay ); - }, - - /** - * Initiate an Ajax check of the destination filename - * - * warningId parameter must be a properly escaped - * HTML id attribute value. - * - * @param {HTMLElement} field destination filename field - * @param {string} warningId ID of element where warning will go - * @param {HTMLElement} ackElt hidden field element for warning acknowledgement - */ - checkNow: function ( field, warningId, ackElt ) { - var cached; - if ( !ajaxUploadDestCheck ) { - return; - } - if ( this.timeoutID ) { - window.clearTimeout( this.timeoutID ); - } - for ( cached in this.responseCache ) { - if ( cached === field.value ) { - this.setWarning( this.responseCache[cached], warningId, ackElt ); - return; - } - } - this.timeout( field, warningId, ackElt ); - }, - - /** - * Start spinner graphic and make Ajax call - * - * warningId parameter must be a properly escaped - * HTML id attribute value. - * - * @param {HTMLElement} field destination filename field - * @param {string} warningId ID of element where warning will go - * @param {HTMLElement} ackElt hidden field element for warning acknowledgement - */ - timeout: function ( field, warningId, ackElt ) { - var filename = field.value; - - if ( !ajaxUploadDestCheck || !sajax_init_object() || filename === '' ) { - return; - } - - $.removeSpinner( warningId ); - $( field ).injectSpinner( warningId ); - sajax_do_call( - 'SpecialUpload::ajaxGetExistsWarning', - [filename], - function ( result ) { - wgUploadWarningObj.processResult( result.responseText, filename, warningId, ackElt ); - } - ); - }, - - /** - * Ajax callback function - * - * warningId parameter must be a properly escaped - * HTML id attribute value. - * - * @param {string} resulthtml message returned by Ajax call - * @param {string} filename the filename being checked - * @param {string} warningId ID of element where warning will go - * @param {HTMLElement} ackElt hidden field element for warning acknowledgement - */ - processResult: function ( resulthtml, filename, warningId, ackElt ) { - $.removeSpinner( warningId ); - this.setWarning( resulthtml, warningId, ackElt ); - this.responseCache[filename] = resulthtml; - }, - - /** - * Put the warning into the page - * - * warningId parameter must be a properly escaped - * HTML id attribute value. - * - * @param {string} warning HTML text of warning - * @param {string} warningId ID of element where warning will go - * @param {HTMLElement} ackElt hidden field element for warning acknowledgement - */ - setWarning: function ( warning, warningId, ackElt ) { - this.setInnerHTML( warningId, warning ); - - // Set a value in the form indicating that the warning is acknowledged and - // doesn't need to be redisplayed post-upload - if ( warning == '' || warning == ' ' ) { - ackElt[0].value = ''; - } else { - ackElt[0].value = '1'; - } - - }, - - /** - * Put message into element - * - * id parameter must be a properly escaped - * HTML id attribute value. - * - * @param {string} id ID of element where message will go - * @param {string} text HTML text of message - */ - setInnerHTML: function ( id, text ) { - var element = document.getElementById( id ); - // Check for no change to avoid flicker in IE 7 - if (element.innerHTML != text) { - element.innerHTML = text; - } - } -}; - -/** - * Autofill the destination filename with a guess built from the source - * filename. This is called when either the source-file or source-url is - * updated. - * - * warningId parameter must be a properly escaped HTML id attribute value. - * - * @param {HTMLElement} upFile source-file element - * @param {HTMLElement} upUrl source-url element - * @param {HTMLElement} destFile destination-filename element - * @param {HTMLElement} upperm element where upload permissions errors go - * @param {HTMLElement} uppro element where upload-prohibited messages go - * @param {string} warningId Id of element where upload warnings will go - * @param {HTMLElement} ackElt hidden form field recording acknowledgement of warnings - * @param {string} configvar name of mw.config variable controlling whether to do auto-filling - */ -fillDestFilename = window.fillDestFilename = - function ( upFile, upUrl, destFile, upperm, uppro, warningId, ackElt, configvar ) { - var path, slash, backslash, fname, found, ext, i; - - // e.g. mw.config.get( 'wgUploadAutoFill' ) - if ( !mw.config.get( configvar ) ) { - return; - } - if (!document.getElementById) { - return; - } - // Remove any previously flagged errors - if ( upperm ) { - upperm.className = ''; - } - if ( uppro ) { - uppro.className = ''; - } - - path = upFile.value; - // Find trailing part - var slash = path.lastIndexOf('/'); - var backslash = path.lastIndexOf('\\'); - var fname; - if (slash == -1 && backslash == -1) { - fname = path; - } else if (slash > backslash) { - fname = path.substring(slash+1, 10000); - } else { - fname = path.substring(backslash+1, 10000); - } - - // Clear the filename if it does not have a valid extension. - // URLs are less likely to have a useful extension, so don't include them in the - // extension check. - if ( upUrl && mw.config.get( 'wgStrictFileExtensions' ) && fileExtensions && upFile.id !== upUrl.id ) { - found = false; - if ( fname.lastIndexOf( '.' ) !== -1 ) { - var ext = fname.substr( fname.lastIndexOf( '.' ) + 1 ); - for ( var i = 0; i < fileExtensions.length; i += 1 ) { - if ( fileExtensions[i].toLowerCase() === ext.toLowerCase() ) { - found = true; - break; - } - } - } - if( !found ) { - // Not a valid extension - // Clear the upload and set mw-upload-permitted to error - upFile.value = ''; - if ( upperm ) { - upperm.className = 'error'; - } - if ( uppro ) { - uppro.className = 'error'; - } - // Clear wpDestFile as well - if ( destFile ) { - destFile.value = ''; - } - return false; - } - } - - // Replace spaces by underscores - fname = fname.replace( / /g, '_' ); - // Capitalise first letter if needed - if ( mw.config.get( 'wgCapitalizeUploads' ) ) { - fname = fname.charAt( 0 ).toUpperCase().concat( fname.substring( 1, 10000 ) ); - } - - // Output result - if ( destFile ) { - // Call decodeURIComponent function to remove possible URL-encoded characters - // from the file name (bug 30390). Especially likely with upload-form-url. - // decodeURIComponent can throw an exception if input is invalid utf-8 - try { - destFile.value = decodeURIComponent( fname ); - } catch ( e ) { - destFile.value = fname; - } - destFile.onchange(); - } -}; - -window.toggleFilenameFiller = function() { - if(!document.getElementById) return; - var upfield = document.getElementById('wpUploadFile'); - var destName = document.getElementById('wpDestFile').value; - wgUploadAutoFill = ( destName == '' || destName == ' ' ); -}; - -/** - * object to manage Ajax fetching of license text - */ -wgUploadLicenseObj = window.wgUploadLicenseObj = { - - 'responseCache' : { '' : '' }, - - /** - * Initiate an Ajax request for license text to preview - * - * licenseId, destFileId and previewId parameters must be - * properly escaped HTML id attribute values. - * - * @param {string} license value of license selector - * @param {string} licenseId ID of license selector - * @param {string} destFileId ID of destination filename field - * @param {string} previewId ID of element where text will go - */ - fetchPreview: function ( license, licenseId, destFileId, previewId ) { - var cached, title; - if ( !mw.config.get( 'wgAjaxLicensePreview' ) ) { - return; - } - for ( cached in this.responseCache ) { - if ( cached === license ) { - this.showPreview( this.responseCache[license], previewId ); - return; - } - } - $( '#' + licenseId ).injectSpinner( previewId ); - - title = document.getElementById( destFileId ).value; - if ( !title ) { - title = 'File:Sample.jpg'; - } - - var url = mw.util.wikiScript( 'api' ) - + '?action=parse&text={{' + encodeURIComponent( license ) + '}}' - + '&title=' + encodeURIComponent( title ) - + '&prop=text&pst&format=json'; - - var req = sajax_init_object(); - req.onreadystatechange = function() { - if ( req.readyState == 4 && req.status == 200 ) - wgUploadLicenseObj.processResult( eval( '(' + req.responseText + ')' ), license, previewId ); - }; - req.open( 'GET', url, true ); - req.send( '' ); - -// ( new mw.Api() ).get( { -// action: 'parse', -// text: '{{' + license + '}}', -// title: title, -// prop: 'text', -// pst: '' -// } ).done( function ( result ) { -// wgUploadLicenseObj.processResult( result, license, previewId ); -// } ); - }, - - /** - * Ajax callback function. - * - * previewId parameter must be a properly escaped - * HTML id attribute value. - * - * @param {XMLHTTPRequest} result request object from ajax call - * @param {string} license license selector value - * @param {string} previewId ID of element where preview text will go - */ - processResult: function ( result, license, previewId ) { - $.removeSpinner( previewId ); - this.responseCache[license] = result.parse.text['*']; - this.showPreview( this.responseCache[license], previewId ); - }, - - /** - * Display the license preview - * - * previewId parameter must be a properly escaped - * HTML id attribute value. - * - * @param {string} preview HTML text of license - * @param {string} previewId ID of element where text will go - */ - showPreview: function ( preview, previewId ) { - var previewPanel = document.getElementById( previewId ); - if ( previewPanel.innerHTML !== preview ) { - previewPanel.innerHTML = preview; - } - } - -}; - -$( document ).ready( uploadSetup ); - -}( mediaWiki, jQuery ) ); -- To view, visit https://gerrit.wikimedia.org/r/334788 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I3390021444af2eafcb54dc1d1b8a399f8a2bb819 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/MultiUpload Gerrit-Branch: master Gerrit-Owner: Jack Phoenix <j...@countervandalism.net> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits