diff options
Diffstat (limited to 'includes/upload/UploadBase.php')
-rw-r--r-- | includes/upload/UploadBase.php | 314 |
1 files changed, 217 insertions, 97 deletions
diff --git a/includes/upload/UploadBase.php b/includes/upload/UploadBase.php index 546b9db8..a97edbc7 100644 --- a/includes/upload/UploadBase.php +++ b/includes/upload/UploadBase.php @@ -18,14 +18,16 @@ abstract class UploadBase { protected $mDesiredDestName, $mDestName, $mRemoveTempFile, $mSourceType; protected $mTitle = false, $mTitleError = 0; protected $mFilteredName, $mFinalExtension; - protected $mLocalFile; + protected $mLocalFile, $mFileSize, $mFileProps; + protected $mBlackListedExtensions; + protected $mJavaDetected; const SUCCESS = 0; const OK = 0; const EMPTY_FILE = 3; const MIN_LENGTH_PARTNAME = 4; const ILLEGAL_FILENAME = 5; - const OVERWRITE_EXISTING_FILE = 7; # Not used anymore; handled by verifyPermissions() + const OVERWRITE_EXISTING_FILE = 7; # Not used anymore; handled by verifyTitlePermissions() const FILETYPE_MISSING = 8; const FILETYPE_BADTYPE = 9; const VERIFICATION_ERROR = 10; @@ -34,13 +36,7 @@ abstract class UploadBase { const UPLOAD_VERIFICATION_ERROR = 11; const HOOK_ABORTED = 11; const FILE_TOO_LARGE = 12; - - const SESSION_VERSION = 2; - const SESSION_KEYNAME = 'wsUploadData'; - - static public function getSessionKeyname() { - return self::SESSION_KEYNAME; - } + const WINDOWS_NONASCII_FILENAME = 13; public function getVerificationErrorCode( $error ) { $code_to_status = array(self::EMPTY_FILE => 'empty-file', @@ -52,6 +48,7 @@ abstract class UploadBase { self::OVERWRITE_EXISTING_FILE => 'overwrite', self::VERIFICATION_ERROR => 'verification-error', self::HOOK_ABORTED => 'hookaborted', + self::WINDOWS_NONASCII_FILENAME => 'windows-nonascii-filename', ); if( isset( $code_to_status[$error] ) ) { return $code_to_status[$error]; @@ -66,21 +63,21 @@ abstract class UploadBase { */ public static function isEnabled() { global $wgEnableUploads; + if ( !$wgEnableUploads ) { return false; } # Check php's file_uploads setting - if( !wfIniGetBool( 'file_uploads' ) ) { - return false; - } - return true; + return wfIsHipHop() || wfIniGetBool( 'file_uploads' ); } /** * Returns true if the user can use this upload module or else a string * identifying the missing permission. * Can be overriden by subclasses. + * + * @param $user User */ public static function isAllowed( $user ) { foreach ( array( 'upload', 'edit' ) as $permission ) { @@ -96,6 +93,9 @@ abstract class UploadBase { /** * Create a form of UploadBase depending on wpSourceType and initializes it + * + * @param $request WebRequest + * @param $type */ public static function createFromRequest( &$request, $type = null ) { $type = $type ? $type : $request->getVal( 'wpSourceType', 'File' ); @@ -144,6 +144,14 @@ abstract class UploadBase { public function __construct() {} /** + * Returns the upload type. Should be overridden by child classes + * + * @since 1.18 + * @return string + */ + public function getSourceType() { return null; } + + /** * Initialize the path information * @param $name string the desired destination name * @param $tempPath string the temporary path @@ -200,6 +208,19 @@ abstract class UploadBase { } /** + * Finish appending to the Repo file + * + * @param $toAppendPath String: path to the Repo file that will be appended to. + * @return Status Status + */ + protected function appendFinish( $toAppendPath ) { + $repo = RepoGroup::singleton()->getLocalRepo(); + $status = $repo->appendFinish( $toAppendPath ); + return $status; + } + + + /** * @param $srcPath String: the source path * @return the real path if it was a virtual URL */ @@ -226,11 +247,11 @@ abstract class UploadBase { /** * Honor $wgMaxUploadSize */ - global $wgMaxUploadSize; - if( $this->mFileSize > $wgMaxUploadSize ) { - return array( + $maxSize = self::getMaxUploadSize( $this->getSourceType() ); + if( $this->mFileSize > $maxSize ) { + return array( 'status' => self::FILE_TOO_LARGE, - 'max' => $wgMaxUploadSize, + 'max' => $maxSize, ); } @@ -279,6 +300,9 @@ abstract class UploadBase { } if ( $this->mTitleError == self::FILETYPE_BADTYPE ) { $result['finalExt'] = $this->mFinalExtension; + if ( count( $this->mBlackListedExtensions ) ) { + $result['blacklistedExt'] = $this->mBlackListedExtensions; + } } return $result; } @@ -297,15 +321,16 @@ abstract class UploadBase { global $wgVerifyMimeType; if ( $wgVerifyMimeType ) { wfDebug ( "\n\nmime: <$mime> extension: <{$this->mFinalExtension}>\n\n"); - if ( !$this->verifyExtension( $mime, $this->mFinalExtension ) ) { - return array( 'filetype-mime-mismatch' ); - } - global $wgMimeTypeBlacklist; if ( $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) { return array( 'filetype-badmime', $mime ); } + # XXX: Missing extension will be caught by validateName() via getTitle() + if ( $this->mFinalExtension != '' && !$this->verifyExtension( $mime, $this->mFinalExtension ) ) { + return array( 'filetype-mime-mismatch', $this->mFinalExtension, $mime ); + } + # Check IE type $fp = fopen( $this->mTempPath, 'rb' ); $chunk = fread( $fp, 256 ); @@ -330,12 +355,12 @@ abstract class UploadBase { * @return mixed true of the file is verified, array otherwise. */ protected function verifyFile() { + global $wgAllowJavaUploads; # get the title, even though we are doing nothing with it, because - # we need to populate mFinalExtension + # we need to populate mFinalExtension $this->getTitle(); - + $this->mFileProps = File::getPropsFromPath( $this->mTempPath, $this->mFinalExtension ); - $this->checkMacBinary(); # check mime type, if desired $mime = $this->mFileProps[ 'file-mime' ]; @@ -354,9 +379,25 @@ abstract class UploadBase { } } - /** - * Scan the uploaded file for viruses - */ + # Check for Java applets, which if uploaded can bypass cross-site + # restrictions. + if ( !$wgAllowJavaUploads ) { + $this->mJavaDetected = false; + $zipStatus = ZipDirectoryReader::read( $this->mTempPath, + array( $this, 'zipEntryCallback' ) ); + if ( !$zipStatus->isOK() ) { + $errors = $zipStatus->getErrorsArray(); + $error = reset( $errors ); + if ( $error[0] !== 'zip-wrong-format' ) { + return $error; + } + } + if ( $this->mJavaDetected ) { + return array( 'uploadjava' ); + } + } + + # Scan the uploaded file for viruses $virus = $this->detectVirus( $this->mTempPath ); if ( $virus ) { return array( 'uploadvirus', $virus ); @@ -381,17 +422,51 @@ abstract class UploadBase { } /** + * Callback for ZipDirectoryReader to detect Java class files. + */ + function zipEntryCallback( $entry ) { + $names = array( $entry['name'] ); + + // If there is a null character, cut off the name at it, because JDK's + // ZIP_GetEntry() uses strcmp() if the name hashes match. If a file name + // were constructed which had ".class\0" followed by a string chosen to + // make the hash collide with the truncated name, that file could be + // returned in response to a request for the .class file. + $nullPos = strpos( $entry['name'], "\000" ); + if ( $nullPos !== false ) { + $names[] = substr( $entry['name'], 0, $nullPos ); + } + + // If there is a trailing slash in the file name, we have to strip it, + // because that's what ZIP_GetEntry() does. + if ( preg_grep( '!\.class/?$!', $names ) ) { + $this->mJavaDetected = true; + } + } + + /** + * Alias for verifyTitlePermissions. The function was originally 'verifyPermissions' + * but that suggests it's checking the user, when it's really checking the title + user combination. + * @param $user User object to verify the permissions against + * @return mixed An array as returned by getUserPermissionsErrors or true + * in case the user has proper permissions. + */ + public function verifyPermissions( $user ) { + return $this->verifyTitlePermissions( $user ); + } + + /** * Check whether the user can edit, upload and create the image. This * checks only against the current title; if it returns errors, it may * very well be that another title will not give errors. Therefore * isAllowed() should be called as well for generic is-user-blocked or * can-user-upload checking. * - * @param $user the User object to verify the permissions against + * @param $user User object to verify the permissions against * @return mixed An array as returned by getUserPermissionsErrors or true * in case the user has proper permissions. */ - public function verifyPermissions( $user ) { + public function verifyTitlePermissions( $user ) { /** * If the image is protected, non-sysop users won't be able * to modify it by uploading a new revision. @@ -412,12 +487,12 @@ abstract class UploadBase { $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsCreate, $permErrors ) ); return $permErrors; } - + $overwriteError = $this->checkOverwrite( $user ); if ( $overwriteError !== true ) { return array( $overwriteError ); } - + return true; } @@ -427,11 +502,12 @@ abstract class UploadBase { * @return Array of warnings */ public function checkWarnings() { + global $wgLang; + $warnings = array(); $localFile = $this->getLocalFile(); $filename = $localFile->getName(); - $n = strrpos( $filename, '.' ); /** * Check whether the resulting filename is different from the desired one, @@ -448,7 +524,8 @@ abstract class UploadBase { global $wgCheckFileExtensions, $wgFileExtensions; if ( $wgCheckFileExtensions ) { if ( !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) ) { - $warnings['filetype-unwanted-type'] = $this->mFinalExtension; + $warnings['filetype-unwanted-type'] = array( $this->mFinalExtension, + $wgLang->commaList( $wgFileExtensions ), count( $wgFileExtensions ) ); } } @@ -493,24 +570,26 @@ abstract class UploadBase { * Really perform the upload. Stores the file in the local repo, watches * if necessary and runs the UploadComplete hook. * - * @return mixed Status indicating the whether the upload succeeded. + * @param $user User + * + * @return Status indicating the whether the upload succeeded. */ public function performUpload( $comment, $pageText, $watch, $user ) { - $status = $this->getLocalFile()->upload( - $this->mTempPath, - $comment, + $status = $this->getLocalFile()->upload( + $this->mTempPath, + $comment, $pageText, File::DELETE_SOURCE, - $this->mFileProps, - false, - $user + $this->mFileProps, + false, + $user ); if( $status->isGood() ) { if ( $watch ) { $user->addWatch( $this->getLocalFile()->getTitle() ); } - + wfRunHooks( 'UploadComplete', array( &$this ) ); } @@ -527,13 +606,23 @@ abstract class UploadBase { if ( $this->mTitle !== false ) { return $this->mTitle; } + + /* Assume that if a user specified File:Something.jpg, this is an error + * and that the namespace prefix needs to be stripped of. + */ + $title = Title::newFromText( $this->mDesiredDestName ); + if ( $title && $title->getNamespace() == NS_FILE ) { + $this->mFilteredName = $title->getDBkey(); + } else { + $this->mFilteredName = $this->mDesiredDestName; + } /** * Chop off any directories in the given filename. Then * filter out illegal characters, and try to make a legible name * out of it. We'll strip some silently that Title would die on. */ - $this->mFilteredName = wfStripIllegalFilenameChars( $this->mDesiredDestName ); + $this->mFilteredName = wfStripIllegalFilenameChars( $this->mFilteredName ); /* Normalize to title form before we do any further processing */ $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName ); if( is_null( $nt ) ) { @@ -552,20 +641,48 @@ abstract class UploadBase { $this->mFinalExtension = trim( $ext[count( $ext ) - 1] ); } else { $this->mFinalExtension = ''; + + # No extension, try guessing one + $magic = MimeMagic::singleton(); + $mime = $magic->guessMimeType( $this->mTempPath ); + if ( $mime !== 'unknown/unknown' ) { + # Get a space separated list of extensions + $extList = $magic->getExtensionsForType( $mime ); + if ( $extList ) { + # Set the extension to the canonical extension + $this->mFinalExtension = strtok( $extList, ' ' ); + + # Fix up the other variables + $this->mFilteredName .= ".{$this->mFinalExtension}"; + $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName ); + $ext = array( $this->mFinalExtension ); + } + } + } /* Don't allow users to override the blacklist (check file extension) */ global $wgCheckFileExtensions, $wgStrictFileExtensions; global $wgFileExtensions, $wgFileBlacklist; + + $blackListedExtensions = $this->checkFileExtensionList( $ext, $wgFileBlacklist ); + if ( $this->mFinalExtension == '' ) { $this->mTitleError = self::FILETYPE_MISSING; return $this->mTitle = null; - } elseif ( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) || + } elseif ( $blackListedExtensions || ( $wgCheckFileExtensions && $wgStrictFileExtensions && - !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) ) ) { + !$this->checkFileExtensionList( $ext, $wgFileExtensions ) ) ) { + $this->mBlackListedExtensions = $blackListedExtensions; $this->mTitleError = self::FILETYPE_BADTYPE; return $this->mTitle = null; } + + // Windows may be broken with special characters, see bug XXX + if ( wfIsWindows() && !preg_match( '/^[\x0-\x7f]*$/', $nt->getText() ) ) { + $this->mTitleError = self::WINDOWS_NONASCII_FILENAME; + return $this->mTitle = null; + } # If there was more than one "extension", reassemble the base # filename to prevent bogus complaints about length @@ -585,6 +702,8 @@ abstract class UploadBase { /** * Return the local file and initializes if necessary. + * + * @return LocalFile */ public function getLocalFile() { if( is_null( $this->mLocalFile ) ) { @@ -619,31 +738,40 @@ abstract class UploadBase { * by design) then we may want to stash the file temporarily, get more information, and publish the file later. * * This method will stash a file in a temporary directory for later processing, and save the necessary descriptive info - * into the user's session. - * This method returns the file object, which also has a 'sessionKey' property which can be passed through a form or + * into the database. + * This method returns the file object, which also has a 'fileKey' property which can be passed through a form or * API request to find this stashed file again. * - * @param $key String: (optional) the session key used to find the file info again. If not supplied, a key will be autogenerated. - * @return File: stashed file + * @param $key String: (optional) the file key used to find the file info again. If not supplied, a key will be autogenerated. + * @return UploadStashFile stashed file */ - public function stashSessionFile( $key = null ) { + public function stashFile( $key = null ) { + // was stashSessionFile $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash(); - $data = array( - 'mFileProps' => $this->mFileProps - ); - $file = $stash->stashFile( $this->mTempPath, $data, $key ); + + $file = $stash->stashFile( $this->mTempPath, $this->getSourceType(), $key ); $this->mLocalFile = $file; return $file; } /** - * Stash a file in a temporary directory, returning a key which can be used to find the file again. See stashSessionFile(). + * Stash a file in a temporary directory, returning a key which can be used to find the file again. See stashFile(). * - * @param $key String: (optional) the session key used to find the file info again. If not supplied, a key will be autogenerated. - * @return String: session key + * @param $key String: (optional) the file key used to find the file info again. If not supplied, a key will be autogenerated. + * @return String: file key + */ + public function stashFileGetKey( $key = null ) { + return $this->stashFile( $key )->getFileKey(); + } + + /** + * alias for stashFileGetKey, for backwards compatibility + * + * @param $key String: (optional) the file key used to find the file info again. If not supplied, a key will be autogenerated. + * @return String: file key */ public function stashSession( $key = null ) { - return $this->stashSessionFile( $key )->getSessionKey(); + return $this->stashFileGetKey( $key ); } /** @@ -689,19 +817,14 @@ abstract class UploadBase { /** * Perform case-insensitive match against a list of file extensions. - * Returns true if any of the extensions are in the list. + * Returns an array of matching extensions. * * @param $ext Array * @param $list Array * @return Boolean */ public static function checkFileExtensionList( $ext, $list ) { - foreach( $ext as $e ) { - if( in_array( strtolower( $e ), $list ) ) { - return true; - } - } - return false; + return array_intersect( array_map( 'strtolower', $ext ), $list ); } /** @@ -788,7 +911,7 @@ abstract class UploadBase { $chunk = trim( $chunk ); - # FIXME: convert from UTF-16 if necessarry! + # @todo FIXME: Convert from UTF-16 if necessarry! wfDebug( __METHOD__ . ": checking for embedded scripts and HTML stuff\n" ); # check for HTML doctype @@ -828,6 +951,7 @@ abstract class UploadBase { foreach( $tags as $tag ) { if( false !== strpos( $chunk, $tag ) ) { + wfDebug( __METHOD__ . ": found something that may make it be mistaken for html: $tag\n" ); return true; } } @@ -841,16 +965,19 @@ abstract class UploadBase { # look for script-types if( preg_match( '!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim', $chunk ) ) { + wfDebug( __METHOD__ . ": found script types\n" ); return true; } # look for html-style script-urls if( preg_match( '!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) { + wfDebug( __METHOD__ . ": found html-style script urls\n" ); return true; } # look for css-style script-urls if( preg_match( '!url\s*\(\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) { + wfDebug( __METHOD__ . ": found css-style script urls\n" ); return true; } @@ -989,33 +1116,11 @@ abstract class UploadBase { } /** - * Check if the temporary file is MacBinary-encoded, as some uploads - * from Internet Explorer on Mac OS Classic and Mac OS X will be. - * If so, the data fork will be extracted to a second temporary file, - * which will then be checked for validity and either kept or discarded. - */ - private function checkMacBinary() { - $macbin = new MacBinary( $this->mTempPath ); - if( $macbin->isValid() ) { - $dataFile = tempnam( wfTempDir(), 'WikiMacBinary' ); - $dataHandle = fopen( $dataFile, 'wb' ); - - wfDebug( __METHOD__ . ": Extracting MacBinary data fork to $dataFile\n" ); - $macbin->extractData( $dataHandle ); - - $this->mTempPath = $dataFile; - $this->mFileSize = $macbin->dataForkLength(); - - // We'll have to manually remove the new file if it's not kept. - $this->mRemoveTempFile = true; - } - $macbin->close(); - } - - /** * Check if there's an overwrite conflict and, if so, if restrictions * forbid this user from performing the upload. * + * @param $user User + * * @return mixed true on success, array on failure */ private function checkOverwrite( $user ) { @@ -1072,7 +1177,7 @@ abstract class UploadBase { * - File exists with normalized extension * - The file looks like a thumbnail and the original exists * - * @param $file The File object to check + * @param $file File The File object to check * @return mixed False if the file does not exists, else an array */ public static function getExistsWarning( $file ) { @@ -1170,9 +1275,9 @@ abstract class UploadBase { */ public static function getFilenamePrefixBlacklist() { $blacklist = array(); - $message = wfMsgForContent( 'filename-prefix-blacklist' ); - if( $message && !( wfEmptyMsg( 'filename-prefix-blacklist', $message ) || $message == '-' ) ) { - $lines = explode( "\n", $message ); + $message = wfMessage( 'filename-prefix-blacklist' )->inContentLanguage(); + if( !$message->isDisabled() ) { + $lines = explode( "\n", $message->plain() ); foreach( $lines as $line ) { // Remove comment lines $comment = substr( trim( $line ), 0, 1 ); @@ -1191,18 +1296,18 @@ abstract class UploadBase { } /** - * Gets image info about the file just uploaded. + * Gets image info about the file just uploaded. * - * Also has the effect of setting metadata to be an 'indexed tag name' in returned API result if + * Also has the effect of setting metadata to be an 'indexed tag name' in returned API result if * 'metadata' was requested. Oddly, we have to pass the "result" object down just so it can do that - * with the appropriate format, presumably. + * with the appropriate format, presumably. * * @param $result ApiResult: * @return Array: image info */ public function getImageInfo( $result ) { $file = $this->getLocalFile(); - // TODO This cries out for refactoring. We really want to say $file->getAllInfo(); here. + // TODO This cries out for refactoring. We really want to say $file->getAllInfo(); here. // Perhaps "info" methods should be moved into files, and the API should just wrap them in queries. if ( $file instanceof UploadStashFile ) { $imParam = ApiQueryStashImageInfo::getPropertyNames(); @@ -1220,4 +1325,19 @@ abstract class UploadBase { unset( $code['status'] ); return Status::newFatal( $this->getVerificationErrorCode( $code ), $error ); } + + public static function getMaxUploadSize( $forType = null ) { + global $wgMaxUploadSize; + + if ( is_array( $wgMaxUploadSize ) ) { + if ( !is_null( $forType ) && isset( $wgMaxUploadSize[$forType] ) ) { + return $wgMaxUploadSize[$forType]; + } else { + return $wgMaxUploadSize['*']; + } + } else { + return intval( $wgMaxUploadSize ); + } + + } } |