';
}
if ( !$this->mDestWarningAck ) {
$warning .= self::getExistsWarning( $this->mLocalFile );
}
if( $warning != '' ) {
/**
* Stash the file in a temporary location; the user can choose
* to let it through and we'll complete the upload then.
*/
$resultDetails = array( 'warning' => $warning );
return self::UPLOAD_WARNING;
}
}
/**
* Try actually saving the thing...
* It will show an error form on failure.
*/
$pageText = self::getInitialPageText( $this->mComment, $this->mLicense,
$this->mCopyrightStatus, $this->mCopyrightSource );
$status = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText,
File::DELETE_SOURCE, $this->mFileProps );
if ( !$status->isGood() ) {
$resultDetails = array( 'internal' => $status->getWikiText() );
return self::INTERNAL_ERROR;
} else {
if ( $this->mWatchthis ) {
global $wgUser;
$wgUser->addWatch( $this->mLocalFile->getTitle() );
}
// Success, redirect to description page
$img = null; // @todo: added to avoid passing a ref to null - should this be defined somewhere?
wfRunHooks( 'UploadComplete', array( &$this ) );
return self::SUCCESS;
}
}
/**
* Do existence checks on a file and produce a warning
* This check is static and can be done pre-upload via AJAX
* Returns an HTML fragment consisting of one or more LI elements if there is a warning
* Returns an empty string if there is no warning
*/
static function getExistsWarning( $file ) {
global $wgUser, $wgContLang;
// Check for uppercase extension. We allow these filenames but check if an image
// with lowercase extension exists already
$warning = '';
$align = $wgContLang->isRtl() ? 'left' : 'right';
if( strpos( $file->getName(), '.' ) == false ) {
$partname = $file->getName();
$rawExtension = '';
} else {
list( $partname, $rawExtension ) = explode( '.', $file->getName(), 2 );
}
$sk = $wgUser->getSkin();
if ( $rawExtension != $file->getExtension() ) {
// 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::newFromText( $partname . '.' . $file->getExtension() );
$file_lc = wfLocalFile( $nt_lc );
} else {
$file_lc = false;
}
if( $file->exists() ) {
$dlink = $sk->makeKnownLinkObj( $file->getTitle() );
if ( $file->allowInlineDisplay() ) {
$dlink2 = $sk->makeImageLinkObj( $file->getTitle(), wfMsgExt( 'fileexists-thumb', 'parseinline' ),
$file->getName(), $align, array(), false, true );
} elseif ( !$file->allowInlineDisplay() && $file->isSafeFile() ) {
$icon = $file->iconThumb();
$dlink2 = '
';
break;
}
}
if ( $file->wasDeleted() && !$file->exists() ) {
# If the file existed before and was deleted, warn the user of this
# Don't bother doing so if the file exists now, however
$ltitle = SpecialPage::getTitleFor( 'Log' );
$llink = $sk->makeKnownLinkObj( $ltitle, wfMsgHtml( 'deletionlog' ),
'type=delete&page=' . $file->getTitle()->getPrefixedUrl() );
$warning .= '
' . wfMsgWikiHtml( 'filewasdeleted', $llink ) . '
';
}
return $warning;
}
/**
* Get a list of warnings
*
* @param string local filename, e.g. 'file exists', 'non-descriptive filename'
* @return array list of warning messages
*/
static function ajaxGetExistsWarning( $filename ) {
$file = wfFindFile( $filename );
if( !$file ) {
// Force local file so we have an object to do further checks against
// if there isn't an exact match...
$file = wfLocalFile( $filename );
}
$s = ' ';
if ( $file ) {
$warning = self::getExistsWarning( $file );
if ( $warning !== '' ) {
$s = "
$warning
";
}
}
return $s;
}
/**
* Render a preview of a given license for the AJAX preview on upload
*
* @param string $license
* @return string
*/
public static function ajaxGetLicensePreview( $license ) {
global $wgParser, $wgUser;
$text = '{{' . $license . '}}';
$title = Title::makeTitle( NS_IMAGE, 'Sample.jpg' );
$options = ParserOptions::newFromUser( $wgUser );
// Expand subst: first, then live templates...
$text = $wgParser->preSaveTransform( $text, $title, $wgUser, $options );
$output = $wgParser->parse( $text, $title, $options );
return $output->getText();
}
/**
* Get a list of blacklisted filename prefixes from [[MediaWiki:filename-prefix-blacklist]]
*
* @return array list of prefixes
*/
public static function getFilenamePrefixBlacklist() {
$blacklist = array();
$message = wfMsgForContent( 'filename-prefix-blacklist' );
if( $message && !( wfEmptyMsg( 'filename-prefix-blacklist', $message ) || $message == '-' ) ) {
$lines = explode( "\n", $message );
foreach( $lines as $line ) {
// Remove comment lines
$comment = substr( trim( $line ), 0, 1 );
if ( $comment == '#' || $comment == '' ) {
continue;
}
// Remove additional comments after a prefix
$comment = strpos( $line, '#' );
if ( $comment > 0 ) {
$line = substr( $line, 0, $comment-1 );
}
$blacklist[] = trim( $line );
}
}
return $blacklist;
}
/**
* 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;
$repo = RepoGroup::singleton()->getLocalRepo();
$status = $repo->storeTemp( $saveName, $tempName );
if ( !$status->isGood() ) {
$this->showError( $status->getWikiText() );
return false;
} else {
return $status->value;
}
}
/**
* 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->mDestName, $this->mTempPath );
if( !$stash ) {
# Couldn't save the file.
return false;
}
$key = mt_rand( 0, 0x7fffffff );
$_SESSION['wsUploadData'][$key] = array(
'mTempPath' => $stash,
'mFileSize' => $this->mFileSize,
'mSrcName' => $this->mSrcName,
'mFileProps' => $this->mFileProps,
'version' => self::SESSION_VERSION,
);
return $key;
}
/**
* Remove a temporarily kept file stashed by saveTempUploadedFile().
* @access private
* @return success
*/
function unsaveUploadedFile() {
global $wgOut;
$repo = RepoGroup::singleton()->getLocalRepo();
$success = $repo->freeTemp( $this->mTempPath );
if ( ! $success ) {
$wgOut->showFileDeleteError( $this->mTempPath );
return false;
} else {
return true;
}
}
/* -------------------------------------------------------------- */
/**
* @param string $error as HTML
* @access private
*/
function uploadError( $error ) {
global $wgOut;
$wgOut->addHTML( "
" . wfMsgHtml( 'uploadwarning' ) . "
\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, $wgContLang;
global $wgUseCopyrightUpload;
$this->mSessionKey = $this->stashSession();
if( !$this->mSessionKey ) {
# Couldn't save file; an error has been displayed so let's go.
return;
}
$wgOut->addHTML( "
\n" );
# 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 ) {
$delim = wfMsgExt( 'comma-separator', array( 'escapenoentities' ) );
if( $wgStrictFileExtensions ) {
# Everything not permitted is banned
$wgOut->addHTML(
'
" );
}
/* -------------------------------------------------------------- */
/**
* 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;
}
/**
* Verifies that it's ok to include the uploaded file
*
* @param string $tmpfile the full path of the temporary file to verify
* @param string $extension The filename extension that the file is to be served with
* @return mixed true of the file is verified, a WikiError object otherwise.
*/
function verify( $tmpfile, $extension ) {
#magically determine mime type
$magic=& MimeMagic::singleton();
$mime= $magic->guessMimeType($tmpfile,false);
#check mime type, if desired
global $wgVerifyMimeType;
if ($wgVerifyMimeType) {
wfDebug ( "\n\nmime: <$mime> extension: <$extension>\n\n");
#check mime type against file extension
if( !$this->verifyExtension( $mime, $extension ) ) {
return new WikiErrorMsg( 'uploadcorrupt' );
}
#check mime type blacklist
global $wgMimeTypeBlacklist;
if( isset($wgMimeTypeBlacklist) && !is_null($wgMimeTypeBlacklist)
&& $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) {
return new WikiErrorMsg( 'filetype-badmime', htmlspecialchars( $mime ) );
}
}
#check for htmlish code and javascript
if( $this->detectScript ( $tmpfile, $mime, $extension ) ) {
return new WikiErrorMsg( 'uploadscripted' );
}
/**
* Scan the uploaded file for viruses
*/
$virus= $this->detectVirus($tmpfile);
if ( $virus ) {
return new WikiErrorMsg( 'uploadvirus', htmlspecialchars($virus) );
}
wfDebug( __METHOD__.": all clear; passing.\n" );
return true;
}
/**
* Checks if the mime type of the uploaded file matches the file extension.
*
* @param string $mime the mime type of the uploaded file
* @param string $extension The filename extension that the file is to be served with
* @return bool
*/
function verifyExtension( $mime, $extension ) {
$magic =& MimeMagic::singleton();
if ( ! $mime || $mime == 'unknown' || $mime == 'unknown/unknown' )
if ( ! $magic->isRecognizableExtension( $extension ) ) {
wfDebug( __METHOD__.": passing file with unknown detected mime type; " .
"unrecognized extension '$extension', can't verify\n" );
return true;
} else {
wfDebug( __METHOD__.": rejecting file with unknown detected mime type; ".
"recognized extension '$extension', so probably invalid file\n" );
return false;
}
$match= $magic->isMatchingExtension($extension,$mime);
if ($match===NULL) {
wfDebug( __METHOD__.": no file extension known for mime type $mime, passing file\n" );
return true;
} elseif ($match===true) {
wfDebug( __METHOD__.": mime type $mime matches extension $extension, passing file\n" );
#TODO: if it's a bitmap, make sure PHP or ImageMagic resp. can handle it!
return true;
} else {
wfDebug( __METHOD__.": mime type $mime mismatches file extension $extension, rejecting file\n" );
return false;
}
}
/**
* Heuristic for detecting files that *could* contain JavaScript instructions or
* things that may look like HTML to a browser and are thus
* potentially harmful. The present implementation will produce false positives in some situations.
*
* @param string $file Pathname to the temporary upload file
* @param string $mime The mime type of the file
* @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) {
global $wgAllowTitlesInSVG;
#ugly hack: for text files, always look at the entire file.
#For binarie field, just check the first K.
if (strpos($mime,'text/')===0) $chunk = file_get_contents( $file );
else {
$fp = fopen( $file, 'rb' );
$chunk = fread( $fp, 1024 );
fclose( $fp );
}
$chunk= strtolower( $chunk );
if (!$chunk) return false;
#decode from UTF-16 if needed (could be used for obfuscation).
if (substr($chunk,0,2)=="\xfe\xff") $enc= "UTF-16BE";
elseif (substr($chunk,0,2)=="\xff\xfe") $enc= "UTF-16LE";
else $enc= NULL;
if ($enc) $chunk= iconv($enc,"ASCII//IGNORE",$chunk);
$chunk= trim($chunk);
#FIXME: convert from UTF-16 if necessarry!
wfDebug("SpecialUpload::detectScript: checking for embedded scripts and HTML stuff\n");
#check for HTML doctype
if (eregi("addHTML( "
Bad configuration: unknown virus scanner: $wgAntivirus
\n" );
return "unknown antivirus: $wgAntivirus";
}
# look up scanner configuration
$command = $wgAntivirusSetup[$wgAntivirus]["command"];
$exitCodeMap = $wgAntivirusSetup[$wgAntivirus]["codemap"];
$msgPattern = isset( $wgAntivirusSetup[$wgAntivirus]["messagepattern"] ) ?
$wgAntivirusSetup[$wgAntivirus]["messagepattern"] : null;
if ( strpos( $command,"%f" ) === false ) {
# simple pattern: append file to scan
$command .= " " . wfEscapeShellArg( $file );
} else {
# complex pattern: replace "%f" with file to scan
$command = str_replace( "%f", wfEscapeShellArg( $file ), $command );
}
wfDebug( __METHOD__.": running virus scan: $command \n" );
# execute virus scanner
$exitCode = false;
#NOTE: there's a 50 line workaround to make stderr redirection work on windows, too.
# that does not seem to be worth the pain.
# Ask me (Duesentrieb) about it if it's ever needed.
$output = array();
if ( wfIsWindows() ) {
exec( "$command", $output, $exitCode );
} else {
exec( "$command 2>&1", $output, $exitCode );
}
# map exit code to AV_xxx constants.
$mappedCode = $exitCode;
if ( $exitCodeMap ) {
if ( isset( $exitCodeMap[$exitCode] ) ) {
$mappedCode = $exitCodeMap[$exitCode];
} elseif ( isset( $exitCodeMap["*"] ) ) {
$mappedCode = $exitCodeMap["*"];
}
}
if ( $mappedCode === AV_SCAN_FAILED ) {
# scan failed (code was mapped to false by $exitCodeMap)
wfDebug( __METHOD__.": failed to scan $file (code $exitCode).\n" );
if ( $wgAntivirusRequired ) {
return "scan failed (code $exitCode)";
} else {
return NULL;
}
} else if ( $mappedCode === AV_SCAN_ABORTED ) {
# scan failed because filetype is unknown (probably imune)
wfDebug( __METHOD__.": unsupported file type $file (code $exitCode).\n" );
return NULL;
} else if ( $mappedCode === AV_NO_VIRUS ) {
# no virus found
wfDebug( __METHOD__.": file passed virus scan.\n" );
return false;
} else {
$output = join( "\n", $output );
$output = trim( $output );
if ( !$output ) {
$output = true; #if there's no output, return true
} elseif ( $msgPattern ) {
$groups = array();
if ( preg_match( $msgPattern, $output, $groups ) ) {
if ( $groups[1] ) {
$output = $groups[1];
}
}
}
wfDebug( __METHOD__.": FOUND VIRUS! scanner feedback: $output" );
return $output;
}
}
/**
* 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.
*
* @access private
*/
function checkMacBinary() {
$macbin = new MacBinary( $this->mTempPath );
if( $macbin->isValid() ) {
$dataFile = tempnam( wfTempDir(), "WikiMacBinary" );
$dataHandle = fopen( $dataFile, 'wb' );
wfDebug( "SpecialUpload::checkMacBinary: 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();
}
/**
* If we've modified the upload file we need to manually remove it
* on exit to clean up.
* @access private
*/
function cleanupTempFile() {
if ( $this->mRemoveTempFile && file_exists( $this->mTempPath ) ) {
wfDebug( "SpecialUpload::cleanupTempFile: Removing temporary file {$this->mTempPath}\n" );
unlink( $this->mTempPath );
}
}
/**
* 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
* @access private
*/
function checkOverwrite( $name ) {
$img = wfFindFile( $name );
$error = '';
if( $img ) {
global $wgUser, $wgOut;
if( $img->isLocal() ) {
if( !self::userCanReUpload( $wgUser, $img->name ) ) {
$error = 'fileexists-forbidden';
}
} else {
if( !$wgUser->isAllowed( 'reupload' ) ||
!$wgUser->isAllowed( 'reupload-shared' ) ) {
$error = "fileexists-shared-forbidden";
}
}
}
if( $error ) {
$errorText = wfMsg( $error, wfEscapeWikiText( $img->getName() ) );
return $errorText;
}
// Rockin', go ahead and upload
return true;
}
/**
* Check if a user is the last uploader
*
* @param User $user
* @param string $img, image name
* @return bool
*/
public static function userCanReUpload( User $user, $img ) {
if( $user->isAllowed( 'reupload' ) )
return true; // non-conditional
if( !$user->isAllowed( 'reupload-own' ) )
return false;
$dbr = wfGetDB( DB_SLAVE );
$row = $dbr->selectRow('image',
/* SELECT */ 'img_user',
/* WHERE */ array( 'img_name' => $img )
);
if ( !$row )
return false;
return $user->getID() == $row->img_user;
}
/**
* Display an error with a wikitext description
*/
function showError( $description ) {
global $wgOut;
$wgOut->setPageTitle( wfMsg( "internalerror" ) );
$wgOut->setRobotpolicy( "noindex,nofollow" );
$wgOut->setArticleRelated( false );
$wgOut->enableClientCache( false );
$wgOut->addWikiText( $description );
}
/**
* Get the initial image page text based on a comment and optional file status information
*/
static function getInitialPageText( $comment, $license, $copyStatus, $source ) {
global $wgUseCopyrightUpload;
if ( $wgUseCopyrightUpload ) {
if ( $license != '' ) {
$licensetxt = '== ' . wfMsgForContent( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n";
}
$pageText = '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $comment . "\n" .
'== ' . wfMsgForContent ( 'filestatus' ) . " ==\n" . $copyStatus . "\n" .
"$licensetxt" .
'== ' . wfMsgForContent ( 'filesource' ) . " ==\n" . $source ;
} else {
if ( $license != '' ) {
$filedesc = $comment == '' ? '' : '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $comment . "\n";
$pageText = $filedesc .
'== ' . wfMsgForContent ( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n";
} else {
$pageText = $comment;
}
}
return $pageText;
}
/**
* If there are rows in the deletion log for this file, show them,
* along with a nice little note for the user
*
* @param OutputPage $out
* @param string filename
*/
private function showDeletionLog( $out, $filename ) {
$reader = new LogReader(
new FauxRequest(
array(
'page' => $filename,
'type' => 'delete',
)
)
);
if( $reader->hasRows() ) {
$out->addHtml( '