From 393e0d71f4c5478bea53ff62ff818aebbabb3e40 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Wed, 22 May 2013 00:45:21 +0200 Subject: Update to MediaWiki 1.20.6 --- includes/AutoLoader.php | 1 + includes/DefaultSettings.php | 2 +- includes/MessageBlobStore.php | 220 +++++++++++---------- includes/api/ApiUpload.php | 7 +- includes/resourceloader/ResourceLoader.php | 20 +- .../resourceloader/ResourceLoaderFileModule.php | 22 ++- includes/upload/UploadBase.php | 75 ++++--- includes/upload/UploadFromChunks.php | 41 +++- includes/upload/UploadFromStash.php | 9 +- includes/upload/UploadStash.php | 11 +- 10 files changed, 257 insertions(+), 151 deletions(-) (limited to 'includes') diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 9bdfcd29..a8b22027 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -965,6 +965,7 @@ $wgAutoloadLocalClasses = array( 'UnwatchedpagesPage' => 'includes/specials/SpecialUnwatchedpages.php', 'UploadChunkFileException' => 'includes/upload/UploadFromChunks.php', 'UploadChunkZeroLengthFileException' => 'includes/upload/UploadFromChunks.php', + 'UploadChunkVerificationException' => 'includes/upload/UploadFromChunks.php', 'UploadForm' => 'includes/specials/SpecialUpload.php', 'UploadSourceField' => 'includes/specials/SpecialUpload.php', 'UserrightsPage' => 'includes/specials/SpecialUserrights.php', diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 6abc2b10..710605ad 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -59,7 +59,7 @@ if( !defined( 'MEDIAWIKI' ) ) { $wgConf = new SiteConfiguration; /** MediaWiki version number */ -$wgVersion = '1.20.5'; +$wgVersion = '1.20.6'; /** Name of the site. It must be changed in LocalSettings.php */ $wgSitename = 'MediaWiki'; diff --git a/includes/MessageBlobStore.php b/includes/MessageBlobStore.php index 34014e1b..c96ea56e 100644 --- a/includes/MessageBlobStore.php +++ b/includes/MessageBlobStore.php @@ -80,42 +80,45 @@ class MessageBlobStore { return false; } - $dbw = wfGetDB( DB_MASTER ); - $success = $dbw->insert( 'msg_resource', array( - 'mr_lang' => $lang, - 'mr_resource' => $name, - 'mr_blob' => $blob, - 'mr_timestamp' => $dbw->timestamp() - ), - __METHOD__, - array( 'IGNORE' ) - ); - - if ( $success ) { - if ( $dbw->affectedRows() == 0 ) { - // Blob was already present, fetch it - $blob = $dbw->selectField( 'msg_resource', 'mr_blob', array( - 'mr_resource' => $name, - 'mr_lang' => $lang, - ), - __METHOD__ - ); - } else { - // Update msg_resource_links - $rows = array(); + try { + $dbw = wfGetDB( DB_MASTER ); + $success = $dbw->insert( 'msg_resource', array( + 'mr_lang' => $lang, + 'mr_resource' => $name, + 'mr_blob' => $blob, + 'mr_timestamp' => $dbw->timestamp() + ), + __METHOD__, + array( 'IGNORE' ) + ); - foreach ( $module->getMessages() as $key ) { - $rows[] = array( - 'mrl_resource' => $name, - 'mrl_message' => $key + if ( $success ) { + if ( $dbw->affectedRows() == 0 ) { + // Blob was already present, fetch it + $blob = $dbw->selectField( 'msg_resource', 'mr_blob', array( + 'mr_resource' => $name, + 'mr_lang' => $lang, + ), + __METHOD__ + ); + } else { + // Update msg_resource_links + $rows = array(); + + foreach ( $module->getMessages() as $key ) { + $rows[] = array( + 'mrl_resource' => $name, + 'mrl_message' => $key + ); + } + $dbw->insert( 'msg_resource_links', $rows, + __METHOD__, array( 'IGNORE' ) ); } - $dbw->insert( 'msg_resource_links', $rows, - __METHOD__, array( 'IGNORE' ) - ); } + } catch ( Exception $e ) { + wfDebug( __METHOD__ . " failed to update DB: $e\n" ); } - return $blob; } @@ -140,49 +143,52 @@ class MessageBlobStore { // Save the old and new blobs for later $oldBlob = $row->mr_blob; $newBlob = self::generateMessageBlob( $module, $lang ); - - $newRow = array( - 'mr_resource' => $name, - 'mr_lang' => $lang, - 'mr_blob' => $newBlob, - 'mr_timestamp' => $dbw->timestamp() - ); - $dbw->replace( 'msg_resource', - array( array( 'mr_resource', 'mr_lang' ) ), - $newRow, __METHOD__ - ); - - // Figure out which messages were added and removed - $oldMessages = array_keys( FormatJson::decode( $oldBlob, true ) ); - $newMessages = array_keys( FormatJson::decode( $newBlob, true ) ); - $added = array_diff( $newMessages, $oldMessages ); - $removed = array_diff( $oldMessages, $newMessages ); + try { + $newRow = array( + 'mr_resource' => $name, + 'mr_lang' => $lang, + 'mr_blob' => $newBlob, + 'mr_timestamp' => $dbw->timestamp() + ); - // Delete removed messages, insert added ones - if ( $removed ) { - $dbw->delete( 'msg_resource_links', array( - 'mrl_resource' => $name, - 'mrl_message' => $removed - ), __METHOD__ + $dbw->replace( 'msg_resource', + array( array( 'mr_resource', 'mr_lang' ) ), + $newRow, __METHOD__ ); - } - $newLinksRows = array(); + // Figure out which messages were added and removed + $oldMessages = array_keys( FormatJson::decode( $oldBlob, true ) ); + $newMessages = array_keys( FormatJson::decode( $newBlob, true ) ); + $added = array_diff( $newMessages, $oldMessages ); + $removed = array_diff( $oldMessages, $newMessages ); - foreach ( $added as $message ) { - $newLinksRows[] = array( - 'mrl_resource' => $name, - 'mrl_message' => $message - ); - } + // Delete removed messages, insert added ones + if ( $removed ) { + $dbw->delete( 'msg_resource_links', array( + 'mrl_resource' => $name, + 'mrl_message' => $removed + ), __METHOD__ + ); + } - if ( $newLinksRows ) { - $dbw->insert( 'msg_resource_links', $newLinksRows, __METHOD__, - array( 'IGNORE' ) // just in case - ); - } + $newLinksRows = array(); + + foreach ( $added as $message ) { + $newLinksRows[] = array( + 'mrl_resource' => $name, + 'mrl_message' => $message + ); + } + if ( $newLinksRows ) { + $dbw->insert( 'msg_resource_links', $newLinksRows, __METHOD__, + array( 'IGNORE' ) // just in case + ); + } + } catch ( Exception $e ) { + wfDebug( __METHOD__ . " failed to update DB: $e\n" ); + } return $newBlob; } @@ -192,50 +198,58 @@ class MessageBlobStore { * @param $key String: message key */ public static function updateMessage( $key ) { - $dbw = wfGetDB( DB_MASTER ); - - // Keep running until the updates queue is empty. - // Due to update conflicts, the queue might not be emptied - // in one iteration. - $updates = null; - do { - $updates = self::getUpdatesForMessage( $key, $updates ); - - foreach ( $updates as $k => $update ) { - // Update the row on the condition that it - // didn't change since we fetched it by putting - // the timestamp in the WHERE clause. - $success = $dbw->update( 'msg_resource', - array( - 'mr_blob' => $update['newBlob'], - 'mr_timestamp' => $dbw->timestamp() ), - array( - 'mr_resource' => $update['resource'], - 'mr_lang' => $update['lang'], - 'mr_timestamp' => $update['timestamp'] ), - __METHOD__ - ); + try { + $dbw = wfGetDB( DB_MASTER ); + + // Keep running until the updates queue is empty. + // Due to update conflicts, the queue might not be emptied + // in one iteration. + $updates = null; + do { + $updates = self::getUpdatesForMessage( $key, $updates ); + + foreach ( $updates as $k => $update ) { + // Update the row on the condition that it + // didn't change since we fetched it by putting + // the timestamp in the WHERE clause. + $success = $dbw->update( 'msg_resource', + array( + 'mr_blob' => $update['newBlob'], + 'mr_timestamp' => $dbw->timestamp() ), + array( + 'mr_resource' => $update['resource'], + 'mr_lang' => $update['lang'], + 'mr_timestamp' => $update['timestamp'] ), + __METHOD__ + ); - // Only requeue conflicted updates. - // If update() returned false, don't retry, for - // fear of getting into an infinite loop - if ( !( $success && $dbw->affectedRows() == 0 ) ) { - // Not conflicted - unset( $updates[$k] ); + // Only requeue conflicted updates. + // If update() returned false, don't retry, for + // fear of getting into an infinite loop + if ( !( $success && $dbw->affectedRows() == 0 ) ) { + // Not conflicted + unset( $updates[$k] ); + } } - } - } while ( count( $updates ) ); + } while ( count( $updates ) ); - // No need to update msg_resource_links because we didn't add - // or remove any messages, we just changed their contents. + // No need to update msg_resource_links because we didn't add + // or remove any messages, we just changed their contents. + } catch ( Exception $e ) { + wfDebug( __METHOD__ . " failed to update DB: $e\n" ); + } } public static function clear() { // TODO: Give this some more thought // TODO: Is TRUNCATE better? - $dbw = wfGetDB( DB_MASTER ); - $dbw->delete( 'msg_resource', '*', __METHOD__ ); - $dbw->delete( 'msg_resource_links', '*', __METHOD__ ); + try { + $dbw = wfGetDB( DB_MASTER ); + $dbw->delete( 'msg_resource', '*', __METHOD__ ); + $dbw->delete( 'msg_resource_links', '*', __METHOD__ ); + } catch ( Exception $e ) { + wfDebug( __METHOD__ . " failed to update DB: $e\n" ); + } } /** diff --git a/includes/api/ApiUpload.php b/includes/api/ApiUpload.php index 3a9b5c56..e7a7849b 100644 --- a/includes/api/ApiUpload.php +++ b/includes/api/ApiUpload.php @@ -187,7 +187,12 @@ class ApiUpload extends ApiBase { $chunkPath = $request->getFileTempname( 'chunk' ); $chunkSize = $request->getUpload( 'chunk' )->getSize(); if ($this->mParams['offset'] == 0) { - $result['filekey'] = $this->performStash(); + try { + $result['filekey'] = $this->performStash(); + } catch ( MWException $e ) { + // FIXME: Error handling here is wrong/different from rest of this + $this->dieUsage( $e->getMessage(), 'stashfailed' ); + } } else { $status = $this->mUpload->addChunk($chunkPath, $chunkSize, $this->mParams['offset']); diff --git a/includes/resourceloader/ResourceLoader.php b/includes/resourceloader/ResourceLoader.php index 3b48a266..7b87f9d4 100644 --- a/includes/resourceloader/ResourceLoader.php +++ b/includes/resourceloader/ResourceLoader.php @@ -176,6 +176,7 @@ class ResourceLoader { } catch ( Exception $exception ) { // Return exception as a comment $result = $this->makeComment( $exception->__toString() ); + $this->hasErrors = true; } wfProfileOut( __METHOD__ ); @@ -434,6 +435,7 @@ class ResourceLoader { wfProfileIn( __METHOD__ ); $errors = ''; + $this->hasErrors = false; // Split requested modules into two groups, modules and missing $modules = array(); @@ -445,6 +447,7 @@ class ResourceLoader { // This is a security issue, see bug 34907. if ( $module->getGroup() === 'private' ) { $errors .= $this->makeComment( "Cannot show private module \"$name\"" ); + $this->hasErrors = true; continue; } $modules[$name] = $this->getModule( $name ); @@ -459,6 +462,7 @@ class ResourceLoader { } catch( Exception $e ) { // Add exception to the output as a comment $errors .= $this->makeComment( $e->__toString() ); + $this->hasErrors = true; } wfProfileIn( __METHOD__.'-getModifiedTime' ); @@ -476,14 +480,12 @@ class ResourceLoader { } catch ( Exception $e ) { // Add exception to the output as a comment $errors .= $this->makeComment( $e->__toString() ); + $this->hasErrors = true; } } wfProfileOut( __METHOD__.'-getModifiedTime' ); - // Send content type and cache related headers - $this->sendResponseHeaders( $context, $mtime ); - // If there's an If-Modified-Since header, respond with a 304 appropriately if ( $this->tryRespondLastModified( $context, $mtime ) ) { wfProfileOut( __METHOD__ ); @@ -500,6 +502,7 @@ class ResourceLoader { // response in a comment if we're in debug mode. if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) { $response = $this->makeComment( $warnings ) . $response; + $this->hasErrors = true; } // Save response to file cache unless there are errors @@ -514,6 +517,9 @@ class ResourceLoader { } } + // Send content type and cache related headers + $this->sendResponseHeaders( $context, $mtime, $this->hasErrors ); + // Remove the output buffer and output the response ob_end_clean(); echo $response; @@ -525,13 +531,15 @@ class ResourceLoader { * Send content type and last modified headers to the client. * @param $context ResourceLoaderContext * @param $mtime string TS_MW timestamp to use for last-modified + * @param $error bool Whether there are commented-out errors in the response * @return void */ - protected function sendResponseHeaders( ResourceLoaderContext $context, $mtime ) { + protected function sendResponseHeaders( ResourceLoaderContext $context, $mtime, $errors ) { global $wgResourceLoaderMaxage; // If a version wasn't specified we need a shorter expiry time for updates // to propagate to clients quickly - if ( is_null( $context->getVersion() ) ) { + // If there were errors, we also need a shorter expiry time so we can recover quickly + if ( is_null( $context->getVersion() ) || $errors ) { $maxage = $wgResourceLoaderMaxage['unversioned']['client']; $smaxage = $wgResourceLoaderMaxage['unversioned']['server']; // If a version was specified we can use a longer expiry time since changing @@ -679,6 +687,7 @@ class ResourceLoader { } catch ( Exception $e ) { // Add exception to the output as a comment $exceptions .= $this->makeComment( $e->__toString() ); + $this->hasErrors = true; } } else { $blobs = array(); @@ -784,6 +793,7 @@ class ResourceLoader { } catch ( Exception $e ) { // Add exception to the output as a comment $exceptions .= $this->makeComment( $e->__toString() ); + $this->hasErrors = true; // Register module as missing $missing[] = $name; diff --git a/includes/resourceloader/ResourceLoaderFileModule.php b/includes/resourceloader/ResourceLoaderFileModule.php index d0c56ae8..8b9b7277 100644 --- a/includes/resourceloader/ResourceLoaderFileModule.php +++ b/includes/resourceloader/ResourceLoaderFileModule.php @@ -308,15 +308,19 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { // Collect referenced files $this->localFileRefs = array_unique( $this->localFileRefs ); // If the list has been modified since last time we cached it, update the cache - if ( $this->localFileRefs !== $this->getFileDependencies( $context->getSkin() ) && !wfReadOnly() ) { - $dbw = wfGetDB( DB_MASTER ); - $dbw->replace( 'module_deps', - array( array( 'md_module', 'md_skin' ) ), array( - 'md_module' => $this->getName(), - 'md_skin' => $context->getSkin(), - 'md_deps' => FormatJson::encode( $this->localFileRefs ), - ) - ); + try { + if ( $this->localFileRefs !== $this->getFileDependencies( $context->getSkin() ) ) { + $dbw = wfGetDB( DB_MASTER ); + $dbw->replace( 'module_deps', + array( array( 'md_module', 'md_skin' ) ), array( + 'md_module' => $this->getName(), + 'md_skin' => $context->getSkin(), + 'md_deps' => FormatJson::encode( $this->localFileRefs ), + ) + ); + } + } catch ( Exception $e ) { + wfDebug( __METHOD__ . " failed to update DB: $e\n" ); } return $styles; } diff --git a/includes/upload/UploadBase.php b/includes/upload/UploadBase.php index 3a5733ca..0848780f 100644 --- a/includes/upload/UploadBase.php +++ b/includes/upload/UploadBase.php @@ -345,6 +345,8 @@ abstract class UploadBase { /** * Verify the mime type * + * @note Only checks that it is not an evil mime. The does it have + * correct extension given its mime type check is in verifyFile. * @param $mime string representing the mime * @return mixed true if the file is verified, an array otherwise */ @@ -359,12 +361,6 @@ abstract class UploadBase { return array( 'filetype-badmime', $mime ); } - # XXX: Missing extension will be caught by validateName() via getTitle() - if ( $this->mFinalExtension != '' && !$this->verifyExtension( $mime, $this->mFinalExtension ) ) { - wfProfileOut( __METHOD__ ); - return array( 'filetype-mime-mismatch', $this->mFinalExtension, $mime ); - } - # Check IE type $fp = fopen( $this->mTempPath, 'rb' ); $chunk = fread( $fp, 256 ); @@ -391,6 +387,56 @@ abstract class UploadBase { * @return mixed true of the file is verified, array otherwise. */ protected function verifyFile() { + global $wgVerifyMimeType; + wfProfileIn( __METHOD__ ); + + $status = $this->verifyPartialFile(); + if ( $status !== true ) { + wfProfileOut( __METHOD__ ); + return $status; + } + + if ( $wgVerifyMimeType ) { + $this->mFileProps = FSFile::getPropsFromPath( $this->mTempPath, $this->mFinalExtension ); + $mime = $this->mFileProps['file-mime']; + + # XXX: Missing extension will be caught by validateName() via getTitle() + if ( $this->mFinalExtension != '' && !$this->verifyExtension( $mime, $this->mFinalExtension ) ) { + wfProfileOut( __METHOD__ ); + return array( 'filetype-mime-mismatch', $this->mFinalExtension, $mime ); + } + } + + $handler = MediaHandler::getHandler( $mime ); + if ( $handler ) { + $handlerStatus = $handler->verifyUpload( $this->mTempPath ); + if ( !$handlerStatus->isOK() ) { + $errors = $handlerStatus->getErrorsArray(); + wfProfileOut( __METHOD__ ); + return reset( $errors ); + } + } + + wfRunHooks( 'UploadVerifyFile', array( $this, $mime, &$status ) ); + if ( $status !== true ) { + wfProfileOut( __METHOD__ ); + return $status; + } + + wfDebug( __METHOD__ . ": all clear; passing.\n" ); + wfProfileOut( __METHOD__ ); + return true; + } + + /** + * A verification routine suitable for partial files + * + * Runs the blacklist checks, but not any checks that may + * assume the entire file is present. + * + * @return Mixed true for valid or array with error message key. + */ + protected function verifyPartialFile() { global $wgAllowJavaUploads, $wgDisableUploadScriptChecks; wfProfileIn( __METHOD__ ); @@ -449,23 +495,6 @@ abstract class UploadBase { return array( 'uploadvirus', $virus ); } - $handler = MediaHandler::getHandler( $mime ); - if ( $handler ) { - $handlerStatus = $handler->verifyUpload( $this->mTempPath ); - if ( !$handlerStatus->isOK() ) { - $errors = $handlerStatus->getErrorsArray(); - wfProfileOut( __METHOD__ ); - return reset( $errors ); - } - } - - wfRunHooks( 'UploadVerifyFile', array( $this, $mime, &$status ) ); - if ( $status !== true ) { - wfProfileOut( __METHOD__ ); - return $status; - } - - wfDebug( __METHOD__ . ": all clear; passing.\n" ); wfProfileOut( __METHOD__ ); return true; } diff --git a/includes/upload/UploadFromChunks.php b/includes/upload/UploadFromChunks.php index 0542bba5..531f7be4 100644 --- a/includes/upload/UploadFromChunks.php +++ b/includes/upload/UploadFromChunks.php @@ -69,6 +69,8 @@ class UploadFromChunks extends UploadFromFile { // Stash file is the called on creating a new chunk session: $this->mChunkIndex = 0; $this->mOffset = 0; + + $this->verifyChunk(); // Create a local stash target $this->mLocalFile = parent::stashFile(); // Update the initial file offset ( based on file size ) @@ -127,9 +129,18 @@ class UploadFromChunks extends UploadFromFile { if( !$status->isOk() ){ return $status; } + + $this->mTempPath = $tmpPath; // file system path + $this->mFileSize = filesize( $this->mTempPath ); //Since this was set for the last chunk previously + $ret = $this->verifyUpload(); + if ( $ret['status'] !== UploadBase::OK ) { + wfDebugLog( 'fileconcatenate', "Verification failed for chunked upload" ); + $status->fatal( $this->getVerificationErrorCode( $ret['status'] ) ); + return $status; + } + // Update the mTempPath and mLocalFile // ( for FileUpload or normal Stash to take over ) - $this->mTempPath = $tmpPath; // file system path $this->mLocalFile = parent::stashFile(); return $status; @@ -181,6 +192,15 @@ class UploadFromChunks extends UploadFromFile { if ( $preAppendOffset == $offset ) { // Update local chunk index for the current chunk $this->mChunkIndex++; + try { + # For some reason mTempPath is set to first part + $oldTemp = $this->mTempPath; + $this->mTempPath = $chunkPath; + $this->verifyChunk(); + $this->mTempPath = $oldTemp; + } catch ( UploadChunkVerificationException $e ) { + return Status::newFatal( $e->getMessage() ); + } $status = $this->outputChunk( $chunkPath ); if( $status->isGood() ){ // Update local offset: @@ -300,7 +320,26 @@ class UploadFromChunks extends UploadFromFile { } return $this->mFileKey . '.' . $index ; } + + /** + * Verify that the chunk isn't really an evil html file + * + * @throws UploadChunkVerificationException + */ + private function verifyChunk() { + // Rest mDesiredDestName here so we verify the name as if it were mFileKey + $oldDesiredDestName = $this->mDesiredDestName; + $this->mDesiredDestName = $this->mFileKey; + $this->mTitle = false; + $res = $this->verifyPartialFile(); + $this->mDesiredDestName = $oldDesiredDestName; + $this->mTitle = false; + if( is_array( $res ) ) { + throw new UploadChunkVerificationException( $res[0] ); + } + } } class UploadChunkZeroLengthFileException extends MWException {}; class UploadChunkFileException extends MWException {}; +class UploadChunkVerificationException extends MWException {}; diff --git a/includes/upload/UploadFromStash.php b/includes/upload/UploadFromStash.php index 607965f3..d79641ce 100644 --- a/includes/upload/UploadFromStash.php +++ b/includes/upload/UploadFromStash.php @@ -129,14 +129,9 @@ class UploadFromStash extends UploadBase { return $this->mSourceType; } - /** - * File has been previously verified so no need to do so again. - * - * @return bool + /* + * protected function verifyFile() inherited */ - protected function verifyFile() { - return true; - } /** * Stash the file. diff --git a/includes/upload/UploadStash.php b/includes/upload/UploadStash.php index c7fd23a9..53a90582 100644 --- a/includes/upload/UploadStash.php +++ b/includes/upload/UploadStash.php @@ -422,6 +422,7 @@ class UploadStash { * @return string */ public static function getExtensionForPath( $path ) { + global $wgFileBlacklist; // Does this have an extension? $n = strrpos( $path, '.' ); $extension = null; @@ -441,7 +442,15 @@ class UploadStash { throw new UploadStashFileException( "extension is null" ); } - return File::normalizeExtension( $extension ); + $extension = File::normalizeExtension( $extension ); + if ( in_array( $extension, $wgFileBlacklist ) ) { + // The file should already be checked for being evil. + // However, if somehow we got here, we definitely + // don't want to give it an extension of .php and + // put it in a web accesible directory. + return ''; + } + return $extension; } /** -- cgit v1.2.3-54-g00ecf