execute();
}
/**
*
* @package MediaWiki
* @subpackage SpecialPage
*/
class UploadForm {
/**#@+
* @access private
*/
var $mUploadAffirm, $mUploadFile, $mUploadDescription, $mIgnoreWarning;
var $mUploadSaveName, $mUploadTempName, $mUploadSize, $mUploadOldVersion;
var $mUploadCopyStatus, $mUploadSource, $mReUpload, $mAction, $mUpload;
var $mOname, $mSessionKey, $mStashed, $mDestFile;
/**#@-*/
/**
* Constructor : initialise object
* Get data POSTed through the form and assign them to the object
* @param $request Data posted.
*/
function UploadForm( &$request ) {
$this->mDestFile = $request->getText( 'wpDestFile' );
if( !$request->wasPosted() ) {
# GET requests just give the main form; no data except wpDestfile.
return;
}
$this->mUploadAffirm = $request->getCheck( 'wpUploadAffirm' );
$this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning');
$this->mReUpload = $request->getCheck( 'wpReUpload' );
$this->mUpload = $request->getCheck( 'wpUpload' );
$this->mUploadDescription = $request->getText( 'wpUploadDescription' );
$this->mUploadCopyStatus = $request->getText( 'wpUploadCopyStatus' );
$this->mUploadSource = $request->getText( 'wpUploadSource');
$this->mAction = $request->getVal( 'action' );
$this->mSessionKey = $request->getInt( 'wpSessionKey' );
if( !empty( $this->mSessionKey ) &&
isset( $_SESSION['wsUploadData'][$this->mSessionKey] ) ) {
/**
* Confirming a temporarily stashed upload.
* We don't want path names to be forged, so we keep
* them in the session on the server and just give
* an opaque key to the user agent.
*/
$data = $_SESSION['wsUploadData'][$this->mSessionKey];
$this->mUploadTempName = $data['mUploadTempName'];
$this->mUploadSize = $data['mUploadSize'];
$this->mOname = $data['mOname'];
$this->mStashed = true;
} else {
/**
*Check for a newly uploaded file.
*/
$this->mUploadTempName = $request->getFileTempName( 'wpUploadFile' );
$this->mUploadSize = $request->getFileSize( 'wpUploadFile' );
$this->mOname = $request->getFileName( 'wpUploadFile' );
$this->mSessionKey = false;
$this->mStashed = false;
}
}
/**
* Start doing stuff
* @access public
*/
function execute() {
global $wgUser, $wgOut;
global $wgEnableUploads, $wgUploadDirectory;
/** Show an error message if file upload is disabled */
if( ! $wgEnableUploads ) {
$wgOut->addWikiText( wfMsg( 'uploaddisabled' ) );
return;
}
/** Various rights checks */
if( ( $wgUser->isAnon() )
OR $wgUser->isBlocked() ) {
$wgOut->errorpage( 'uploadnologin', 'uploadnologintext' );
return;
}
if( wfReadOnly() ) {
$wgOut->readOnlyPage();
return;
}
/** Check if the image directory is writeable, this is a common mistake */
if ( !is_writeable( $wgUploadDirectory ) ) {
$wgOut->addWikiText( wfMsg( 'upload_directory_read_only', $wgUploadDirectory ) );
return;
}
if( $this->mReUpload ) {
$this->unsaveUploadedFile();
$this->mainUploadForm();
} else if ( 'submit' == $this->mAction || $this->mUpload ) {
$this->processUpload();
} else {
$this->mainUploadForm();
}
}
/* -------------------------------------------------------------- */
/**
* Really do the upload
* Checks are made in SpecialUpload::execute()
* @access private
*/
function processUpload() {
global $wgUser, $wgOut, $wgLang, $wgContLang;
global $wgUploadDirectory;
global $wgUseCopyrightUpload, $wgCheckCopyrightUpload;
/**
* If there was no filename or a zero size given, give up quick.
*/
if( trim( $this->mOname ) == '' || empty( $this->mUploadSize ) ) {
return $this->mainUploadForm('
'.wfMsg( 'emptyfile' ).'
');
}
/**
* When using detailed copyright, if user filled field, assume he
* confirmed the upload
*/
if ( $wgUseCopyrightUpload ) {
$this->mUploadAffirm = true;
if( $wgCheckCopyrightUpload &&
( trim( $this->mUploadCopyStatus ) == '' ||
trim( $this->mUploadSource ) == '' ) ) {
$this->mUploadAffirm = false;
}
}
/** User need to confirm his upload */
if( !$this->mUploadAffirm ) {
$this->mainUploadForm( wfMsg( 'noaffirmation' ) );
return;
}
# Chop off any directories in the given filename
if ( $this->mDestFile ) {
$basename = basename( $this->mDestFile );
} else {
$basename = basename( $this->mOname );
}
/**
* We'll want to blacklist against *any* 'extension', and use
* only the final one for the whitelist.
*/
list( $partname, $ext ) = $this->splitExtensions( $basename );
if( count( $ext ) ) {
$finalExt = $ext[count( $ext ) - 1];
} else {
$finalExt = '';
}
$fullExt = implode( '.', $ext );
if ( strlen( $partname ) < 3 ) {
$this->mainUploadForm( wfMsg( 'minlength' ) );
return;
}
/**
* Filter out illegal characters, and try to make a legible name
* out of it. We'll strip some silently that Title would die on.
*/
$filtered = preg_replace ( "/[^".Title::legalChars()."]|:/", '-', $basename );
$nt = Title::newFromText( $filtered );
if( is_null( $nt ) ) {
return $this->uploadError( wfMsg( 'illegalfilename', htmlspecialchars( $filtered ) ) );
}
$nt =& Title::makeTitle( NS_IMAGE, $nt->getDBkey() );
$this->mUploadSaveName = $nt->getDBkey();
/**
* If the image is protected, non-sysop users won't be able
* to modify it by uploading a new revision.
*/
if( !$nt->userCanEdit() ) {
return $this->uploadError( wfMsg( 'protectedpage' ) );
}
/* Don't allow users to override the blacklist */
global $wgStrictFileExtensions;
global $wgFileExtensions, $wgFileBlacklist;
if( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) ||
($wgStrictFileExtensions &&
!$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) ) {
return $this->uploadError( wfMsg( 'badfiletype', htmlspecialchars( $fullExt ) ) );
}
/**
* Look at the contents of the file; if we can recognize the
* type but it's corrupt or data of the wrong type, we should
* probably not accept it.
*/
if( !$this->mStashed && !$this->verify( $this->mUploadTempName, $finalExt ) ) {
return $this->uploadError( wfMsg( 'uploadcorrupt' ) );
}
/**
* Check for non-fatal conditions
*/
if ( ! $this->mIgnoreWarning ) {
$warning = '';
if( $this->mUploadSaveName != ucfirst( $filtered ) ) {
$warning .= '
';
}
}
global $wgUploadSizeWarning;
if ( $wgUploadSizeWarning && ( $this->mUploadSize > $wgUploadSizeWarning ) ) {
# TODO: Format $wgUploadSizeWarning to something that looks better than the raw byte
# value, perhaps add GB,MB and KB suffixes?
$warning .= '
';
}
if( $warning != '' ) {
/**
* Stash the file in a temporary location; the user can choose
* to let it through and we'll complete the upload then.
*/
return $this->uploadWarning($warning);
}
}
/**
* Try actually saving the thing...
* It will show an error form on failure.
*/
if( $this->saveUploadedFile( $this->mUploadSaveName,
$this->mUploadTempName,
!empty( $this->mSessionKey ) ) ) {
/**
* Update the upload log and create the description page
* if it's a new file.
*/
$img = Image::newFromName( $this->mUploadSaveName );
$success = $img->recordUpload( $this->mUploadOldVersion,
$this->mUploadDescription,
$this->mUploadCopyStatus,
$this->mUploadSource );
if ( $success ) {
$this->showSuccess();
} else {
// Image::recordUpload() fails if the image went missing, which is
// unlikely, hence the lack of a specialised message
$wgOut->fileNotFoundError( $this->mUploadSaveName );
}
}
}
/**
* Move the uploaded file from its temporary location to the final
* destination. If a previous version of the file exists, move
* it into the archive subdirectory.
*
* @todo If the later save fails, we may have disappeared the original file.
*
* @param string $saveName
* @param string $tempName full path to the temporary file
* @param bool $useRename if true, doesn't check that the source file
* is a PHP-managed upload temporary
*/
function saveUploadedFile( $saveName, $tempName, $useRename = false ) {
global $wgUploadDirectory, $wgOut;
$dest = wfImageDir( $saveName );
$archive = wfImageArchiveDir( $saveName );
$this->mSavedFile = "{$dest}/{$saveName}";
if( is_file( $this->mSavedFile ) ) {
$this->mUploadOldVersion = gmdate( 'YmdHis' ) . "!{$saveName}";
wfSuppressWarnings();
$success = rename( $this->mSavedFile, "${archive}/{$this->mUploadOldVersion}" );
wfRestoreWarnings();
if( ! $success ) {
$wgOut->fileRenameError( $this->mSavedFile,
"${archive}/{$this->mUploadOldVersion}" );
return false;
}
} else {
$this->mUploadOldVersion = '';
}
if( $useRename ) {
wfSuppressWarnings();
$success = rename( $tempName, $this->mSavedFile );
wfRestoreWarnings();
if( ! $success ) {
$wgOut->fileCopyError( $tempName, $this->mSavedFile );
return false;
}
} else {
wfSuppressWarnings();
$success = move_uploaded_file( $tempName, $this->mSavedFile );
wfRestoreWarnings();
if( ! $success ) {
$wgOut->fileCopyError( $tempName, $this->mSavedFile );
return false;
}
}
chmod( $this->mSavedFile, 0644 );
return true;
}
/**
* Stash a file in a temporary directory for later processing
* after the user has confirmed it.
*
* If the user doesn't explicitly cancel or accept, these files
* can accumulate in the temp directory.
*
* @param string $saveName - the destination filename
* @param string $tempName - the source temporary file to save
* @return string - full path the stashed file, or false on failure
* @access private
*/
function saveTempUploadedFile( $saveName, $tempName ) {
global $wgOut;
$archive = wfImageArchiveDir( $saveName, 'temp' );
$stash = $archive . '/' . gmdate( "YmdHis" ) . '!' . $saveName;
if ( !move_uploaded_file( $tempName, $stash ) ) {
$wgOut->fileCopyError( $tempName, $stash );
return false;
}
return $stash;
}
/**
* Stash a file in a temporary directory for later processing,
* and save the necessary descriptive info into the session.
* 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
*/
function stashSession() {
$stash = $this->saveTempUploadedFile(
$this->mUploadSaveName, $this->mUploadTempName );
if( !$stash ) {
# Couldn't save the file.
return false;
}
$key = mt_rand( 0, 0x7fffffff );
$_SESSION['wsUploadData'][$key] = array(
'mUploadTempName' => $stash,
'mUploadSize' => $this->mUploadSize,
'mOname' => $this->mOname );
return $key;
}
/**
* Remove a temporarily kept file stashed by saveTempUploadedFile().
* @access private
*/
function unsaveUploadedFile() {
wfSuppressWarnings();
$success = unlink( $this->mUploadTempName );
wfRestoreWarnings();
if ( ! $success ) {
$wgOut->fileDeleteError( $this->mUploadTempName );
}
}
/* -------------------------------------------------------------- */
/**
* Show some text and linkage on successful upload.
* @access private
*/
function showSuccess() {
global $wgUser, $wgOut, $wgContLang;
$sk = $wgUser->getSkin();
$ilink = $sk->makeMediaLink( $this->mUploadSaveName, Image::imageUrl( $this->mUploadSaveName ) );
$dname = $wgContLang->getNsText( NS_IMAGE ) . ':'.$this->mUploadSaveName;
$dlink = $sk->makeKnownLink( $dname, $dname );
$wgOut->addHTML( '
'.$text."\n" );
$wgOut->returnToMain( false );
}
/**
* @param string $error as HTML
* @access private
*/
function uploadError( $error ) {
global $wgOut;
$sub = wfMsg( 'uploadwarning' );
$wgOut->addHTML( "
{$sub}
\n" );
$wgOut->addHTML( "
{$error}
\n" );
}
/**
* There's something wrong with this file, not enough to reject it
* totally but we require manual intervention to save it for real.
* Stash it away, then present a form asking to confirm or cancel.
*
* @param string $warning as HTML
* @access private
*/
function uploadWarning( $warning ) {
global $wgOut, $wgUser, $wgLang, $wgUploadDirectory, $wgRequest;
global $wgUseCopyrightUpload;
$this->mSessionKey = $this->stashSession();
if( !$this->mSessionKey ) {
# Couldn't save file; an error has been displayed so let's go.
return;
}
$sub = wfMsg( 'uploadwarning' );
$wgOut->addHTML( "
{$sub}
\n" );
$wgOut->addHTML( "
{$warning}
\n" );
$save = wfMsg( 'savefile' );
$reupload = wfMsg( 'reupload' );
$iw = wfMsg( 'ignorewarning' );
$reup = wfMsg( 'reuploaddesc' );
$titleObj = Title::makeTitle( NS_SPECIAL, 'Upload' );
$action = $titleObj->escapeLocalURL( 'action=submit' );
if ( $wgUseCopyrightUpload )
{
$copyright = "
mUploadCopyStatus ) . "\" />
mUploadSource ) . "\" />
";
} else {
$copyright = "";
}
$wgOut->addHTML( "
\n" );
}
/**
* Displays the main upload form, optionally with a highlighted
* error message up at the top.
*
* @param string $msg as HTML
* @access private
*/
function mainUploadForm( $msg='' ) {
global $wgOut, $wgUser, $wgLang, $wgUploadDirectory, $wgRequest;
global $wgUseCopyrightUpload;
$cols = intval($wgUser->getOption( 'cols' ));
$ew = $wgUser->getOption( 'editwidth' );
if ( $ew ) $ew = " style=\"width:100%\"";
else $ew = '';
if ( '' != $msg ) {
$sub = wfMsg( 'uploaderror' );
$wgOut->addHTML( "
" ;
}
$wgOut->addHTML( "
\n" );
}
/* -------------------------------------------------------------- */
/**
* Split a file into a base name and all dot-delimited 'extensions'
* on the end. Some web server configurations will fall back to
* earlier pseudo-'extensions' to determine type and execute
* scripts, so the blacklist needs to check them all.
*
* @return array
*/
function splitExtensions( $filename ) {
$bits = explode( '.', $filename );
$basename = array_shift( $bits );
return array( $basename, $bits );
}
/**
* Perform case-insensitive match against a list of file extensions.
* Returns true if the extension is in the list.
*
* @param string $ext
* @param array $list
* @return bool
*/
function checkFileExtension( $ext, $list ) {
return in_array( strtolower( $ext ), $list );
}
/**
* Perform case-insensitive match against a list of file extensions.
* Returns true if any of the extensions are in the list.
*
* @param array $ext
* @param array $list
* @return bool
*/
function checkFileExtensionList( $ext, $list ) {
foreach( $ext as $e ) {
if( in_array( strtolower( $e ), $list ) ) {
return true;
}
}
return false;
}
/**
* Returns false if the file is of a known type but can't be recognized,
* indicating a corrupt file.
* Returns true otherwise; unknown file types are not checked if given
* with an unrecognized extension.
*
* @param string $tmpfile Pathname to the temporary upload file
* @param string $extension The filename extension that the file is to be served with
* @return bool
*/
function verify( $tmpfile, $extension ) {
if( $this->triggersIEbug( $tmpfile ) ||
$this->triggersSafariBug( $tmpfile ) ) {
return false;
}
$fname = 'SpecialUpload::verify';
$mergeExtensions = array(
'jpg' => 'jpeg',
'tif' => 'tiff' );
$extensionTypes = array(
# See http://www.php.net/getimagesize
1 => 'gif',
2 => 'jpeg',
3 => 'png',
4 => 'swf',
5 => 'psd',
6 => 'bmp',
7 => 'tiff',
8 => 'tiff',
9 => 'jpc',
10 => 'jp2',
11 => 'jpx',
12 => 'jb2',
13 => 'swc',
14 => 'iff',
15 => 'wbmp',
16 => 'xbm' );
$extension = strtolower( $extension );
if( isset( $mergeExtensions[$extension] ) ) {
$extension = $mergeExtensions[$extension];
}
wfDebug( "$fname: Testing file '$tmpfile' with given extension '$extension'\n" );
if( !in_array( $extension, $extensionTypes ) ) {
# Not a recognized image type. We don't know how to verify these.
# They're allowed by policy or they wouldn't get this far, so we'll
# let them slide for now.
wfDebug( "$fname: Unknown extension; passing.\n" );
return true;
}
wfSuppressWarnings();
$data = getimagesize( $tmpfile );
wfRestoreWarnings();
if( false === $data ) {
# Didn't recognize the image type.
# Either the image is corrupt or someone's slipping us some
# bogus data such as HTML+JavaScript trying to take advantage
# of an Internet Explorer security flaw.
wfDebug( "$fname: getimagesize() doesn't recognize the file; rejecting.\n" );
return false;
}
$imageType = $data[2];
if( !isset( $extensionTypes[$imageType] ) ) {
# Now we're kind of confused. Perhaps new image types added
# to PHP's support that we don't know about.
# We'll let these slide for now.
wfDebug( "$fname: getimagesize() knows the file, but we don't recognize the type; passing.\n" );
return true;
}
$ext = strtolower( $extension );
if( $extension != $extensionTypes[$imageType] ) {
# The given filename extension doesn't match the
# file type. Probably just a mistake, but it's a stupid
# one and we shouldn't let it pass. KILL THEM!
wfDebug( "$fname: file extension does not match recognized type; rejecting.\n" );
return false;
}
wfDebug( "$fname: all clear; passing.\n" );
return true;
}
/**
* Internet Explorer for Windows performs some really stupid file type
* autodetection which can cause it to interpret valid image files as HTML
* and potentially execute JavaScript, creating a cross-site scripting
* attack vectors.
*
* Returns true if IE is likely to mistake the given file for HTML.
*
* @param string $filename
* @return bool
*/
function triggersIEbug( $filename ) {
$file = fopen( $filename, 'rb' );
$chunk = strtolower( fread( $file, 256 ) );
fclose( $file );
$tags = array(
'