MediaWiki  REL1_24
UploadBase.php
Go to the documentation of this file.
00001 <?php
00038 abstract class UploadBase {
00039     protected $mTempPath;
00040     protected $mDesiredDestName, $mDestName, $mRemoveTempFile, $mSourceType;
00041     protected $mTitle = false, $mTitleError = 0;
00042     protected $mFilteredName, $mFinalExtension;
00043     protected $mLocalFile, $mFileSize, $mFileProps;
00044     protected $mBlackListedExtensions;
00045     protected $mJavaDetected, $mSVGNSError;
00046 
00047     protected static $safeXmlEncodings = array(
00048         'UTF-8',
00049         'ISO-8859-1',
00050         'ISO-8859-2',
00051         'UTF-16',
00052         'UTF-32'
00053     );
00054 
00055     const SUCCESS = 0;
00056     const OK = 0;
00057     const EMPTY_FILE = 3;
00058     const MIN_LENGTH_PARTNAME = 4;
00059     const ILLEGAL_FILENAME = 5;
00060     const OVERWRITE_EXISTING_FILE = 7; # Not used anymore; handled by verifyTitlePermissions()
00061     const FILETYPE_MISSING = 8;
00062     const FILETYPE_BADTYPE = 9;
00063     const VERIFICATION_ERROR = 10;
00064 
00065     # HOOK_ABORTED is the new name of UPLOAD_VERIFICATION_ERROR
00066     const UPLOAD_VERIFICATION_ERROR = 11;
00067     const HOOK_ABORTED = 11;
00068     const FILE_TOO_LARGE = 12;
00069     const WINDOWS_NONASCII_FILENAME = 13;
00070     const FILENAME_TOO_LONG = 14;
00071 
00072     const SESSION_STATUS_KEY = 'wsUploadStatusData';
00073 
00078     public function getVerificationErrorCode( $error ) {
00079         $code_to_status = array(
00080             self::EMPTY_FILE => 'empty-file',
00081             self::FILE_TOO_LARGE => 'file-too-large',
00082             self::FILETYPE_MISSING => 'filetype-missing',
00083             self::FILETYPE_BADTYPE => 'filetype-banned',
00084             self::MIN_LENGTH_PARTNAME => 'filename-tooshort',
00085             self::ILLEGAL_FILENAME => 'illegal-filename',
00086             self::OVERWRITE_EXISTING_FILE => 'overwrite',
00087             self::VERIFICATION_ERROR => 'verification-error',
00088             self::HOOK_ABORTED => 'hookaborted',
00089             self::WINDOWS_NONASCII_FILENAME => 'windows-nonascii-filename',
00090             self::FILENAME_TOO_LONG => 'filename-toolong',
00091         );
00092         if ( isset( $code_to_status[$error] ) ) {
00093             return $code_to_status[$error];
00094         }
00095 
00096         return 'unknown-error';
00097     }
00098 
00104     public static function isEnabled() {
00105         global $wgEnableUploads;
00106 
00107         if ( !$wgEnableUploads ) {
00108             return false;
00109         }
00110 
00111         # Check php's file_uploads setting
00112         return wfIsHHVM() || wfIniGetBool( 'file_uploads' );
00113     }
00114 
00123     public static function isAllowed( $user ) {
00124         foreach ( array( 'upload', 'edit' ) as $permission ) {
00125             if ( !$user->isAllowed( $permission ) ) {
00126                 return $permission;
00127             }
00128         }
00129 
00130         return true;
00131     }
00132 
00133     // Upload handlers. Should probably just be a global.
00134     private static $uploadHandlers = array( 'Stash', 'File', 'Url' );
00135 
00143     public static function createFromRequest( &$request, $type = null ) {
00144         $type = $type ? $type : $request->getVal( 'wpSourceType', 'File' );
00145 
00146         if ( !$type ) {
00147             return null;
00148         }
00149 
00150         // Get the upload class
00151         $type = ucfirst( $type );
00152 
00153         // Give hooks the chance to handle this request
00154         $className = null;
00155         wfRunHooks( 'UploadCreateFromRequest', array( $type, &$className ) );
00156         if ( is_null( $className ) ) {
00157             $className = 'UploadFrom' . $type;
00158             wfDebug( __METHOD__ . ": class name: $className\n" );
00159             if ( !in_array( $type, self::$uploadHandlers ) ) {
00160                 return null;
00161             }
00162         }
00163 
00164         // Check whether this upload class is enabled
00165         if ( !call_user_func( array( $className, 'isEnabled' ) ) ) {
00166             return null;
00167         }
00168 
00169         // Check whether the request is valid
00170         if ( !call_user_func( array( $className, 'isValidRequest' ), $request ) ) {
00171             return null;
00172         }
00173 
00175         $handler = new $className;
00176 
00177         $handler->initializeFromRequest( $request );
00178 
00179         return $handler;
00180     }
00181 
00187     public static function isValidRequest( $request ) {
00188         return false;
00189     }
00190 
00191     public function __construct() {
00192     }
00193 
00200     public function getSourceType() {
00201         return null;
00202     }
00203 
00212     public function initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile = false ) {
00213         $this->mDesiredDestName = $name;
00214         if ( FileBackend::isStoragePath( $tempPath ) ) {
00215             throw new MWException( __METHOD__ . " given storage path `$tempPath`." );
00216         }
00217         $this->mTempPath = $tempPath;
00218         $this->mFileSize = $fileSize;
00219         $this->mRemoveTempFile = $removeTempFile;
00220     }
00221 
00227     abstract public function initializeFromRequest( &$request );
00228 
00233     public function fetchFile() {
00234         return Status::newGood();
00235     }
00236 
00241     public function isEmptyFile() {
00242         return empty( $this->mFileSize );
00243     }
00244 
00249     public function getFileSize() {
00250         return $this->mFileSize;
00251     }
00252 
00257     public function getTempFileSha1Base36() {
00258         return FSFile::getSha1Base36FromPath( $this->mTempPath );
00259     }
00260 
00265     function getRealPath( $srcPath ) {
00266         wfProfileIn( __METHOD__ );
00267         $repo = RepoGroup::singleton()->getLocalRepo();
00268         if ( $repo->isVirtualUrl( $srcPath ) ) {
00272             $tmpFile = $repo->getLocalCopy( $srcPath );
00273             if ( $tmpFile ) {
00274                 $tmpFile->bind( $this ); // keep alive with $this
00275             }
00276             $path = $tmpFile ? $tmpFile->getPath() : false;
00277         } else {
00278             $path = $srcPath;
00279         }
00280         wfProfileOut( __METHOD__ );
00281 
00282         return $path;
00283     }
00284 
00289     public function verifyUpload() {
00290         wfProfileIn( __METHOD__ );
00291 
00295         if ( $this->isEmptyFile() ) {
00296             wfProfileOut( __METHOD__ );
00297 
00298             return array( 'status' => self::EMPTY_FILE );
00299         }
00300 
00304         $maxSize = self::getMaxUploadSize( $this->getSourceType() );
00305         if ( $this->mFileSize > $maxSize ) {
00306             wfProfileOut( __METHOD__ );
00307 
00308             return array(
00309                 'status' => self::FILE_TOO_LARGE,
00310                 'max' => $maxSize,
00311             );
00312         }
00313 
00319         $verification = $this->verifyFile();
00320         if ( $verification !== true ) {
00321             wfProfileOut( __METHOD__ );
00322 
00323             return array(
00324                 'status' => self::VERIFICATION_ERROR,
00325                 'details' => $verification
00326             );
00327         }
00328 
00332         $result = $this->validateName();
00333         if ( $result !== true ) {
00334             wfProfileOut( __METHOD__ );
00335 
00336             return $result;
00337         }
00338 
00339         $error = '';
00340         if ( !wfRunHooks( 'UploadVerification',
00341             array( $this->mDestName, $this->mTempPath, &$error ) )
00342         ) {
00343             wfProfileOut( __METHOD__ );
00344 
00345             return array( 'status' => self::HOOK_ABORTED, 'error' => $error );
00346         }
00347 
00348         wfProfileOut( __METHOD__ );
00349 
00350         return array( 'status' => self::OK );
00351     }
00352 
00359     public function validateName() {
00360         $nt = $this->getTitle();
00361         if ( is_null( $nt ) ) {
00362             $result = array( 'status' => $this->mTitleError );
00363             if ( $this->mTitleError == self::ILLEGAL_FILENAME ) {
00364                 $result['filtered'] = $this->mFilteredName;
00365             }
00366             if ( $this->mTitleError == self::FILETYPE_BADTYPE ) {
00367                 $result['finalExt'] = $this->mFinalExtension;
00368                 if ( count( $this->mBlackListedExtensions ) ) {
00369                     $result['blacklistedExt'] = $this->mBlackListedExtensions;
00370                 }
00371             }
00372 
00373             return $result;
00374         }
00375         $this->mDestName = $this->getLocalFile()->getName();
00376 
00377         return true;
00378     }
00379 
00389     protected function verifyMimeType( $mime ) {
00390         global $wgVerifyMimeType;
00391         wfProfileIn( __METHOD__ );
00392         if ( $wgVerifyMimeType ) {
00393             wfDebug( "mime: <$mime> extension: <{$this->mFinalExtension}>\n" );
00394             global $wgMimeTypeBlacklist;
00395             if ( $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) {
00396                 wfProfileOut( __METHOD__ );
00397 
00398                 return array( 'filetype-badmime', $mime );
00399             }
00400 
00401             # Check what Internet Explorer would detect
00402             $fp = fopen( $this->mTempPath, 'rb' );
00403             $chunk = fread( $fp, 256 );
00404             fclose( $fp );
00405 
00406             $magic = MimeMagic::singleton();
00407             $extMime = $magic->guessTypesForExtension( $this->mFinalExtension );
00408             $ieTypes = $magic->getIEMimeTypes( $this->mTempPath, $chunk, $extMime );
00409             foreach ( $ieTypes as $ieType ) {
00410                 if ( $this->checkFileExtension( $ieType, $wgMimeTypeBlacklist ) ) {
00411                     wfProfileOut( __METHOD__ );
00412 
00413                     return array( 'filetype-bad-ie-mime', $ieType );
00414                 }
00415             }
00416         }
00417 
00418         wfProfileOut( __METHOD__ );
00419 
00420         return true;
00421     }
00422 
00428     protected function verifyFile() {
00429         global $wgVerifyMimeType;
00430         wfProfileIn( __METHOD__ );
00431 
00432         $status = $this->verifyPartialFile();
00433         if ( $status !== true ) {
00434             wfProfileOut( __METHOD__ );
00435 
00436             return $status;
00437         }
00438 
00439         $this->mFileProps = FSFile::getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
00440         $mime = $this->mFileProps['mime'];
00441 
00442         if ( $wgVerifyMimeType ) {
00443             # XXX: Missing extension will be caught by validateName() via getTitle()
00444             if ( $this->mFinalExtension != '' && !$this->verifyExtension( $mime, $this->mFinalExtension ) ) {
00445                 wfProfileOut( __METHOD__ );
00446 
00447                 return array( 'filetype-mime-mismatch', $this->mFinalExtension, $mime );
00448             }
00449         }
00450 
00451         $handler = MediaHandler::getHandler( $mime );
00452         if ( $handler ) {
00453             $handlerStatus = $handler->verifyUpload( $this->mTempPath );
00454             if ( !$handlerStatus->isOK() ) {
00455                 $errors = $handlerStatus->getErrorsArray();
00456                 wfProfileOut( __METHOD__ );
00457 
00458                 return reset( $errors );
00459             }
00460         }
00461 
00462         wfRunHooks( 'UploadVerifyFile', array( $this, $mime, &$status ) );
00463         if ( $status !== true ) {
00464             wfProfileOut( __METHOD__ );
00465 
00466             return $status;
00467         }
00468 
00469         wfDebug( __METHOD__ . ": all clear; passing.\n" );
00470         wfProfileOut( __METHOD__ );
00471 
00472         return true;
00473     }
00474 
00483     protected function verifyPartialFile() {
00484         global $wgAllowJavaUploads, $wgDisableUploadScriptChecks;
00485         wfProfileIn( __METHOD__ );
00486 
00487         # getTitle() sets some internal parameters like $this->mFinalExtension
00488         $this->getTitle();
00489 
00490         $this->mFileProps = FSFile::getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
00491 
00492         # check MIME type, if desired
00493         $mime = $this->mFileProps['file-mime'];
00494         $status = $this->verifyMimeType( $mime );
00495         if ( $status !== true ) {
00496             wfProfileOut( __METHOD__ );
00497 
00498             return $status;
00499         }
00500 
00501         # check for htmlish code and javascript
00502         if ( !$wgDisableUploadScriptChecks ) {
00503             if ( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) {
00504                 wfProfileOut( __METHOD__ );
00505 
00506                 return array( 'uploadscripted' );
00507             }
00508             if ( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) {
00509                 $svgStatus = $this->detectScriptInSvg( $this->mTempPath );
00510                 if ( $svgStatus !== false ) {
00511                     wfProfileOut( __METHOD__ );
00512 
00513                     return $svgStatus;
00514                 }
00515             }
00516         }
00517 
00518         # Check for Java applets, which if uploaded can bypass cross-site
00519         # restrictions.
00520         if ( !$wgAllowJavaUploads ) {
00521             $this->mJavaDetected = false;
00522             $zipStatus = ZipDirectoryReader::read( $this->mTempPath,
00523                 array( $this, 'zipEntryCallback' ) );
00524             if ( !$zipStatus->isOK() ) {
00525                 $errors = $zipStatus->getErrorsArray();
00526                 $error = reset( $errors );
00527                 if ( $error[0] !== 'zip-wrong-format' ) {
00528                     wfProfileOut( __METHOD__ );
00529 
00530                     return $error;
00531                 }
00532             }
00533             if ( $this->mJavaDetected ) {
00534                 wfProfileOut( __METHOD__ );
00535 
00536                 return array( 'uploadjava' );
00537             }
00538         }
00539 
00540         # Scan the uploaded file for viruses
00541         $virus = $this->detectVirus( $this->mTempPath );
00542         if ( $virus ) {
00543             wfProfileOut( __METHOD__ );
00544 
00545             return array( 'uploadvirus', $virus );
00546         }
00547 
00548         wfProfileOut( __METHOD__ );
00549 
00550         return true;
00551     }
00552 
00558     function zipEntryCallback( $entry ) {
00559         $names = array( $entry['name'] );
00560 
00561         // If there is a null character, cut off the name at it, because JDK's
00562         // ZIP_GetEntry() uses strcmp() if the name hashes match. If a file name
00563         // were constructed which had ".class\0" followed by a string chosen to
00564         // make the hash collide with the truncated name, that file could be
00565         // returned in response to a request for the .class file.
00566         $nullPos = strpos( $entry['name'], "\000" );
00567         if ( $nullPos !== false ) {
00568             $names[] = substr( $entry['name'], 0, $nullPos );
00569         }
00570 
00571         // If there is a trailing slash in the file name, we have to strip it,
00572         // because that's what ZIP_GetEntry() does.
00573         if ( preg_grep( '!\.class/?$!', $names ) ) {
00574             $this->mJavaDetected = true;
00575         }
00576     }
00577 
00587     public function verifyPermissions( $user ) {
00588         return $this->verifyTitlePermissions( $user );
00589     }
00590 
00602     public function verifyTitlePermissions( $user ) {
00607         $nt = $this->getTitle();
00608         if ( is_null( $nt ) ) {
00609             return true;
00610         }
00611         $permErrors = $nt->getUserPermissionsErrors( 'edit', $user );
00612         $permErrorsUpload = $nt->getUserPermissionsErrors( 'upload', $user );
00613         if ( !$nt->exists() ) {
00614             $permErrorsCreate = $nt->getUserPermissionsErrors( 'create', $user );
00615         } else {
00616             $permErrorsCreate = array();
00617         }
00618         if ( $permErrors || $permErrorsUpload || $permErrorsCreate ) {
00619             $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsUpload, $permErrors ) );
00620             $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsCreate, $permErrors ) );
00621 
00622             return $permErrors;
00623         }
00624 
00625         $overwriteError = $this->checkOverwrite( $user );
00626         if ( $overwriteError !== true ) {
00627             return array( $overwriteError );
00628         }
00629 
00630         return true;
00631     }
00632 
00640     public function checkWarnings() {
00641         global $wgLang;
00642         wfProfileIn( __METHOD__ );
00643 
00644         $warnings = array();
00645 
00646         $localFile = $this->getLocalFile();
00647         $filename = $localFile->getName();
00648 
00653         $comparableName = str_replace( ' ', '_', $this->mDesiredDestName );
00654         $comparableName = Title::capitalize( $comparableName, NS_FILE );
00655 
00656         if ( $this->mDesiredDestName != $filename && $comparableName != $filename ) {
00657             $warnings['badfilename'] = $filename;
00658             // Debugging for bug 62241
00659             wfDebugLog( 'upload', "Filename: '$filename', mDesiredDestName: "
00660                 . "'$this->mDesiredDestName', comparableName: '$comparableName'" );
00661         }
00662 
00663         // Check whether the file extension is on the unwanted list
00664         global $wgCheckFileExtensions, $wgFileExtensions;
00665         if ( $wgCheckFileExtensions ) {
00666             $extensions = array_unique( $wgFileExtensions );
00667             if ( !$this->checkFileExtension( $this->mFinalExtension, $extensions ) ) {
00668                 $warnings['filetype-unwanted-type'] = array( $this->mFinalExtension,
00669                     $wgLang->commaList( $extensions ), count( $extensions ) );
00670             }
00671         }
00672 
00673         global $wgUploadSizeWarning;
00674         if ( $wgUploadSizeWarning && ( $this->mFileSize > $wgUploadSizeWarning ) ) {
00675             $warnings['large-file'] = array( $wgUploadSizeWarning, $this->mFileSize );
00676         }
00677 
00678         if ( $this->mFileSize == 0 ) {
00679             $warnings['emptyfile'] = true;
00680         }
00681 
00682         $exists = self::getExistsWarning( $localFile );
00683         if ( $exists !== false ) {
00684             $warnings['exists'] = $exists;
00685         }
00686 
00687         // Check dupes against existing files
00688         $hash = $this->getTempFileSha1Base36();
00689         $dupes = RepoGroup::singleton()->findBySha1( $hash );
00690         $title = $this->getTitle();
00691         // Remove all matches against self
00692         foreach ( $dupes as $key => $dupe ) {
00693             if ( $title->equals( $dupe->getTitle() ) ) {
00694                 unset( $dupes[$key] );
00695             }
00696         }
00697         if ( $dupes ) {
00698             $warnings['duplicate'] = $dupes;
00699         }
00700 
00701         // Check dupes against archives
00702         $archivedImage = new ArchivedFile( null, 0, "{$hash}.{$this->mFinalExtension}" );
00703         if ( $archivedImage->getID() > 0 ) {
00704             if ( $archivedImage->userCan( File::DELETED_FILE ) ) {
00705                 $warnings['duplicate-archive'] = $archivedImage->getName();
00706             } else {
00707                 $warnings['duplicate-archive'] = '';
00708             }
00709         }
00710 
00711         wfProfileOut( __METHOD__ );
00712 
00713         return $warnings;
00714     }
00715 
00727     public function performUpload( $comment, $pageText, $watch, $user ) {
00728         wfProfileIn( __METHOD__ );
00729 
00730         $status = $this->getLocalFile()->upload(
00731             $this->mTempPath,
00732             $comment,
00733             $pageText,
00734             File::DELETE_SOURCE,
00735             $this->mFileProps,
00736             false,
00737             $user
00738         );
00739 
00740         if ( $status->isGood() ) {
00741             if ( $watch ) {
00742                 WatchAction::doWatch(
00743                     $this->getLocalFile()->getTitle(),
00744                     $user,
00745                     WatchedItem::IGNORE_USER_RIGHTS
00746                 );
00747             }
00748             wfRunHooks( 'UploadComplete', array( &$this ) );
00749         }
00750 
00751         wfProfileOut( __METHOD__ );
00752 
00753         return $status;
00754     }
00755 
00762     public function getTitle() {
00763         if ( $this->mTitle !== false ) {
00764             return $this->mTitle;
00765         }
00766         /* Assume that if a user specified File:Something.jpg, this is an error
00767          * and that the namespace prefix needs to be stripped of.
00768          */
00769         $title = Title::newFromText( $this->mDesiredDestName );
00770         if ( $title && $title->getNamespace() == NS_FILE ) {
00771             $this->mFilteredName = $title->getDBkey();
00772         } else {
00773             $this->mFilteredName = $this->mDesiredDestName;
00774         }
00775 
00776         # oi_archive_name is max 255 bytes, which include a timestamp and an
00777         # exclamation mark, so restrict file name to 240 bytes.
00778         if ( strlen( $this->mFilteredName ) > 240 ) {
00779             $this->mTitleError = self::FILENAME_TOO_LONG;
00780             $this->mTitle = null;
00781 
00782             return $this->mTitle;
00783         }
00784 
00790         $this->mFilteredName = wfStripIllegalFilenameChars( $this->mFilteredName );
00791         /* Normalize to title form before we do any further processing */
00792         $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
00793         if ( is_null( $nt ) ) {
00794             $this->mTitleError = self::ILLEGAL_FILENAME;
00795             $this->mTitle = null;
00796 
00797             return $this->mTitle;
00798         }
00799         $this->mFilteredName = $nt->getDBkey();
00800 
00805         list( $partname, $ext ) = $this->splitExtensions( $this->mFilteredName );
00806 
00807         if ( count( $ext ) ) {
00808             $this->mFinalExtension = trim( $ext[count( $ext ) - 1] );
00809         } else {
00810             $this->mFinalExtension = '';
00811 
00812             # No extension, try guessing one
00813             $magic = MimeMagic::singleton();
00814             $mime = $magic->guessMimeType( $this->mTempPath );
00815             if ( $mime !== 'unknown/unknown' ) {
00816                 # Get a space separated list of extensions
00817                 $extList = $magic->getExtensionsForType( $mime );
00818                 if ( $extList ) {
00819                     # Set the extension to the canonical extension
00820                     $this->mFinalExtension = strtok( $extList, ' ' );
00821 
00822                     # Fix up the other variables
00823                     $this->mFilteredName .= ".{$this->mFinalExtension}";
00824                     $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
00825                     $ext = array( $this->mFinalExtension );
00826                 }
00827             }
00828         }
00829 
00830         /* Don't allow users to override the blacklist (check file extension) */
00831         global $wgCheckFileExtensions, $wgStrictFileExtensions;
00832         global $wgFileExtensions, $wgFileBlacklist;
00833 
00834         $blackListedExtensions = $this->checkFileExtensionList( $ext, $wgFileBlacklist );
00835 
00836         if ( $this->mFinalExtension == '' ) {
00837             $this->mTitleError = self::FILETYPE_MISSING;
00838             $this->mTitle = null;
00839 
00840             return $this->mTitle;
00841         } elseif ( $blackListedExtensions ||
00842             ( $wgCheckFileExtensions && $wgStrictFileExtensions &&
00843                 !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) )
00844         ) {
00845             $this->mBlackListedExtensions = $blackListedExtensions;
00846             $this->mTitleError = self::FILETYPE_BADTYPE;
00847             $this->mTitle = null;
00848 
00849             return $this->mTitle;
00850         }
00851 
00852         // Windows may be broken with special characters, see bug 1780
00853         if ( !preg_match( '/^[\x0-\x7f]*$/', $nt->getText() )
00854             && !RepoGroup::singleton()->getLocalRepo()->backendSupportsUnicodePaths()
00855         ) {
00856             $this->mTitleError = self::WINDOWS_NONASCII_FILENAME;
00857             $this->mTitle = null;
00858 
00859             return $this->mTitle;
00860         }
00861 
00862         # If there was more than one "extension", reassemble the base
00863         # filename to prevent bogus complaints about length
00864         if ( count( $ext ) > 1 ) {
00865             $iterations = count( $ext ) - 1;
00866             for ( $i = 0; $i < $iterations; $i++ ) {
00867                 $partname .= '.' . $ext[$i];
00868             }
00869         }
00870 
00871         if ( strlen( $partname ) < 1 ) {
00872             $this->mTitleError = self::MIN_LENGTH_PARTNAME;
00873             $this->mTitle = null;
00874 
00875             return $this->mTitle;
00876         }
00877 
00878         $this->mTitle = $nt;
00879 
00880         return $this->mTitle;
00881     }
00882 
00888     public function getLocalFile() {
00889         if ( is_null( $this->mLocalFile ) ) {
00890             $nt = $this->getTitle();
00891             $this->mLocalFile = is_null( $nt ) ? null : wfLocalFile( $nt );
00892         }
00893 
00894         return $this->mLocalFile;
00895     }
00896 
00912     public function stashFile( User $user = null ) {
00913         // was stashSessionFile
00914         wfProfileIn( __METHOD__ );
00915 
00916         $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash( $user );
00917         $file = $stash->stashFile( $this->mTempPath, $this->getSourceType() );
00918         $this->mLocalFile = $file;
00919 
00920         wfProfileOut( __METHOD__ );
00921 
00922         return $file;
00923     }
00924 
00931     public function stashFileGetKey() {
00932         return $this->stashFile()->getFileKey();
00933     }
00934 
00940     public function stashSession() {
00941         return $this->stashFileGetKey();
00942     }
00943 
00948     public function cleanupTempFile() {
00949         if ( $this->mRemoveTempFile && $this->mTempPath && file_exists( $this->mTempPath ) ) {
00950             wfDebug( __METHOD__ . ": Removing temporary file {$this->mTempPath}\n" );
00951             unlink( $this->mTempPath );
00952         }
00953     }
00954 
00955     public function getTempPath() {
00956         return $this->mTempPath;
00957     }
00958 
00968     public static function splitExtensions( $filename ) {
00969         $bits = explode( '.', $filename );
00970         $basename = array_shift( $bits );
00971 
00972         return array( $basename, $bits );
00973     }
00974 
00983     public static function checkFileExtension( $ext, $list ) {
00984         return in_array( strtolower( $ext ), $list );
00985     }
00986 
00995     public static function checkFileExtensionList( $ext, $list ) {
00996         return array_intersect( array_map( 'strtolower', $ext ), $list );
00997     }
00998 
01006     public static function verifyExtension( $mime, $extension ) {
01007         $magic = MimeMagic::singleton();
01008 
01009         if ( !$mime || $mime == 'unknown' || $mime == 'unknown/unknown' ) {
01010             if ( !$magic->isRecognizableExtension( $extension ) ) {
01011                 wfDebug( __METHOD__ . ": passing file with unknown detected mime type; " .
01012                     "unrecognized extension '$extension', can't verify\n" );
01013 
01014                 return true;
01015             } else {
01016                 wfDebug( __METHOD__ . ": rejecting file with unknown detected mime type; " .
01017                     "recognized extension '$extension', so probably invalid file\n" );
01018 
01019                 return false;
01020             }
01021         }
01022 
01023         $match = $magic->isMatchingExtension( $extension, $mime );
01024 
01025         if ( $match === null ) {
01026             if ( $magic->getTypesForExtension( $extension ) !== null ) {
01027                 wfDebug( __METHOD__ . ": No extension known for $mime, but we know a mime for $extension\n" );
01028 
01029                 return false;
01030             } else {
01031                 wfDebug( __METHOD__ . ": no file extension known for mime type $mime, passing file\n" );
01032 
01033                 return true;
01034             }
01035         } elseif ( $match === true ) {
01036             wfDebug( __METHOD__ . ": mime type $mime matches extension $extension, passing file\n" );
01037 
01039             return true;
01040         } else {
01041             wfDebug( __METHOD__
01042                 . ": mime type $mime mismatches file extension $extension, rejecting file\n" );
01043 
01044             return false;
01045         }
01046     }
01047 
01059     public static function detectScript( $file, $mime, $extension ) {
01060         global $wgAllowTitlesInSVG;
01061         wfProfileIn( __METHOD__ );
01062 
01063         # ugly hack: for text files, always look at the entire file.
01064         # For binary field, just check the first K.
01065 
01066         if ( strpos( $mime, 'text/' ) === 0 ) {
01067             $chunk = file_get_contents( $file );
01068         } else {
01069             $fp = fopen( $file, 'rb' );
01070             $chunk = fread( $fp, 1024 );
01071             fclose( $fp );
01072         }
01073 
01074         $chunk = strtolower( $chunk );
01075 
01076         if ( !$chunk ) {
01077             wfProfileOut( __METHOD__ );
01078 
01079             return false;
01080         }
01081 
01082         # decode from UTF-16 if needed (could be used for obfuscation).
01083         if ( substr( $chunk, 0, 2 ) == "\xfe\xff" ) {
01084             $enc = 'UTF-16BE';
01085         } elseif ( substr( $chunk, 0, 2 ) == "\xff\xfe" ) {
01086             $enc = 'UTF-16LE';
01087         } else {
01088             $enc = null;
01089         }
01090 
01091         if ( $enc ) {
01092             $chunk = iconv( $enc, "ASCII//IGNORE", $chunk );
01093         }
01094 
01095         $chunk = trim( $chunk );
01096 
01098         wfDebug( __METHOD__ . ": checking for embedded scripts and HTML stuff\n" );
01099 
01100         # check for HTML doctype
01101         if ( preg_match( "/<!DOCTYPE *X?HTML/i", $chunk ) ) {
01102             wfProfileOut( __METHOD__ );
01103 
01104             return true;
01105         }
01106 
01107         // Some browsers will interpret obscure xml encodings as UTF-8, while
01108         // PHP/expat will interpret the given encoding in the xml declaration (bug 47304)
01109         if ( $extension == 'svg' || strpos( $mime, 'image/svg' ) === 0 ) {
01110             if ( self::checkXMLEncodingMissmatch( $file ) ) {
01111                 wfProfileOut( __METHOD__ );
01112 
01113                 return true;
01114             }
01115         }
01116 
01132         $tags = array(
01133             '<a href',
01134             '<body',
01135             '<head',
01136             '<html', #also in safari
01137             '<img',
01138             '<pre',
01139             '<script', #also in safari
01140             '<table'
01141         );
01142 
01143         if ( !$wgAllowTitlesInSVG && $extension !== 'svg' && $mime !== 'image/svg' ) {
01144             $tags[] = '<title';
01145         }
01146 
01147         foreach ( $tags as $tag ) {
01148             if ( false !== strpos( $chunk, $tag ) ) {
01149                 wfDebug( __METHOD__ . ": found something that may make it be mistaken for html: $tag\n" );
01150                 wfProfileOut( __METHOD__ );
01151 
01152                 return true;
01153             }
01154         }
01155 
01156         /*
01157          * look for JavaScript
01158          */
01159 
01160         # resolve entity-refs to look at attributes. may be harsh on big files... cache result?
01161         $chunk = Sanitizer::decodeCharReferences( $chunk );
01162 
01163         # look for script-types
01164         if ( preg_match( '!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim', $chunk ) ) {
01165             wfDebug( __METHOD__ . ": found script types\n" );
01166             wfProfileOut( __METHOD__ );
01167 
01168             return true;
01169         }
01170 
01171         # look for html-style script-urls
01172         if ( preg_match( '!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
01173             wfDebug( __METHOD__ . ": found html-style script urls\n" );
01174             wfProfileOut( __METHOD__ );
01175 
01176             return true;
01177         }
01178 
01179         # look for css-style script-urls
01180         if ( preg_match( '!url\s*\(\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
01181             wfDebug( __METHOD__ . ": found css-style script urls\n" );
01182             wfProfileOut( __METHOD__ );
01183 
01184             return true;
01185         }
01186 
01187         wfDebug( __METHOD__ . ": no scripts found\n" );
01188         wfProfileOut( __METHOD__ );
01189 
01190         return false;
01191     }
01192 
01200     public static function checkXMLEncodingMissmatch( $file ) {
01201         global $wgSVGMetadataCutoff;
01202         $contents = file_get_contents( $file, false, null, -1, $wgSVGMetadataCutoff );
01203         $encodingRegex = '!encoding[ \t\n\r]*=[ \t\n\r]*[\'"](.*?)[\'"]!si';
01204 
01205         if ( preg_match( "!<\?xml\b(.*?)\?>!si", $contents, $matches ) ) {
01206             if ( preg_match( $encodingRegex, $matches[1], $encMatch )
01207                 && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
01208             ) {
01209                 wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'\n" );
01210 
01211                 return true;
01212             }
01213         } elseif ( preg_match( "!<\?xml\b!si", $contents ) ) {
01214             // Start of XML declaration without an end in the first $wgSVGMetadataCutoff
01215             // bytes. There shouldn't be a legitimate reason for this to happen.
01216             wfDebug( __METHOD__ . ": Unmatched XML declaration start\n" );
01217 
01218             return true;
01219         } elseif ( substr( $contents, 0, 4 ) == "\x4C\x6F\xA7\x94" ) {
01220             // EBCDIC encoded XML
01221             wfDebug( __METHOD__ . ": EBCDIC Encoded XML\n" );
01222 
01223             return true;
01224         }
01225 
01226         // It's possible the file is encoded with multi-byte encoding, so re-encode attempt to
01227         // detect the encoding in case is specifies an encoding not whitelisted in self::$safeXmlEncodings
01228         $attemptEncodings = array( 'UTF-16', 'UTF-16BE', 'UTF-32', 'UTF-32BE' );
01229         foreach ( $attemptEncodings as $encoding ) {
01230             wfSuppressWarnings();
01231             $str = iconv( $encoding, 'UTF-8', $contents );
01232             wfRestoreWarnings();
01233             if ( $str != '' && preg_match( "!<\?xml\b(.*?)\?>!si", $str, $matches ) ) {
01234                 if ( preg_match( $encodingRegex, $matches[1], $encMatch )
01235                     && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
01236                 ) {
01237                     wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'\n" );
01238 
01239                     return true;
01240                 }
01241             } elseif ( $str != '' && preg_match( "!<\?xml\b!si", $str ) ) {
01242                 // Start of XML declaration without an end in the first $wgSVGMetadataCutoff
01243                 // bytes. There shouldn't be a legitimate reason for this to happen.
01244                 wfDebug( __METHOD__ . ": Unmatched XML declaration start\n" );
01245 
01246                 return true;
01247             }
01248         }
01249 
01250         return false;
01251     }
01252 
01257     protected function detectScriptInSvg( $filename ) {
01258         $this->mSVGNSError = false;
01259         $check = new XmlTypeCheck(
01260             $filename,
01261             array( $this, 'checkSvgScriptCallback' ),
01262             true,
01263             array( 'processing_instruction_handler' => 'UploadBase::checkSvgPICallback' )
01264         );
01265         if ( $check->wellFormed !== true ) {
01266             // Invalid xml (bug 58553)
01267             return array( 'uploadinvalidxml' );
01268         } elseif ( $check->filterMatch ) {
01269             if ( $this->mSVGNSError ) {
01270                 return array( 'uploadscriptednamespace', $this->mSVGNSError );
01271             }
01272 
01273             return array( 'uploadscripted' );
01274         }
01275 
01276         return false;
01277     }
01278 
01285     public static function checkSvgPICallback( $target, $data ) {
01286         // Don't allow external stylesheets (bug 57550)
01287         if ( preg_match( '/xml-stylesheet/i', $target ) ) {
01288             return true;
01289         }
01290 
01291         return false;
01292     }
01293 
01300     public function checkSvgScriptCallback( $element, $attribs, $data = null ) {
01301 
01302         list( $namespace, $strippedElement ) = $this->splitXmlNamespace( $element );
01303 
01304         // We specifically don't include:
01305         // http://www.w3.org/1999/xhtml (bug 60771)
01306         static $validNamespaces = array(
01307             '',
01308             'adobe:ns:meta/',
01309             'http://creativecommons.org/ns#',
01310             'http://inkscape.sourceforge.net/dtd/sodipodi-0.dtd',
01311             'http://ns.adobe.com/adobeillustrator/10.0/',
01312             'http://ns.adobe.com/adobesvgviewerextensions/3.0/',
01313             'http://ns.adobe.com/extensibility/1.0/',
01314             'http://ns.adobe.com/flows/1.0/',
01315             'http://ns.adobe.com/illustrator/1.0/',
01316             'http://ns.adobe.com/imagereplacement/1.0/',
01317             'http://ns.adobe.com/pdf/1.3/',
01318             'http://ns.adobe.com/photoshop/1.0/',
01319             'http://ns.adobe.com/saveforweb/1.0/',
01320             'http://ns.adobe.com/variables/1.0/',
01321             'http://ns.adobe.com/xap/1.0/',
01322             'http://ns.adobe.com/xap/1.0/g/',
01323             'http://ns.adobe.com/xap/1.0/g/img/',
01324             'http://ns.adobe.com/xap/1.0/mm/',
01325             'http://ns.adobe.com/xap/1.0/rights/',
01326             'http://ns.adobe.com/xap/1.0/stype/dimensions#',
01327             'http://ns.adobe.com/xap/1.0/stype/font#',
01328             'http://ns.adobe.com/xap/1.0/stype/manifestitem#',
01329             'http://ns.adobe.com/xap/1.0/stype/resourceevent#',
01330             'http://ns.adobe.com/xap/1.0/stype/resourceref#',
01331             'http://ns.adobe.com/xap/1.0/t/pg/',
01332             'http://purl.org/dc/elements/1.1/',
01333             'http://purl.org/dc/elements/1.1',
01334             'http://schemas.microsoft.com/visio/2003/svgextensions/',
01335             'http://sodipodi.sourceforge.net/dtd/sodipodi-0.dtd',
01336             'http://taptrix.com/inkpad/svg_extensions',
01337             'http://web.resource.org/cc/',
01338             'http://www.freesoftware.fsf.org/bkchem/cdml',
01339             'http://www.inkscape.org/namespaces/inkscape',
01340             'http://www.opengis.net/gml',
01341             'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
01342             'http://www.w3.org/2000/svg',
01343             'http://www.w3.org/tr/rec-rdf-syntax/',
01344         );
01345 
01346         if ( !in_array( $namespace, $validNamespaces ) ) {
01347             wfDebug( __METHOD__ . ": Non-svg namespace '$namespace' in uploaded file.\n" );
01349             $this->mSVGNSError = $namespace;
01350 
01351             return true;
01352         }
01353 
01354         /*
01355          * check for elements that can contain javascript
01356          */
01357         if ( $strippedElement == 'script' ) {
01358             wfDebug( __METHOD__ . ": Found script element '$element' in uploaded file.\n" );
01359 
01360             return true;
01361         }
01362 
01363         # e.g., <svg xmlns="http://www.w3.org/2000/svg">
01364         #  <handler xmlns:ev="http://www.w3.org/2001/xml-events" ev:event="load">alert(1)</handler> </svg>
01365         if ( $strippedElement == 'handler' ) {
01366             wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file.\n" );
01367 
01368             return true;
01369         }
01370 
01371         # SVG reported in Feb '12 that used xml:stylesheet to generate javascript block
01372         if ( $strippedElement == 'stylesheet' ) {
01373             wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file.\n" );
01374 
01375             return true;
01376         }
01377 
01378         # Block iframes, in case they pass the namespace check
01379         if ( $strippedElement == 'iframe' ) {
01380             wfDebug( __METHOD__ . ": iframe in uploaded file.\n" );
01381 
01382             return true;
01383         }
01384 
01385         # Check <style> css
01386         if ( $strippedElement == 'style'
01387             && self::checkCssFragment( Sanitizer::normalizeCss( $data ) )
01388         ) {
01389             wfDebug( __METHOD__ . ": hostile css in style element.\n" );
01390             return true;
01391         }
01392 
01393         foreach ( $attribs as $attrib => $value ) {
01394             $stripped = $this->stripXmlNamespace( $attrib );
01395             $value = strtolower( $value );
01396 
01397             if ( substr( $stripped, 0, 2 ) == 'on' ) {
01398                 wfDebug( __METHOD__
01399                     . ": Found event-handler attribute '$attrib'='$value' in uploaded file.\n" );
01400 
01401                 return true;
01402             }
01403 
01404             # href with non-local target (don't allow http://, javascript:, etc)
01405             if ( $stripped == 'href'
01406                 && strpos( $value, 'data:' ) !== 0
01407                 && strpos( $value, '#' ) !== 0
01408             ) {
01409                 if ( !( $strippedElement === 'a'
01410                     && preg_match( '!^https?://!im', $value ) )
01411                 ) {
01412                     wfDebug( __METHOD__ . ": Found href attribute <$strippedElement "
01413                         . "'$attrib'='$value' in uploaded file.\n" );
01414 
01415                     return true;
01416                 }
01417             }
01418 
01419             # href with embedded svg as target
01420             if ( $stripped == 'href' && preg_match( '!data:[^,]*image/svg[^,]*,!sim', $value ) ) {
01421                 wfDebug( __METHOD__ . ": Found href to embedded svg "
01422                     . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file.\n" );
01423 
01424                 return true;
01425             }
01426 
01427             # href with embedded (text/xml) svg as target
01428             if ( $stripped == 'href' && preg_match( '!data:[^,]*text/xml[^,]*,!sim', $value ) ) {
01429                 wfDebug( __METHOD__ . ": Found href to embedded svg "
01430                     . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file.\n" );
01431 
01432                 return true;
01433             }
01434 
01435             # Change href with animate from (http://html5sec.org/#137). This doesn't seem
01436             # possible without embedding the svg, but filter here in case.
01437             if ( $stripped == 'from'
01438                 && $strippedElement === 'animate'
01439                 && !preg_match( '!^https?://!im', $value )
01440             ) {
01441                 wfDebug( __METHOD__ . ": Found animate that might be changing href using from "
01442                     . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file.\n" );
01443 
01444                 return true;
01445             }
01446 
01447             # use set/animate to add event-handler attribute to parent
01448             if ( ( $strippedElement == 'set' || $strippedElement == 'animate' )
01449                 && $stripped == 'attributename'
01450                 && substr( $value, 0, 2 ) == 'on'
01451             ) {
01452                 wfDebug( __METHOD__ . ": Found svg setting event-handler attribute with "
01453                     . "\"<$strippedElement $stripped='$value'...\" in uploaded file.\n" );
01454 
01455                 return true;
01456             }
01457 
01458             # use set to add href attribute to parent element
01459             if ( $strippedElement == 'set'
01460                 && $stripped == 'attributename'
01461                 && strpos( $value, 'href' ) !== false
01462             ) {
01463                 wfDebug( __METHOD__ . ": Found svg setting href attribute '$value' in uploaded file.\n" );
01464 
01465                 return true;
01466             }
01467 
01468             # use set to add a remote / data / script target to an element
01469             if ( $strippedElement == 'set'
01470                 && $stripped == 'to'
01471                 && preg_match( '!(http|https|data|script):!sim', $value )
01472             ) {
01473                 wfDebug( __METHOD__ . ": Found svg setting attribute to '$value' in uploaded file.\n" );
01474 
01475                 return true;
01476             }
01477 
01478             # use handler attribute with remote / data / script
01479             if ( $stripped == 'handler' && preg_match( '!(http|https|data|script):!sim', $value ) ) {
01480                 wfDebug( __METHOD__ . ": Found svg setting handler with remote/data/script "
01481                     . "'$attrib'='$value' in uploaded file.\n" );
01482 
01483                 return true;
01484             }
01485 
01486             # use CSS styles to bring in remote code
01487             if ( $stripped == 'style'
01488                 && self::checkCssFragment( Sanitizer::normalizeCss( $value ) )
01489             ) {
01490                 wfDebug( __METHOD__ . ": Found svg setting a style with "
01491                     . "remote url '$attrib'='$value' in uploaded file.\n" );
01492                 return true;
01493             }
01494 
01495             # Several attributes can include css, css character escaping isn't allowed
01496             $cssAttrs = array( 'font', 'clip-path', 'fill', 'filter', 'marker',
01497                 'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke' );
01498             if ( in_array( $stripped, $cssAttrs )
01499                 && self::checkCssFragment( $value )
01500             ) {
01501                 wfDebug( __METHOD__ . ": Found svg setting a style with "
01502                     . "remote url '$attrib'='$value' in uploaded file.\n" );
01503                 return true;
01504             }
01505 
01506             # image filters can pull in url, which could be svg that executes scripts
01507             if ( $strippedElement == 'image'
01508                 && $stripped == 'filter'
01509                 && preg_match( '!url\s*\(!sim', $value )
01510             ) {
01511                 wfDebug( __METHOD__ . ": Found image filter with url: "
01512                     . "\"<$strippedElement $stripped='$value'...\" in uploaded file.\n" );
01513 
01514                 return true;
01515             }
01516         }
01517 
01518         return false; //No scripts detected
01519     }
01520 
01528     private static function checkCssFragment( $value ) {
01529 
01530         # Forbid external stylesheets, for both reliability and to protect viewer's privacy
01531         if ( strpos( $value, '@import' ) !== false ) {
01532             return true;
01533         }
01534 
01535         # We allow @font-face to embed fonts with data: urls, so we snip the string
01536         # 'url' out so this case won't match when we check for urls below
01537         $pattern = '!(@font-face\s*{[^}]*src:)url(\("data:;base64,)!im';
01538         $value = preg_replace( $pattern, '$1$2', $value );
01539 
01540         # Check for remote and executable CSS. Unlike in Sanitizer::checkCss, the CSS
01541         # properties filter and accelerator don't seem to be useful for xss in SVG files.
01542         # Expression and -o-link don't seem to work either, but filtering them here in case.
01543         # Additionally, we catch remote urls like url("http:..., url('http:..., url(http:...,
01544         # but not local ones such as url("#..., url('#..., url(#....
01545         if ( preg_match( '!expression
01546                 | -o-link\s*:
01547                 | -o-link-source\s*:
01548                 | -o-replace\s*:!imx', $value ) ) {
01549             return true;
01550         }
01551 
01552         if ( preg_match_all(
01553                 "!(\s*(url|image|image-set)\s*\(\s*[\"']?\s*[^#]+.*?\))!sim",
01554                 $value,
01555                 $matches
01556             ) !== 0
01557         ) {
01558             # TODO: redo this in one regex. Until then, url("#whatever") matches the first
01559             foreach ( $matches[1] as $match ) {
01560                 if ( !preg_match( "!\s*(url|image|image-set)\s*\(\s*(#|'#|\"#)!im", $match ) ) {
01561                     return true;
01562                 }
01563             }
01564         }
01565 
01566         if ( preg_match( '/[\000-\010\013\016-\037\177]/', $value ) ) {
01567             return true;
01568         }
01569 
01570         return false;
01571     }
01572 
01578     private static function splitXmlNamespace( $element ) {
01579         // 'http://www.w3.org/2000/svg:script' -> array( 'http://www.w3.org/2000/svg', 'script' )
01580         $parts = explode( ':', strtolower( $element ) );
01581         $name = array_pop( $parts );
01582         $ns = implode( ':', $parts );
01583 
01584         return array( $ns, $name );
01585     }
01586 
01591     private function stripXmlNamespace( $name ) {
01592         // 'http://www.w3.org/2000/svg:script' -> 'script'
01593         $parts = explode( ':', strtolower( $name ) );
01594 
01595         return array_pop( $parts );
01596     }
01597 
01608     public static function detectVirus( $file ) {
01609         global $wgAntivirus, $wgAntivirusSetup, $wgAntivirusRequired, $wgOut;
01610         wfProfileIn( __METHOD__ );
01611 
01612         if ( !$wgAntivirus ) {
01613             wfDebug( __METHOD__ . ": virus scanner disabled\n" );
01614             wfProfileOut( __METHOD__ );
01615 
01616             return null;
01617         }
01618 
01619         if ( !$wgAntivirusSetup[$wgAntivirus] ) {
01620             wfDebug( __METHOD__ . ": unknown virus scanner: $wgAntivirus\n" );
01621             $wgOut->wrapWikiMsg( "<div class=\"error\">\n$1\n</div>",
01622                 array( 'virus-badscanner', $wgAntivirus ) );
01623             wfProfileOut( __METHOD__ );
01624 
01625             return wfMessage( 'virus-unknownscanner' )->text() . " $wgAntivirus";
01626         }
01627 
01628         # look up scanner configuration
01629         $command = $wgAntivirusSetup[$wgAntivirus]['command'];
01630         $exitCodeMap = $wgAntivirusSetup[$wgAntivirus]['codemap'];
01631         $msgPattern = isset( $wgAntivirusSetup[$wgAntivirus]['messagepattern'] ) ?
01632             $wgAntivirusSetup[$wgAntivirus]['messagepattern'] : null;
01633 
01634         if ( strpos( $command, "%f" ) === false ) {
01635             # simple pattern: append file to scan
01636             $command .= " " . wfEscapeShellArg( $file );
01637         } else {
01638             # complex pattern: replace "%f" with file to scan
01639             $command = str_replace( "%f", wfEscapeShellArg( $file ), $command );
01640         }
01641 
01642         wfDebug( __METHOD__ . ": running virus scan: $command \n" );
01643 
01644         # execute virus scanner
01645         $exitCode = false;
01646 
01647         # NOTE: there's a 50 line workaround to make stderr redirection work on windows, too.
01648         #      that does not seem to be worth the pain.
01649         #      Ask me (Duesentrieb) about it if it's ever needed.
01650         $output = wfShellExecWithStderr( $command, $exitCode );
01651 
01652         # map exit code to AV_xxx constants.
01653         $mappedCode = $exitCode;
01654         if ( $exitCodeMap ) {
01655             if ( isset( $exitCodeMap[$exitCode] ) ) {
01656                 $mappedCode = $exitCodeMap[$exitCode];
01657             } elseif ( isset( $exitCodeMap["*"] ) ) {
01658                 $mappedCode = $exitCodeMap["*"];
01659             }
01660         }
01661 
01662         /* NB: AV_NO_VIRUS is 0 but AV_SCAN_FAILED is false,
01663          * so we need the strict equalities === and thus can't use a switch here
01664          */
01665         if ( $mappedCode === AV_SCAN_FAILED ) {
01666             # scan failed (code was mapped to false by $exitCodeMap)
01667             wfDebug( __METHOD__ . ": failed to scan $file (code $exitCode).\n" );
01668 
01669             $output = $wgAntivirusRequired
01670                 ? wfMessage( 'virus-scanfailed', array( $exitCode ) )->text()
01671                 : null;
01672         } elseif ( $mappedCode === AV_SCAN_ABORTED ) {
01673             # scan failed because filetype is unknown (probably imune)
01674             wfDebug( __METHOD__ . ": unsupported file type $file (code $exitCode).\n" );
01675             $output = null;
01676         } elseif ( $mappedCode === AV_NO_VIRUS ) {
01677             # no virus found
01678             wfDebug( __METHOD__ . ": file passed virus scan.\n" );
01679             $output = false;
01680         } else {
01681             $output = trim( $output );
01682 
01683             if ( !$output ) {
01684                 $output = true; #if there's no output, return true
01685             } elseif ( $msgPattern ) {
01686                 $groups = array();
01687                 if ( preg_match( $msgPattern, $output, $groups ) ) {
01688                     if ( $groups[1] ) {
01689                         $output = $groups[1];
01690                     }
01691                 }
01692             }
01693 
01694             wfDebug( __METHOD__ . ": FOUND VIRUS! scanner feedback: $output \n" );
01695         }
01696 
01697         wfProfileOut( __METHOD__ );
01698 
01699         return $output;
01700     }
01701 
01710     private function checkOverwrite( $user ) {
01711         // First check whether the local file can be overwritten
01712         $file = $this->getLocalFile();
01713         if ( $file->exists() ) {
01714             if ( !self::userCanReUpload( $user, $file ) ) {
01715                 return array( 'fileexists-forbidden', $file->getName() );
01716             } else {
01717                 return true;
01718             }
01719         }
01720 
01721         /* Check shared conflicts: if the local file does not exist, but
01722          * wfFindFile finds a file, it exists in a shared repository.
01723          */
01724         $file = wfFindFile( $this->getTitle() );
01725         if ( $file && !$user->isAllowed( 'reupload-shared' ) ) {
01726             return array( 'fileexists-shared-forbidden', $file->getName() );
01727         }
01728 
01729         return true;
01730     }
01731 
01739     public static function userCanReUpload( User $user, $img ) {
01740         if ( $user->isAllowed( 'reupload' ) ) {
01741             return true; // non-conditional
01742         }
01743         if ( !$user->isAllowed( 'reupload-own' ) ) {
01744             return false;
01745         }
01746         if ( is_string( $img ) ) {
01747             $img = wfLocalFile( $img );
01748         }
01749         if ( !( $img instanceof LocalFile ) ) {
01750             return false;
01751         }
01752 
01753         return $user->getId() == $img->getUser( 'id' );
01754     }
01755 
01767     public static function getExistsWarning( $file ) {
01768         if ( $file->exists() ) {
01769             return array( 'warning' => 'exists', 'file' => $file );
01770         }
01771 
01772         if ( $file->getTitle()->getArticleID() ) {
01773             return array( 'warning' => 'page-exists', 'file' => $file );
01774         }
01775 
01776         if ( $file->wasDeleted() && !$file->exists() ) {
01777             return array( 'warning' => 'was-deleted', 'file' => $file );
01778         }
01779 
01780         if ( strpos( $file->getName(), '.' ) == false ) {
01781             $partname = $file->getName();
01782             $extension = '';
01783         } else {
01784             $n = strrpos( $file->getName(), '.' );
01785             $extension = substr( $file->getName(), $n + 1 );
01786             $partname = substr( $file->getName(), 0, $n );
01787         }
01788         $normalizedExtension = File::normalizeExtension( $extension );
01789 
01790         if ( $normalizedExtension != $extension ) {
01791             // We're not using the normalized form of the extension.
01792             // Normal form is lowercase, using most common of alternate
01793             // extensions (eg 'jpg' rather than 'JPEG').
01794             //
01795             // Check for another file using the normalized form...
01796             $nt_lc = Title::makeTitle( NS_FILE, "{$partname}.{$normalizedExtension}" );
01797             $file_lc = wfLocalFile( $nt_lc );
01798 
01799             if ( $file_lc->exists() ) {
01800                 return array(
01801                     'warning' => 'exists-normalized',
01802                     'file' => $file,
01803                     'normalizedFile' => $file_lc
01804                 );
01805             }
01806         }
01807 
01808         // Check for files with the same name but a different extension
01809         $similarFiles = RepoGroup::singleton()->getLocalRepo()->findFilesByPrefix(
01810             "{$partname}.", 1 );
01811         if ( count( $similarFiles ) ) {
01812             return array(
01813                 'warning' => 'exists-normalized',
01814                 'file' => $file,
01815                 'normalizedFile' => $similarFiles[0],
01816             );
01817         }
01818 
01819         if ( self::isThumbName( $file->getName() ) ) {
01820             # Check for filenames like 50px- or 180px-, these are mostly thumbnails
01821             $nt_thb = Title::newFromText(
01822                 substr( $partname, strpos( $partname, '-' ) + 1 ) . '.' . $extension,
01823                 NS_FILE
01824             );
01825             $file_thb = wfLocalFile( $nt_thb );
01826             if ( $file_thb->exists() ) {
01827                 return array(
01828                     'warning' => 'thumb',
01829                     'file' => $file,
01830                     'thumbFile' => $file_thb
01831                 );
01832             } else {
01833                 // File does not exist, but we just don't like the name
01834                 return array(
01835                     'warning' => 'thumb-name',
01836                     'file' => $file,
01837                     'thumbFile' => $file_thb
01838                 );
01839             }
01840         }
01841 
01842         foreach ( self::getFilenamePrefixBlacklist() as $prefix ) {
01843             if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) {
01844                 return array(
01845                     'warning' => 'bad-prefix',
01846                     'file' => $file,
01847                     'prefix' => $prefix
01848                 );
01849             }
01850         }
01851 
01852         return false;
01853     }
01854 
01860     public static function isThumbName( $filename ) {
01861         $n = strrpos( $filename, '.' );
01862         $partname = $n ? substr( $filename, 0, $n ) : $filename;
01863 
01864         return (
01865             substr( $partname, 3, 3 ) == 'px-' ||
01866             substr( $partname, 2, 3 ) == 'px-'
01867         ) &&
01868         preg_match( "/[0-9]{2}/", substr( $partname, 0, 2 ) );
01869     }
01870 
01876     public static function getFilenamePrefixBlacklist() {
01877         $blacklist = array();
01878         $message = wfMessage( 'filename-prefix-blacklist' )->inContentLanguage();
01879         if ( !$message->isDisabled() ) {
01880             $lines = explode( "\n", $message->plain() );
01881             foreach ( $lines as $line ) {
01882                 // Remove comment lines
01883                 $comment = substr( trim( $line ), 0, 1 );
01884                 if ( $comment == '#' || $comment == '' ) {
01885                     continue;
01886                 }
01887                 // Remove additional comments after a prefix
01888                 $comment = strpos( $line, '#' );
01889                 if ( $comment > 0 ) {
01890                     $line = substr( $line, 0, $comment - 1 );
01891                 }
01892                 $blacklist[] = trim( $line );
01893             }
01894         }
01895 
01896         return $blacklist;
01897     }
01898 
01910     public function getImageInfo( $result ) {
01911         $file = $this->getLocalFile();
01917         if ( $file instanceof UploadStashFile ) {
01918             $imParam = ApiQueryStashImageInfo::getPropertyNames();
01919             $info = ApiQueryStashImageInfo::getInfo( $file, array_flip( $imParam ), $result );
01920         } else {
01921             $imParam = ApiQueryImageInfo::getPropertyNames();
01922             $info = ApiQueryImageInfo::getInfo( $file, array_flip( $imParam ), $result );
01923         }
01924 
01925         return $info;
01926     }
01927 
01932     public function convertVerifyErrorToStatus( $error ) {
01933         $code = $error['status'];
01934         unset( $code['status'] );
01935 
01936         return Status::newFatal( $this->getVerificationErrorCode( $code ), $error );
01937     }
01938 
01943     public static function getMaxUploadSize( $forType = null ) {
01944         global $wgMaxUploadSize;
01945 
01946         if ( is_array( $wgMaxUploadSize ) ) {
01947             if ( !is_null( $forType ) && isset( $wgMaxUploadSize[$forType] ) ) {
01948                 return $wgMaxUploadSize[$forType];
01949             } else {
01950                 return $wgMaxUploadSize['*'];
01951             }
01952         } else {
01953             return intval( $wgMaxUploadSize );
01954         }
01955     }
01956 
01963     public static function getSessionStatus( $statusKey ) {
01964         return isset( $_SESSION[self::SESSION_STATUS_KEY][$statusKey] )
01965             ? $_SESSION[self::SESSION_STATUS_KEY][$statusKey]
01966             : false;
01967     }
01968 
01976     public static function setSessionStatus( $statusKey, $value ) {
01977         if ( $value === false ) {
01978             unset( $_SESSION[self::SESSION_STATUS_KEY][$statusKey] );
01979         } else {
01980             $_SESSION[self::SESSION_STATUS_KEY][$statusKey] = $value;
01981         }
01982     }
01983 }