<?php
-
-class UploadBase {
- var $mTempPath;
- var $mDesiredDestName, $mDestName, $mRemoveTempFile, $mSourceType;
- var $mTitle = false, $mTitleError = 0;
- var $mFilteredName, $mFinalExtension;
+/**
+ * @file
+ * @ingroup upload
+ *
+ * UploadBase and subclasses are the backend of MediaWiki's file uploads.
+ * The frontends are formed by ApiUpload and SpecialUpload.
+ *
+ * See also includes/docs/upload.txt
+ *
+ * @author Brion Vibber
+ * @author Bryan Tong Minh
+ * @author Michael Dale
+ */
+
+abstract class UploadBase {
+ protected $mTempPath;
+ protected $mDesiredDestName, $mDestName, $mRemoveTempFile, $mSourceType;
+ protected $mTitle = false, $mTitleError = 0;
+ protected $mFilteredName, $mFinalExtension;
+ protected $mLocalFile;
const SUCCESS = 0;
const OK = 0;
* Returns true if uploads are enabled.
* Can be override by subclasses.
*/
- static function isEnabled() {
+ public static function isEnabled() {
global $wgEnableUploads;
if ( !$wgEnableUploads )
return false;
* identifying the missing permission.
* Can be overriden by subclasses.
*/
- static function isAllowed( $user ) {
+ public static function isAllowed( $user ) {
if( !$user->isAllowed( 'upload' ) )
return 'upload';
return true;
/**
* Create a form of UploadBase depending on wpSourceType and initializes it
*/
- static function createFromRequest( &$request, $type = null ) {
- $type = $type ? $type : $request->getVal( 'wpSourceType' );
+ public static function createFromRequest( &$request, $type = null ) {
+ $type = $type ? $type : $request->getVal( 'wpSourceType', 'File' );
if( !$type )
return null;
+ // Get the upload class
$type = ucfirst( $type );
$className = 'UploadFrom' . $type;
wfDebug( __METHOD__ . ": class name: $className\n" );
if( !in_array( $type, self::$uploadHandlers ) )
return null;
+ // Check whether this upload class is enabled
if( !call_user_func( array( $className, 'isEnabled' ) ) )
return null;
+ // Check whether the request is valid
if( !call_user_func( array( $className, 'isValidRequest' ), $request ) )
return null;
/**
* Check whether a request if valid for this handler
*/
- static function isValidRequest( $request ) {
+ public static function isValidRequest( $request ) {
return false;
}
- function __construct() {}
+ public function __construct() {}
/**
* Do the real variable initialization
*/
- function initialize( $name, $tempPath, $fileSize, $removeTempFile = false ) {
+ public function initialize( $name, $tempPath, $fileSize, $removeTempFile = false ) {
$this->mDesiredDestName = $name;
$this->mTempPath = $tempPath;
$this->mFileSize = $fileSize;
$this->mRemoveTempFile = $removeTempFile;
}
+
+ /**
+ * Initialize from a WebRequest. Override this in a subclass.
+ */
+ public abstract function initializeFromRequest( &$request );
/**
* Fetch the file. Usually a no-op
*/
- function fetchFile() {
+ public function fetchFile() {
return Status::newGood();
}
/**
* Return the file size
*/
- function isEmptyFile(){
+ public function isEmptyFile(){
return empty( $this->mFileSize );
}
+ /*
+ * getRealPath
+ * @param string $srcPath the source path
+ * @returns the real path if it was a virtual url
+ */
+ function getRealPath( $srcPath ){
+ $repo = RepoGroup::singleton()->getLocalRepo();
+ if ( $repo->isVirtualUrl( $srcPath ) ) {
+ return $repo->resolveVirtualUrl( $srcPath );
+ }
+ return $srcPath;
+ }
+
/**
* Verify whether the upload is sane.
* Returns self::OK or else an array with error information
*/
- function verifyUpload() {
+ public function verifyUpload() {
/**
* If there was no filename or a zero size given, give up quick.
*/
$result['finalExt'] = $this->mFinalExtension;
return $result;
}
- $this->mLocalFile = wfLocalFile( $nt );
- $this->mDestName = $this->mLocalFile->getName();
+ $this->mDestName = $this->getLocalFile()->getName();
/**
* In some cases we may forbid overwriting of existing files.
* type but it's corrupt or data of the wrong type, we should
* probably not accept it.
*/
- $verification = $this->verifyFile( $this->mTempPath );
+ $verification = $this->verifyFile();
if( $verification !== true ) {
if( !is_array( $verification ) )
$verification = array( $verification );
- $verification['status'] = self::VERIFICATION_ERROR;
- return $verification;
+ return array( 'status' => self::VERIFICATION_ERROR,
+ 'details' => $verification );
+
}
$error = '';
if( !wfRunHooks( 'UploadVerification',
array( $this->mDestName, $this->mTempPath, &$error ) ) ) {
+ // This status needs another name...
return array( 'status' => self::UPLOAD_VERIFICATION_ERROR, 'error' => $error );
}
- return self::OK;
+ return array( 'status' => self::OK );
}
/**
* Verifies that it's ok to include the uploaded file
*
- * this function seems to intermixes tmpfile and $this->mTempPath .. no idea why this is
+ * FIXME: this function seems to intermixes tmpfile and $this->mTempPath .. no idea why this is
*
* @param string $tmpfile the full path of the temporary file to verify
* @return mixed true of the file is verified, a string or array otherwise.
*/
- protected function verifyFile( $tmpfile ) {
+ protected function verifyFile() {
$this->mFileProps = File::getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
$this->checkMacBinary();
#magically determine mime type
$magic = MimeMagic::singleton();
- $mime = $magic->guessMimeType( $tmpfile, false );
+ $mime = $magic->guessMimeType( $this->mTempPath, false );
#check mime type, if desired
global $wgVerifyMimeType;
- if ($wgVerifyMimeType) {
+ if ( $wgVerifyMimeType ) {
+ global $wgMimeTypeBlacklist;
if ( $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) )
return array( 'filetype-badmime', $mime );
# Check IE type
- $fp = fopen( $tmpfile, 'rb' );
+ $fp = fopen( $this->mTempPath, 'rb' );
$chunk = fread( $fp, 256 );
fclose( $fp );
$extMime = $magic->guessTypesForExtension( $this->mFinalExtension );
- $ieTypes = $magic->getIEMimeTypes( $tmpfile, $chunk, $extMime );
+ $ieTypes = $magic->getIEMimeTypes( $this->mTempPath, $chunk, $extMime );
foreach ( $ieTypes as $ieType ) {
if ( $this->checkFileExtension( $ieType, $wgMimeTypeBlacklist ) ) {
return array( 'filetype-bad-ie-mime', $ieType );
}
#check for htmlish code and javascript
- if( $this->detectScript( $tmpfile, $mime, $this->mFinalExtension ) ) {
+ if( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) {
return 'uploadscripted';
}
if( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) {
- if( $this->detectScriptInSvg( $tmpfile ) ) {
+ if( self::detectScriptInSvg( $this->mTempPath ) ) {
return 'uploadscripted';
}
}
/**
* Scan the uploaded file for viruses
*/
- $virus = $this->detectVirus( $tmpfile );
+ $virus = $this->detectVirus( $this->mTempPath );
if ( $virus ) {
return array( 'uploadvirus', $virus );
}
}
/**
- * Check whether the user can edit, upload and create the image
+ * Check whether the user can edit, upload and create the image.
+ *
+ * @param User $user the user to verify the permissions against
+ * @return mixed An array as returned by getUserPermissionsErrors or true
+ * in case the user has proper permissions.
*/
- function verifyPermissions( $user ) {
+ public function verifyPermissions( $user ) {
/**
* If the image is protected, non-sysop users won't be able
* to modify it by uploading a new revision.
/**
* Check for non fatal problems with the file
+ *
+ * @return array Array of warnings
*/
- function checkWarnings() {
- $warning = array();
+ public function checkWarnings() {
+ $warnings = array();
- $filename = $this->mLocalFile->getName();
+ $localFile = $this->getLocalFile();
+ $filename = $localFile->getName();
$n = strrpos( $filename, '.' );
$partname = $n ? substr( $filename, 0, $n ) : $filename;
* but ignore things like ucfirst() and spaces/underscore things
*/
$comparableName = str_replace( ' ', '_', $this->mDesiredDestName );
- global $wgCapitalLinks, $wgContLang;
- if ( $wgCapitalLinks ) {
- $comparableName = $wgContLang->ucfirst( $comparableName );
- }
+ $comparableName = Title::capitalize( $comparableName, NS_FILE );
if( $this->mDesiredDestName != $filename && $comparableName != $filename )
- $warning['badfilename'] = $filename;
+ $warnings['badfilename'] = $filename;
// Check whether the file extension is on the unwanted list
global $wgCheckFileExtensions, $wgFileExtensions;
if ( $wgCheckFileExtensions ) {
if ( !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) )
- $warning['filetype-unwanted-type'] = $this->mFinalExtension;
+ $warnings['filetype-unwanted-type'] = $this->mFinalExtension;
}
global $wgUploadSizeWarning;
if ( $wgUploadSizeWarning && ( $this->mFileSize > $wgUploadSizeWarning ) )
- $warning['large-file'] = $wgUploadSizeWarning;
+ $warnings['large-file'] = $wgUploadSizeWarning;
if ( $this->mFileSize == 0 )
- $warning['emptyfile'] = true;
+ $warnings['emptyfile'] = true;
- $exists = self::getExistsWarning( $this->mLocalFile );
+ $exists = self::getExistsWarning( $localFile );
if( $exists !== false )
- $warning['exists'] = $exists;
-
- // Check whether this may be a thumbnail
- if( $exists !== false && $exists[0] != 'thumb'
- && self::isThumbName( $this->mLocalFile->getName() ) ){
- //make the title:
- $nt = $this->getTitle();
- $warning['file-thumbnail-no'] = substr( $filename, 0,
- strpos( $nt->getText() , '-' ) +1 );
- }
+ $warnings['exists'] = $exists;
// Check dupes against existing files
$hash = File::sha1Base36( $this->mTempPath );
unset( $dupes[$key] );
}
if( $dupes )
- $warning['duplicate'] = $dupes;
+ $warnings['duplicate'] = $dupes;
// Check dupes against archives
$archivedImage = new ArchivedFile( null, 0, "{$hash}.{$this->mFinalExtension}" );
if ( $archivedImage->getID() > 0 )
- $warning['duplicate-archive'] = $archivedImage->getName();
-
- $filenamePrefixBlacklist = self::getFilenamePrefixBlacklist();
- foreach( $filenamePrefixBlacklist as $prefix ) {
- if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) {
- $warning['filename-bad-prefix'] = $prefix;
- break;
- }
- }
+ $warnings['duplicate-archive'] = $archivedImage->getName();
- # If the file existed before and was deleted, warn the user of this
- # Don't bother doing so if the file exists now, however
- if( $this->mLocalFile->wasDeleted() && !$this->mLocalFile->exists() )
- $warning['filewasdeleted'] = $this->mLocalFile->getTitle();
-
- return $warning;
+ return $warnings;
}
/**
- * Really perform the upload.
+ * 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.
*/
- function performUpload( $comment, $pageText, $watch, $user ) {
+ public function performUpload( $comment, $pageText, $watch, $user ) {
wfDebug( "\n\n\performUpload: sum:" . $comment . ' c: ' . $pageText . ' w:' . $watch );
- $status = $this->mLocalFile->upload( $this->mTempPath, $comment, $pageText,
+ $status = $this->getLocalFile()->upload( $this->mTempPath, $comment, $pageText,
File::DELETE_SOURCE, $this->mFileProps, false, $user );
if( $status->isGood() && $watch )
- $user->addWatch( $this->mLocalFile->getTitle() );
+ $user->addWatch( $this->getLocalFile()->getTitle() );
if( $status->isGood() )
wfRunHooks( 'UploadComplete', array( &$this ) );
}
/**
- * Returns a title or null
+ * Returns the title of the file to be uploaded. Sets mTitleError in case
+ * the name was illegal.
+ *
+ * @return Title The title of the file or null in case the name was illegal
*/
- function getTitle() {
+ public function getTitle() {
if ( $this->mTitle !== false )
return $this->mTitle;
return $this->mTitle = $nt;
}
- function getLocalFile() {
+ /**
+ * Return the local file and initializes if necessary.
+ */
+ public function getLocalFile() {
if( is_null( $this->mLocalFile ) ) {
$nt = $this->getTitle();
$this->mLocalFile = is_null( $nt ) ? null : wfLocalFile( $nt );
* can accumulate in the temp directory.
*
* @param string $saveName - the destination filename
- * @param string $tempName - the source temporary file to save
+ * @param string $tempSrc - the source temporary file to save
* @return string - full path the stashed file, or false on failure
* @access private
*/
- function saveTempUploadedFile( $saveName, $tempName ) {
+ protected function saveTempUploadedFile( $saveName, $tempSrc ) {
$repo = RepoGroup::singleton()->getLocalRepo();
- $status = $repo->storeTemp( $saveName, $tempName );
+ $status = $repo->storeTemp( $saveName, $tempSrc );
return $status;
}
- /* append to a stashed file */
- function appendToUploadFile( $srcPath, $toAppendPath ){
+ /**
+ * Append a file to a stashed file.
+ *
+ * @param string $srcPath Path to file to append from
+ * @param string $toAppendPath Path to file to append to
+ * @return Status Status
+ */
+ public function appendToUploadFile( $srcPath, $toAppendPath ){
$repo = RepoGroup::singleton()->getLocalRepo();
$status = $repo->append( $srcPath, $toAppendPath );
return $status;
* Returns a key value which will be passed through a form
* to pick up the path info on a later invocation.
*
- * @return int
- * @access private
+ * @return int Session key
*/
- function stashSession() {
+ public function stashSession() {
$status = $this->saveTempUploadedFile( $this->mDestName, $this->mTempPath );
if( !$status->isOK() ) {
# Couldn't save the file.
return false;
}
- $mTempPath = $status->value;
if(!isset($_SESSION))
session_start(); // start up the session (might have been previously closed to prevent php session locking)
$key = $this->getSessionKey();
$_SESSION['wsUploadData'][$key] = array(
- 'mTempPath' => $mTempPath,
+ 'mTempPath' => $status->value,
'mFileSize' => $this->mFileSize,
'mFileProps' => $this->mFileProps,
'version' => self::SESSION_VERSION,
}
/**
- * Pull session key gen from stash in cases where we want to start an upload without much information
+ * Generate a random session key from stash in cases where we want to start an upload without much information
*/
- function getSessionKey(){
+ protected function getSessionKey(){
$key = mt_rand( 0, 0x7fffffff );
$_SESSION['wsUploadData'][$key] = array();
return $key;
* Remove a temporarily kept file stashed by saveTempUploadedFile().
* @return success
*/
- function unsaveUploadedFile() {
+ public function unsaveUploadedFile() {
$repo = RepoGroup::singleton()->getLocalRepo();
$success = $repo->freeTemp( $this->mTempPath );
return $success;
* on exit to clean up.
* @access private
*/
- function cleanupTempFile() {
+ public function cleanupTempFile() {
if ( $this->mRemoveTempFile && $this->mTempPath && file_exists( $this->mTempPath ) ) {
wfDebug( __METHOD__ . ": Removing temporary file {$this->mTempPath}\n" );
unlink( $this->mTempPath );
}
}
- function getTempPath() {
+ public function getTempPath() {
return $this->mTempPath;
}
* @param string $extension The extension of the file
* @return bool true if the file contains something looking like embedded scripts
*/
- function detectScript( $file, $mime, $extension ) {
+ public static function detectScript( $file, $mime, $extension ) {
global $wgAllowTitlesInSVG;
#ugly hack: for text files, always look at the entire file.
* when served with a generic content-type.
*/
$tags = array(
- '<a',
+ '<a href',
'<body',
'<head',
'<html', #also in safari
return false;
}
- function detectScriptInSvg( $filename ) {
+ protected function detectScriptInSvg( $filename ) {
$check = new XmlTypeCheck( $filename, array( $this, 'checkSvgScriptCallback' ) );
return $check->filterMatch;
}
/**
* @todo Replace this with a whitelist filter!
*/
- function checkSvgScriptCallback( $element, $attribs ) {
+ public function checkSvgScriptCallback( $element, $attribs ) {
$stripped = $this->stripXmlNamespace( $element );
if( $stripped == 'script' ) {
* or a string containing feedback from the virus scanner if a virus was found.
* If textual feedback is missing but a virus was found, this function returns true.
*/
- function detectVirus( $file ) {
+ public static function detectVirus( $file ) {
global $wgAntivirus, $wgAntivirusSetup, $wgAntivirusRequired, $wgOut;
if ( !$wgAntivirus ) {
*
* @access private
*/
- function checkMacBinary() {
+ private function checkMacBinary() {
$macbin = new MacBinary( $this->mTempPath );
if( $macbin->isValid() ) {
$dataFile = tempnam( wfTempDir(), 'WikiMacBinary' );
* Check if there's an overwrite conflict and, if so, if restrictions
* forbid this user from performing the upload.
*
- * @return mixed true on success, WikiError on failure
+ * @return mixed true on success, error string on failure
* @access private
*/
- function checkOverwrite() {
+ private function checkOverwrite() {
global $wgUser;
// First check whether the local file can be overwritten
- if( $this->mLocalFile->exists() )
- if( !self::userCanReUpload( $wgUser, $this->mLocalFile ) )
+ $file = $this->getLocalFile();
+ if( $file->exists() ) {
+ if( !self::userCanReUpload( $wgUser, $file ) )
return 'fileexists-forbidden';
+ else
+ return true;
+ }
- // Check shared conflicts
- $file = wfFindFile( $this->mLocalFile->getName() );
- if ( $file && ( !$wgUser->isAllowed( 'reupload' ) ||
- !$wgUser->isAllowed( 'reupload-shared' ) ) )
+ /* Check shared conflicts: if the local file does not exist, but
+ * wfFindFile finds a file, it exists in a shared repository.
+ */
+ $file = wfFindFile( $this->getTitle() );
+ if ( $file && !$wgUser->isAllowed( 'reupload-shared' ) )
return 'fileexists-shared-forbidden';
return true;
return $user->getId() == $img->getUser( 'id' );
}
+ /**
+ * Helper function that does various existence checks for a file.
+ * The following checks are performed:
+ * - The file exists
+ * - Article with the same name as the file exists
+ * - File exists with normalized extension
+ * - The file looks like a thumbnail and the original exists
+ *
+ * @param File $file The file to check
+ * @return mixed False if the file does not exists, else an array
+ */
public static function getExistsWarning( $file ) {
if( $file->exists() )
- return array( 'exists', $file );
+ return array( 'warning' => 'exists', 'file' => $file );
if( $file->getTitle()->getArticleID() )
- return array( 'page-exists', $file );
-
+ return array( 'warning' => 'page-exists', 'file' => $file );
+
+ if ( $file->wasDeleted() && !$file->exists() )
+ return array( 'warning' => 'was-deleted', 'file' => $file );
+
if( strpos( $file->getName(), '.' ) == false ) {
$partname = $file->getName();
- $rawExtension = '';
+ $extension = '';
} else {
$n = strrpos( $file->getName(), '.' );
- $rawExtension = substr( $file->getName(), $n + 1 );
+ $extension = substr( $file->getName(), $n + 1 );
$partname = substr( $file->getName(), 0, $n );
}
+ $normalizedExtension = File::normalizeExtension( $extension );
- if ( $rawExtension != $file->getExtension() ) {
+ if ( $normalizedExtension != $extension ) {
// We're not using the normalized form of the extension.
// Normal form is lowercase, using most common of alternate
// extensions (eg 'jpg' rather than 'JPEG').
//
// Check for another file using the normalized form...
- $nt_lc = Title::makeTitle( NS_FILE, $partname . '.' . $file->getExtension() );
+ $nt_lc = Title::makeTitle( NS_FILE, "{$partname}.{$normalizedExtension}" );
$file_lc = wfLocalFile( $nt_lc );
if( $file_lc->exists() )
- return array( 'exists-normalized', $file_lc );
+ return array( 'warning' => 'exists-normalized', 'file' => $file, 'normalizedFile' => $file_lc );
}
if ( self::isThumbName( $file->getName() ) ) {
# Check for filenames like 50px- or 180px-, these are mostly thumbnails
- $nt_thb = Title::newFromText( substr( $partname , strpos( $partname , '-' ) +1 ) . '.' . $rawExtension );
+ $nt_thb = Title::newFromText( substr( $partname , strpos( $partname , '-' ) +1 ) . '.' . $extension, NS_FILE );
$file_thb = wfLocalFile( $nt_thb );
if( $file_thb->exists() )
- return array( 'thumb', $file_thb );
+ return array( 'warning' => 'thumb', 'file' => $file, 'thumbFile' => $file_thb );
+ else
+ // File does not exist, but we just don't like the name
+ return array( 'warning' => 'thumb-name', 'file' => $file, 'thumbFile' => $file_thb );
+ }
+
+
+ foreach( self::getFilenamePrefixBlacklist() as $prefix ) {
+ if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix )
+ return array( 'warning' => 'bad-prefix', 'file' => $file, 'prefix' => $prefix );
}
+
+
return false;
}
+ /**
+ * Helper function that checks whether the filename looks like a thumbnail
+ */
public static function isThumbName( $filename ) {
$n = strrpos( $filename, '.' );
$partname = $n ? substr( $filename, 0, $n ) : $filename;