MediaWiki
REL1_19
|
00001 <?php 00018 abstract class UploadBase { 00019 protected $mTempPath; 00020 protected $mDesiredDestName, $mDestName, $mRemoveTempFile, $mSourceType; 00021 protected $mTitle = false, $mTitleError = 0; 00022 protected $mFilteredName, $mFinalExtension; 00023 protected $mLocalFile, $mFileSize, $mFileProps; 00024 protected $mBlackListedExtensions; 00025 protected $mJavaDetected, $mSVGNSError; 00026 00027 protected static $safeXmlEncodings = array( 'UTF-8', 'ISO-8859-1', 'ISO-8859-2', 'UTF-16', 'UTF-32' ); 00028 00029 const SUCCESS = 0; 00030 const OK = 0; 00031 const EMPTY_FILE = 3; 00032 const MIN_LENGTH_PARTNAME = 4; 00033 const ILLEGAL_FILENAME = 5; 00034 const OVERWRITE_EXISTING_FILE = 7; # Not used anymore; handled by verifyTitlePermissions() 00035 const FILETYPE_MISSING = 8; 00036 const FILETYPE_BADTYPE = 9; 00037 const VERIFICATION_ERROR = 10; 00038 00039 # HOOK_ABORTED is the new name of UPLOAD_VERIFICATION_ERROR 00040 const UPLOAD_VERIFICATION_ERROR = 11; 00041 const HOOK_ABORTED = 11; 00042 const FILE_TOO_LARGE = 12; 00043 const WINDOWS_NONASCII_FILENAME = 13; 00044 const FILENAME_TOO_LONG = 14; 00045 00046 public function getVerificationErrorCode( $error ) { 00047 $code_to_status = array(self::EMPTY_FILE => 'empty-file', 00048 self::FILE_TOO_LARGE => 'file-too-large', 00049 self::FILETYPE_MISSING => 'filetype-missing', 00050 self::FILETYPE_BADTYPE => 'filetype-banned', 00051 self::MIN_LENGTH_PARTNAME => 'filename-tooshort', 00052 self::ILLEGAL_FILENAME => 'illegal-filename', 00053 self::OVERWRITE_EXISTING_FILE => 'overwrite', 00054 self::VERIFICATION_ERROR => 'verification-error', 00055 self::HOOK_ABORTED => 'hookaborted', 00056 self::WINDOWS_NONASCII_FILENAME => 'windows-nonascii-filename', 00057 self::FILENAME_TOO_LONG => 'filename-toolong', 00058 ); 00059 if( isset( $code_to_status[$error] ) ) { 00060 return $code_to_status[$error]; 00061 } 00062 00063 return 'unknown-error'; 00064 } 00065 00070 public static function isEnabled() { 00071 global $wgEnableUploads; 00072 00073 if ( !$wgEnableUploads ) { 00074 return false; 00075 } 00076 00077 # Check php's file_uploads setting 00078 return wfIsHipHop() || wfIniGetBool( 'file_uploads' ); 00079 } 00080 00088 public static function isAllowed( $user ) { 00089 foreach ( array( 'upload', 'edit' ) as $permission ) { 00090 if ( !$user->isAllowed( $permission ) ) { 00091 return $permission; 00092 } 00093 } 00094 return true; 00095 } 00096 00097 // Upload handlers. Should probably just be a global. 00098 static $uploadHandlers = array( 'Stash', 'File', 'Url' ); 00099 00106 public static function createFromRequest( &$request, $type = null ) { 00107 $type = $type ? $type : $request->getVal( 'wpSourceType', 'File' ); 00108 00109 if( !$type ) { 00110 return null; 00111 } 00112 00113 // Get the upload class 00114 $type = ucfirst( $type ); 00115 00116 // Give hooks the chance to handle this request 00117 $className = null; 00118 wfRunHooks( 'UploadCreateFromRequest', array( $type, &$className ) ); 00119 if ( is_null( $className ) ) { 00120 $className = 'UploadFrom' . $type; 00121 wfDebug( __METHOD__ . ": class name: $className\n" ); 00122 if( !in_array( $type, self::$uploadHandlers ) ) { 00123 return null; 00124 } 00125 } 00126 00127 // Check whether this upload class is enabled 00128 if( !call_user_func( array( $className, 'isEnabled' ) ) ) { 00129 return null; 00130 } 00131 00132 // Check whether the request is valid 00133 if( !call_user_func( array( $className, 'isValidRequest' ), $request ) ) { 00134 return null; 00135 } 00136 00137 $handler = new $className; 00138 00139 $handler->initializeFromRequest( $request ); 00140 return $handler; 00141 } 00142 00146 public static function isValidRequest( $request ) { 00147 return false; 00148 } 00149 00150 public function __construct() {} 00151 00158 public function getSourceType() { return null; } 00159 00168 public function initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile = false ) { 00169 $this->mDesiredDestName = $name; 00170 if ( FileBackend::isStoragePath( $tempPath ) ) { 00171 throw new MWException( __METHOD__ . " given storage path `$tempPath`." ); 00172 } 00173 $this->mTempPath = $tempPath; 00174 $this->mFileSize = $fileSize; 00175 $this->mRemoveTempFile = $removeTempFile; 00176 } 00177 00181 public abstract function initializeFromRequest( &$request ); 00182 00186 public function fetchFile() { 00187 return Status::newGood(); 00188 } 00189 00194 public function isEmptyFile() { 00195 return empty( $this->mFileSize ); 00196 } 00197 00202 public function getFileSize() { 00203 return $this->mFileSize; 00204 } 00205 00210 function getRealPath( $srcPath ) { 00211 $repo = RepoGroup::singleton()->getLocalRepo(); 00212 if ( $repo->isVirtualUrl( $srcPath ) ) { 00213 // @TODO: just make uploads work with storage paths 00214 // UploadFromStash loads files via virtuals URLs 00215 $tmpFile = $repo->getLocalCopy( $srcPath ); 00216 $tmpFile->bind( $this ); // keep alive with $thumb 00217 return $tmpFile->getPath(); 00218 } 00219 return $srcPath; 00220 } 00221 00226 public function verifyUpload() { 00230 if( $this->isEmptyFile() ) { 00231 return array( 'status' => self::EMPTY_FILE ); 00232 } 00233 00237 $maxSize = self::getMaxUploadSize( $this->getSourceType() ); 00238 if( $this->mFileSize > $maxSize ) { 00239 return array( 00240 'status' => self::FILE_TOO_LARGE, 00241 'max' => $maxSize, 00242 ); 00243 } 00244 00250 $verification = $this->verifyFile(); 00251 if( $verification !== true ) { 00252 return array( 00253 'status' => self::VERIFICATION_ERROR, 00254 'details' => $verification 00255 ); 00256 } 00257 00261 $result = $this->validateName(); 00262 if( $result !== true ) { 00263 return $result; 00264 } 00265 00266 $error = ''; 00267 if( !wfRunHooks( 'UploadVerification', 00268 array( $this->mDestName, $this->mTempPath, &$error ) ) ) { 00269 return array( 'status' => self::HOOK_ABORTED, 'error' => $error ); 00270 } 00271 00272 return array( 'status' => self::OK ); 00273 } 00274 00281 protected function validateName() { 00282 $nt = $this->getTitle(); 00283 if( is_null( $nt ) ) { 00284 $result = array( 'status' => $this->mTitleError ); 00285 if( $this->mTitleError == self::ILLEGAL_FILENAME ) { 00286 $result['filtered'] = $this->mFilteredName; 00287 } 00288 if ( $this->mTitleError == self::FILETYPE_BADTYPE ) { 00289 $result['finalExt'] = $this->mFinalExtension; 00290 if ( count( $this->mBlackListedExtensions ) ) { 00291 $result['blacklistedExt'] = $this->mBlackListedExtensions; 00292 } 00293 } 00294 return $result; 00295 } 00296 $this->mDestName = $this->getLocalFile()->getName(); 00297 00298 return true; 00299 } 00300 00309 protected function verifyMimeType( $mime ) { 00310 global $wgVerifyMimeType; 00311 if ( $wgVerifyMimeType ) { 00312 wfDebug ( "\n\nmime: <$mime> extension: <{$this->mFinalExtension}>\n\n"); 00313 global $wgMimeTypeBlacklist; 00314 if ( $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) { 00315 return array( 'filetype-badmime', $mime ); 00316 } 00317 00318 # Check IE type 00319 $fp = fopen( $this->mTempPath, 'rb' ); 00320 $chunk = fread( $fp, 256 ); 00321 fclose( $fp ); 00322 00323 $magic = MimeMagic::singleton(); 00324 $extMime = $magic->guessTypesForExtension( $this->mFinalExtension ); 00325 $ieTypes = $magic->getIEMimeTypes( $this->mTempPath, $chunk, $extMime ); 00326 foreach ( $ieTypes as $ieType ) { 00327 if ( $this->checkFileExtension( $ieType, $wgMimeTypeBlacklist ) ) { 00328 return array( 'filetype-bad-ie-mime', $ieType ); 00329 } 00330 } 00331 } 00332 00333 return true; 00334 } 00335 00341 protected function verifyFile() { 00342 global $wgVerifyMimeType; 00343 wfProfileIn( __METHOD__ ); 00344 00345 $status = $this->verifyPartialFile(); 00346 if ( $status !== true ) { 00347 wfProfileOut( __METHOD__ ); 00348 return $status; 00349 } 00350 00351 $this->mFileProps = FSFile::getPropsFromPath( $this->mTempPath, $this->mFinalExtension ); 00352 $mime = $this->mFileProps['file-mime']; 00353 00354 if ( $wgVerifyMimeType ) { 00355 # XXX: Missing extension will be caught by validateName() via getTitle() 00356 if ( $this->mFinalExtension != '' && !$this->verifyExtension( $mime, $this->mFinalExtension ) ) { 00357 wfProfileOut( __METHOD__ ); 00358 return array( 'filetype-mime-mismatch', $this->mFinalExtension, $mime ); 00359 } 00360 } 00361 00362 $handler = MediaHandler::getHandler( $mime ); 00363 if ( $handler ) { 00364 $handlerStatus = $handler->verifyUpload( $this->mTempPath ); 00365 if ( !$handlerStatus->isOK() ) { 00366 $errors = $handlerStatus->getErrorsArray(); 00367 wfProfileOut( __METHOD__ ); 00368 return reset( $errors ); 00369 } 00370 } 00371 00372 wfRunHooks( 'UploadVerifyFile', array( $this, $mime, &$status ) ); 00373 if ( $status !== true ) { 00374 wfProfileOut( __METHOD__ ); 00375 return $status; 00376 } 00377 00378 wfDebug( __METHOD__ . ": all clear; passing.\n" ); 00379 wfProfileOut( __METHOD__ ); 00380 return true; 00381 } 00382 00391 protected function verifyPartialFile() { 00392 global $wgAllowJavaUploads, $wgDisableUploadScriptChecks; 00393 # get the title, even though we are doing nothing with it, because 00394 # we need to populate mFinalExtension 00395 $this->getTitle(); 00396 00397 $this->mFileProps = FSFile::getPropsFromPath( $this->mTempPath, $this->mFinalExtension ); 00398 00399 # check mime type, if desired 00400 $mime = $this->mFileProps[ 'file-mime' ]; 00401 $status = $this->verifyMimeType( $mime ); 00402 if ( $status !== true ) { 00403 return $status; 00404 } 00405 00406 # check for htmlish code and javascript 00407 if ( !$wgDisableUploadScriptChecks ) { 00408 if( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) { 00409 return array( 'uploadscripted' ); 00410 } 00411 if( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) { 00412 $svgStatus = $this->detectScriptInSvg( $this->mTempPath ); 00413 if ( $svgStatus !== false ) { 00414 return $svgStatus; 00415 } 00416 } 00417 } 00418 00419 # Check for Java applets, which if uploaded can bypass cross-site 00420 # restrictions. 00421 if ( !$wgAllowJavaUploads ) { 00422 $this->mJavaDetected = false; 00423 $zipStatus = ZipDirectoryReader::read( $this->mTempPath, 00424 array( $this, 'zipEntryCallback' ) ); 00425 if ( !$zipStatus->isOK() ) { 00426 $errors = $zipStatus->getErrorsArray(); 00427 $error = reset( $errors ); 00428 if ( $error[0] !== 'zip-wrong-format' ) { 00429 return $error; 00430 } 00431 } 00432 if ( $this->mJavaDetected ) { 00433 return array( 'uploadjava' ); 00434 } 00435 } 00436 00437 # Scan the uploaded file for viruses 00438 $virus = $this->detectVirus( $this->mTempPath ); 00439 if ( $virus ) { 00440 return array( 'uploadvirus', $virus ); 00441 } 00442 00443 return true; 00444 } 00445 00449 function zipEntryCallback( $entry ) { 00450 $names = array( $entry['name'] ); 00451 00452 // If there is a null character, cut off the name at it, because JDK's 00453 // ZIP_GetEntry() uses strcmp() if the name hashes match. If a file name 00454 // were constructed which had ".class\0" followed by a string chosen to 00455 // make the hash collide with the truncated name, that file could be 00456 // returned in response to a request for the .class file. 00457 $nullPos = strpos( $entry['name'], "\000" ); 00458 if ( $nullPos !== false ) { 00459 $names[] = substr( $entry['name'], 0, $nullPos ); 00460 } 00461 00462 // If there is a trailing slash in the file name, we have to strip it, 00463 // because that's what ZIP_GetEntry() does. 00464 if ( preg_grep( '!\.class/?$!', $names ) ) { 00465 $this->mJavaDetected = true; 00466 } 00467 } 00468 00476 public function verifyPermissions( $user ) { 00477 return $this->verifyTitlePermissions( $user ); 00478 } 00479 00491 public function verifyTitlePermissions( $user ) { 00496 $nt = $this->getTitle(); 00497 if( is_null( $nt ) ) { 00498 return true; 00499 } 00500 $permErrors = $nt->getUserPermissionsErrors( 'edit', $user ); 00501 $permErrorsUpload = $nt->getUserPermissionsErrors( 'upload', $user ); 00502 if ( !$nt->exists() ) { 00503 $permErrorsCreate = $nt->getUserPermissionsErrors( 'create', $user ); 00504 } else { 00505 $permErrorsCreate = array(); 00506 } 00507 if( $permErrors || $permErrorsUpload || $permErrorsCreate ) { 00508 $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsUpload, $permErrors ) ); 00509 $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsCreate, $permErrors ) ); 00510 return $permErrors; 00511 } 00512 00513 $overwriteError = $this->checkOverwrite( $user ); 00514 if ( $overwriteError !== true ) { 00515 return array( $overwriteError ); 00516 } 00517 00518 return true; 00519 } 00520 00526 public function checkWarnings() { 00527 global $wgLang; 00528 00529 $warnings = array(); 00530 00531 $localFile = $this->getLocalFile(); 00532 $filename = $localFile->getName(); 00533 00538 $comparableName = str_replace( ' ', '_', $this->mDesiredDestName ); 00539 $comparableName = Title::capitalize( $comparableName, NS_FILE ); 00540 00541 if( $this->mDesiredDestName != $filename && $comparableName != $filename ) { 00542 $warnings['badfilename'] = $filename; 00543 } 00544 00545 // Check whether the file extension is on the unwanted list 00546 global $wgCheckFileExtensions, $wgFileExtensions; 00547 if ( $wgCheckFileExtensions ) { 00548 if ( !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) ) { 00549 $warnings['filetype-unwanted-type'] = array( $this->mFinalExtension, 00550 $wgLang->commaList( $wgFileExtensions ), count( $wgFileExtensions ) ); 00551 } 00552 } 00553 00554 global $wgUploadSizeWarning; 00555 if ( $wgUploadSizeWarning && ( $this->mFileSize > $wgUploadSizeWarning ) ) { 00556 $warnings['large-file'] = $wgUploadSizeWarning; 00557 } 00558 00559 if ( $this->mFileSize == 0 ) { 00560 $warnings['emptyfile'] = true; 00561 } 00562 00563 $exists = self::getExistsWarning( $localFile ); 00564 if( $exists !== false ) { 00565 $warnings['exists'] = $exists; 00566 } 00567 00568 // Check dupes against existing files 00569 $hash = FSFile::getSha1Base36FromPath( $this->mTempPath ); 00570 $dupes = RepoGroup::singleton()->findBySha1( $hash ); 00571 $title = $this->getTitle(); 00572 // Remove all matches against self 00573 foreach ( $dupes as $key => $dupe ) { 00574 if( $title->equals( $dupe->getTitle() ) ) { 00575 unset( $dupes[$key] ); 00576 } 00577 } 00578 if( $dupes ) { 00579 $warnings['duplicate'] = $dupes; 00580 } 00581 00582 // Check dupes against archives 00583 $archivedImage = new ArchivedFile( null, 0, "{$hash}.{$this->mFinalExtension}" ); 00584 if ( $archivedImage->getID() > 0 ) { 00585 $warnings['duplicate-archive'] = $archivedImage->getName(); 00586 } 00587 00588 return $warnings; 00589 } 00590 00599 public function performUpload( $comment, $pageText, $watch, $user ) { 00600 $status = $this->getLocalFile()->upload( 00601 $this->mTempPath, 00602 $comment, 00603 $pageText, 00604 File::DELETE_SOURCE, 00605 $this->mFileProps, 00606 false, 00607 $user 00608 ); 00609 00610 if( $status->isGood() ) { 00611 if ( $watch ) { 00612 $user->addWatch( $this->getLocalFile()->getTitle() ); 00613 } 00614 00615 wfRunHooks( 'UploadComplete', array( &$this ) ); 00616 } 00617 00618 return $status; 00619 } 00620 00627 public function getTitle() { 00628 if ( $this->mTitle !== false ) { 00629 return $this->mTitle; 00630 } 00631 00632 /* Assume that if a user specified File:Something.jpg, this is an error 00633 * and that the namespace prefix needs to be stripped of. 00634 */ 00635 $title = Title::newFromText( $this->mDesiredDestName ); 00636 if ( $title && $title->getNamespace() == NS_FILE ) { 00637 $this->mFilteredName = $title->getDBkey(); 00638 } else { 00639 $this->mFilteredName = $this->mDesiredDestName; 00640 } 00641 00642 # oi_archive_name is max 255 bytes, which include a timestamp and an 00643 # exclamation mark, so restrict file name to 240 bytes. 00644 if ( strlen( $this->mFilteredName ) > 240 ) { 00645 $this->mTitleError = self::FILENAME_TOO_LONG; 00646 return $this->mTitle = null; 00647 } 00648 00654 $this->mFilteredName = wfStripIllegalFilenameChars( $this->mFilteredName ); 00655 /* Normalize to title form before we do any further processing */ 00656 $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName ); 00657 if( is_null( $nt ) ) { 00658 $this->mTitleError = self::ILLEGAL_FILENAME; 00659 return $this->mTitle = null; 00660 } 00661 $this->mFilteredName = $nt->getDBkey(); 00662 00663 00664 00669 list( $partname, $ext ) = $this->splitExtensions( $this->mFilteredName ); 00670 00671 if( count( $ext ) ) { 00672 $this->mFinalExtension = trim( $ext[count( $ext ) - 1] ); 00673 } else { 00674 $this->mFinalExtension = ''; 00675 00676 # No extension, try guessing one 00677 $magic = MimeMagic::singleton(); 00678 $mime = $magic->guessMimeType( $this->mTempPath ); 00679 if ( $mime !== 'unknown/unknown' ) { 00680 # Get a space separated list of extensions 00681 $extList = $magic->getExtensionsForType( $mime ); 00682 if ( $extList ) { 00683 # Set the extension to the canonical extension 00684 $this->mFinalExtension = strtok( $extList, ' ' ); 00685 00686 # Fix up the other variables 00687 $this->mFilteredName .= ".{$this->mFinalExtension}"; 00688 $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName ); 00689 $ext = array( $this->mFinalExtension ); 00690 } 00691 } 00692 00693 } 00694 00695 /* Don't allow users to override the blacklist (check file extension) */ 00696 global $wgCheckFileExtensions, $wgStrictFileExtensions; 00697 global $wgFileExtensions, $wgFileBlacklist; 00698 00699 $blackListedExtensions = $this->checkFileExtensionList( $ext, $wgFileBlacklist ); 00700 00701 if ( $this->mFinalExtension == '' ) { 00702 $this->mTitleError = self::FILETYPE_MISSING; 00703 return $this->mTitle = null; 00704 } elseif ( $blackListedExtensions || 00705 ( $wgCheckFileExtensions && $wgStrictFileExtensions && 00706 !$this->checkFileExtensionList( $ext, $wgFileExtensions ) ) ) { 00707 $this->mBlackListedExtensions = $blackListedExtensions; 00708 $this->mTitleError = self::FILETYPE_BADTYPE; 00709 return $this->mTitle = null; 00710 } 00711 00712 // Windows may be broken with special characters, see bug XXX 00713 if ( wfIsWindows() && !preg_match( '/^[\x0-\x7f]*$/', $nt->getText() ) ) { 00714 $this->mTitleError = self::WINDOWS_NONASCII_FILENAME; 00715 return $this->mTitle = null; 00716 } 00717 00718 # If there was more than one "extension", reassemble the base 00719 # filename to prevent bogus complaints about length 00720 if( count( $ext ) > 1 ) { 00721 for( $i = 0; $i < count( $ext ) - 1; $i++ ) { 00722 $partname .= '.' . $ext[$i]; 00723 } 00724 } 00725 00726 if( strlen( $partname ) < 1 ) { 00727 $this->mTitleError = self::MIN_LENGTH_PARTNAME; 00728 return $this->mTitle = null; 00729 } 00730 00731 return $this->mTitle = $nt; 00732 } 00733 00739 public function getLocalFile() { 00740 if( is_null( $this->mLocalFile ) ) { 00741 $nt = $this->getTitle(); 00742 $this->mLocalFile = is_null( $nt ) ? null : wfLocalFile( $nt ); 00743 } 00744 return $this->mLocalFile; 00745 } 00746 00761 protected function saveTempUploadedFile( $saveName, $tempSrc ) { 00762 $repo = RepoGroup::singleton()->getLocalRepo(); 00763 $status = $repo->storeTemp( $saveName, $tempSrc ); 00764 return $status; 00765 } 00766 00778 public function stashFile() { 00779 // was stashSessionFile 00780 $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash(); 00781 $file = $stash->stashFile( $this->mTempPath, $this->getSourceType() ); 00782 $this->mLocalFile = $file; 00783 return $file; 00784 } 00785 00791 public function stashFileGetKey() { 00792 return $this->stashFile()->getFileKey(); 00793 } 00794 00800 public function stashSession() { 00801 return $this->stashFileGetKey(); 00802 } 00803 00808 public function cleanupTempFile() { 00809 if ( $this->mRemoveTempFile && $this->mTempPath && file_exists( $this->mTempPath ) ) { 00810 wfDebug( __METHOD__ . ": Removing temporary file {$this->mTempPath}\n" ); 00811 unlink( $this->mTempPath ); 00812 } 00813 } 00814 00815 public function getTempPath() { 00816 return $this->mTempPath; 00817 } 00818 00827 public static function splitExtensions( $filename ) { 00828 $bits = explode( '.', $filename ); 00829 $basename = array_shift( $bits ); 00830 return array( $basename, $bits ); 00831 } 00832 00841 public static function checkFileExtension( $ext, $list ) { 00842 return in_array( strtolower( $ext ), $list ); 00843 } 00844 00853 public static function checkFileExtensionList( $ext, $list ) { 00854 return array_intersect( array_map( 'strtolower', $ext ), $list ); 00855 } 00856 00864 public static function verifyExtension( $mime, $extension ) { 00865 $magic = MimeMagic::singleton(); 00866 00867 if ( !$mime || $mime == 'unknown' || $mime == 'unknown/unknown' ) 00868 if ( !$magic->isRecognizableExtension( $extension ) ) { 00869 wfDebug( __METHOD__ . ": passing file with unknown detected mime type; " . 00870 "unrecognized extension '$extension', can't verify\n" ); 00871 return true; 00872 } else { 00873 wfDebug( __METHOD__ . ": rejecting file with unknown detected mime type; ". 00874 "recognized extension '$extension', so probably invalid file\n" ); 00875 return false; 00876 } 00877 00878 $match = $magic->isMatchingExtension( $extension, $mime ); 00879 00880 if ( $match === null ) { 00881 wfDebug( __METHOD__ . ": no file extension known for mime type $mime, passing file\n" ); 00882 return true; 00883 } elseif( $match === true ) { 00884 wfDebug( __METHOD__ . ": mime type $mime matches extension $extension, passing file\n" ); 00885 00886 #TODO: if it's a bitmap, make sure PHP or ImageMagic resp. can handle it! 00887 return true; 00888 00889 } else { 00890 wfDebug( __METHOD__ . ": mime type $mime mismatches file extension $extension, rejecting file\n" ); 00891 return false; 00892 } 00893 } 00894 00906 public static function detectScript( $file, $mime, $extension ) { 00907 global $wgAllowTitlesInSVG; 00908 00909 # ugly hack: for text files, always look at the entire file. 00910 # For binary field, just check the first K. 00911 00912 if( strpos( $mime,'text/' ) === 0 ) { 00913 $chunk = file_get_contents( $file ); 00914 } else { 00915 $fp = fopen( $file, 'rb' ); 00916 $chunk = fread( $fp, 1024 ); 00917 fclose( $fp ); 00918 } 00919 00920 $chunk = strtolower( $chunk ); 00921 00922 if( !$chunk ) { 00923 return false; 00924 } 00925 00926 # decode from UTF-16 if needed (could be used for obfuscation). 00927 if( substr( $chunk, 0, 2 ) == "\xfe\xff" ) { 00928 $enc = 'UTF-16BE'; 00929 } elseif( substr( $chunk, 0, 2 ) == "\xff\xfe" ) { 00930 $enc = 'UTF-16LE'; 00931 } else { 00932 $enc = null; 00933 } 00934 00935 if( $enc ) { 00936 $chunk = iconv( $enc, "ASCII//IGNORE", $chunk ); 00937 } 00938 00939 $chunk = trim( $chunk ); 00940 00941 # @todo FIXME: Convert from UTF-16 if necessarry! 00942 wfDebug( __METHOD__ . ": checking for embedded scripts and HTML stuff\n" ); 00943 00944 # check for HTML doctype 00945 if ( preg_match( "/<!DOCTYPE *X?HTML/i", $chunk ) ) { 00946 return true; 00947 } 00948 00949 // Some browsers will interpret obscure xml encodings as UTF-8, while 00950 // PHP/expat will interpret the given encoding in the xml declaration (bug 47304) 00951 if ( $extension == 'svg' || strpos( $mime, 'image/svg' ) === 0 ) { 00952 if ( self::checkXMLEncodingMissmatch( $file ) ) { 00953 wfProfileOut( __METHOD__ ); 00954 return true; 00955 } 00956 } 00957 00973 $tags = array( 00974 '<a href', 00975 '<body', 00976 '<head', 00977 '<html', #also in safari 00978 '<img', 00979 '<pre', 00980 '<script', #also in safari 00981 '<table' 00982 ); 00983 00984 if( !$wgAllowTitlesInSVG && $extension !== 'svg' && $mime !== 'image/svg' ) { 00985 $tags[] = '<title'; 00986 } 00987 00988 foreach( $tags as $tag ) { 00989 if( false !== strpos( $chunk, $tag ) ) { 00990 wfDebug( __METHOD__ . ": found something that may make it be mistaken for html: $tag\n" ); 00991 return true; 00992 } 00993 } 00994 00995 /* 00996 * look for JavaScript 00997 */ 00998 00999 # resolve entity-refs to look at attributes. may be harsh on big files... cache result? 01000 $chunk = Sanitizer::decodeCharReferences( $chunk ); 01001 01002 # look for script-types 01003 if( preg_match( '!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim', $chunk ) ) { 01004 wfDebug( __METHOD__ . ": found script types\n" ); 01005 return true; 01006 } 01007 01008 # look for html-style script-urls 01009 if( preg_match( '!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) { 01010 wfDebug( __METHOD__ . ": found html-style script urls\n" ); 01011 return true; 01012 } 01013 01014 # look for css-style script-urls 01015 if( preg_match( '!url\s*\(\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) { 01016 wfDebug( __METHOD__ . ": found css-style script urls\n" ); 01017 return true; 01018 } 01019 01020 wfDebug( __METHOD__ . ": no scripts found\n" ); 01021 return false; 01022 } 01023 01028 protected function detectScriptInSvg( $filename ) { 01029 $this->mSVGNSError = false; 01030 $check = new XmlTypeCheck( 01031 $filename, 01032 array( $this, 'checkSvgScriptCallback' ), 01033 array( 'processing_instruction_handler' => 'UploadBase::checkSvgPICallback' ) 01034 ); 01035 if ( $check->wellFormed !== true ) { 01036 // Invalid xml (bug 58553) 01037 return array( 'uploadinvalidxml' ); 01038 } elseif ( $check->filterMatch ) { 01039 if ( $this->mSVGNSError ) { 01040 return array( 'uploadscriptednamespace', $this->mSVGNSError ); 01041 } 01042 return array( 'uploadscripted' ); 01043 } 01044 return false; 01045 } 01046 01047 01055 public static function checkXMLEncodingMissmatch( $file ) { 01056 global $wgSVGMetadataCutoff; 01057 $contents = file_get_contents( $file, false, null, -1, $wgSVGMetadataCutoff ); 01058 $encodingRegex = '!encoding[ \t\n\r]*=[ \t\n\r]*[\'"](.*?)[\'"]!si'; 01059 01060 if ( preg_match( "!<\?xml\b(.*?)\?>!si", $contents, $matches ) ) { 01061 if ( preg_match( $encodingRegex, $matches[1], $encMatch ) 01062 && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings ) 01063 ) { 01064 wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'\n" ); 01065 return true; 01066 } 01067 } elseif ( preg_match( "!<\?xml\b!si", $contents ) ) { 01068 // Start of XML declaration without an end in the first $wgSVGMetadataCutoff 01069 // bytes. There shouldn't be a legitimate reason for this to happen. 01070 wfDebug( __METHOD__ . ": Unmatched XML declaration start\n" ); 01071 return true; 01072 } elseif ( substr( $contents, 0, 4) == "\x4C\x6F\xA7\x94" ) { 01073 // EBCDIC encoded XML 01074 wfDebug( __METHOD__ . ": EBCDIC Encoded XML\n" ); 01075 return true; 01076 } 01077 01078 // It's possible the file is encoded with multi-byte encoding, so re-encode attempt to 01079 // detect the encoding in case is specifies an encoding not whitelisted in self::$safeXmlEncodings 01080 $attemptEncodings = array( 'UTF-16', 'UTF-16BE', 'UTF-32', 'UTF-32BE' ); 01081 foreach ( $attemptEncodings as $encoding ) { 01082 wfSuppressWarnings(); 01083 $str = iconv( $encoding, 'UTF-8', $contents ); 01084 wfRestoreWarnings(); 01085 if ( $str != '' && preg_match( "!<\?xml\b(.*?)\?>!si", $str, $matches ) ) { 01086 if ( preg_match( $encodingRegex, $matches[1], $encMatch ) 01087 && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings ) 01088 ) { 01089 wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'\n" ); 01090 return true; 01091 } 01092 } elseif ( $str != '' && preg_match( "!<\?xml\b!si", $str ) ) { 01093 // Start of XML declaration without an end in the first $wgSVGMetadataCutoff 01094 // bytes. There shouldn't be a legitimate reason for this to happen. 01095 wfDebug( __METHOD__ . ": Unmatched XML declaration start\n" ); 01096 return true; 01097 } 01098 } 01099 01100 return false; 01101 } 01102 01106 public function checkSvgScriptCallback( $element, $attribs, $data = null ) { 01107 01108 list( $namespace, $strippedElement ) = $this->splitXmlNamespace( $element ); 01109 01110 static $validNamespaces = array( 01111 '', 01112 'adobe:ns:meta/', 01113 'http://creativecommons.org/ns#', 01114 'http://inkscape.sourceforge.net/dtd/sodipodi-0.dtd', 01115 'http://ns.adobe.com/adobeillustrator/10.0/', 01116 'http://ns.adobe.com/adobesvgviewerextensions/3.0/', 01117 'http://ns.adobe.com/extensibility/1.0/', 01118 'http://ns.adobe.com/flows/1.0/', 01119 'http://ns.adobe.com/illustrator/1.0/', 01120 'http://ns.adobe.com/imagereplacement/1.0/', 01121 'http://ns.adobe.com/pdf/1.3/', 01122 'http://ns.adobe.com/photoshop/1.0/', 01123 'http://ns.adobe.com/saveforweb/1.0/', 01124 'http://ns.adobe.com/variables/1.0/', 01125 'http://ns.adobe.com/xap/1.0/', 01126 'http://ns.adobe.com/xap/1.0/g/', 01127 'http://ns.adobe.com/xap/1.0/g/img/', 01128 'http://ns.adobe.com/xap/1.0/mm/', 01129 'http://ns.adobe.com/xap/1.0/rights/', 01130 'http://ns.adobe.com/xap/1.0/stype/dimensions#', 01131 'http://ns.adobe.com/xap/1.0/stype/font#', 01132 'http://ns.adobe.com/xap/1.0/stype/manifestitem#', 01133 'http://ns.adobe.com/xap/1.0/stype/resourceevent#', 01134 'http://ns.adobe.com/xap/1.0/stype/resourceref#', 01135 'http://ns.adobe.com/xap/1.0/t/pg/', 01136 'http://purl.org/dc/elements/1.1/', 01137 'http://purl.org/dc/elements/1.1', 01138 'http://schemas.microsoft.com/visio/2003/svgextensions/', 01139 'http://sodipodi.sourceforge.net/dtd/sodipodi-0.dtd', 01140 'http://web.resource.org/cc/', 01141 'http://www.freesoftware.fsf.org/bkchem/cdml', 01142 'http://www.inkscape.org/namespaces/inkscape', 01143 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', 01144 'http://www.w3.org/2000/svg', 01145 ); 01146 01147 if ( !in_array( $namespace, $validNamespaces ) ) { 01148 wfDebug( __METHOD__ . ": Non-svg namespace '$namespace' in uploaded file.\n" ); 01149 // @TODO return a status object to a closure in XmlTypeCheck, for MW1.21+ 01150 $this->mSVGNSError = $namespace; 01151 return true; 01152 } 01153 01154 /* 01155 * check for elements that can contain javascript 01156 */ 01157 if( $strippedElement == 'script' ) { 01158 wfDebug( __METHOD__ . ": Found script element '$element' in uploaded file.\n" ); 01159 return true; 01160 } 01161 01162 # e.g., <svg xmlns="http://www.w3.org/2000/svg"> <handler xmlns:ev="http://www.w3.org/2001/xml-events" ev:event="load">alert(1)</handler> </svg> 01163 if( $strippedElement == 'handler' ) { 01164 wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file.\n" ); 01165 return true; 01166 } 01167 01168 # SVG reported in Feb '12 that used xml:stylesheet to generate javascript block 01169 if( $strippedElement == 'stylesheet' ) { 01170 wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file.\n" ); 01171 return true; 01172 } 01173 01174 # Block iframes, in case they pass the namespace check 01175 if ( $strippedElement == 'iframe' ) { 01176 wfDebug( __METHOD__ . ": iframe in uploaded file.\n" ); 01177 return true; 01178 } 01179 01180 01181 # Check <style> css 01182 if ( $strippedElement == 'style' 01183 && self::checkCssFragment( Sanitizer::normalizeCss( $data ) ) 01184 ) { 01185 wfDebug( __METHOD__ . ": hostile css in style element.\n" ); 01186 return true; 01187 } 01188 01189 foreach( $attribs as $attrib => $value ) { 01190 $stripped = $this->stripXmlNamespace( $attrib ); 01191 $value = strtolower($value); 01192 01193 if( substr( $stripped, 0, 2 ) == 'on' ) { 01194 wfDebug( __METHOD__ . ": Found event-handler attribute '$attrib'='$value' in uploaded file.\n" ); 01195 return true; 01196 } 01197 01198 # href with non-local target (don't allow http://, javascript:, etc) 01199 if ( $stripped == 'href' 01200 && strpos( $value, 'data:' ) !== 0 01201 && strpos( $value, '#' ) !== 0 01202 ) { 01203 if ( !( $strippedElement === 'a' 01204 && preg_match( '!^https?://!im', $value ) ) 01205 ) { 01206 wfDebug( __METHOD__ . ": Found href attribute <$strippedElement " 01207 . "'$attrib'='$value' in uploaded file.\n" ); 01208 01209 return true; 01210 } 01211 } 01212 01213 # href with embeded svg as target 01214 if( $stripped == 'href' && preg_match( '!data:[^,]*image/svg[^,]*,!sim', $value ) ) { 01215 wfDebug( __METHOD__ . ": Found href to embedded svg \"<$strippedElement '$attrib'='$value'...\" in uploaded file.\n" ); 01216 return true; 01217 } 01218 01219 # href with embeded (text/xml) svg as target 01220 if( $stripped == 'href' && preg_match( '!data:[^,]*text/xml[^,]*,!sim', $value ) ) { 01221 wfDebug( __METHOD__ . ": Found href to embedded svg \"<$strippedElement '$attrib'='$value'...\" in uploaded file.\n" ); 01222 return true; 01223 } 01224 01225 # Change href with animate from (http://html5sec.org/#137). This doesn't seem 01226 # possible without embedding the svg, but filter here in case. 01227 if ( $stripped == 'from' 01228 && $strippedElement === 'animate' 01229 && !preg_match( '!^https?://!im', $value ) 01230 ) { 01231 wfDebug( __METHOD__ . ": Found animate that might be changing href using from " 01232 . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file.\n" ); 01233 01234 return true; 01235 } 01236 01237 # use set/animate to add event-handler attribute to parent 01238 if( ( $strippedElement == 'set' || $strippedElement == 'animate' ) && $stripped == 'attributename' && substr( $value, 0, 2 ) == 'on' ) { 01239 wfDebug( __METHOD__ . ": Found svg setting event-handler attribute with \"<$strippedElement $stripped='$value'...\" in uploaded file.\n" ); 01240 return true; 01241 } 01242 01243 # use set to add href attribute to parent element 01244 if( $strippedElement == 'set' && $stripped == 'attributename' && strpos( $value, 'href' ) !== false ) { 01245 wfDebug( __METHOD__ . ": Found svg setting href attibute '$value' in uploaded file.\n" ); 01246 return true; 01247 } 01248 01249 # use set to add a remote / data / script target to an element 01250 if( $strippedElement == 'set' && $stripped == 'to' && preg_match( '!(http|https|data|script):!sim', $value ) ) { 01251 wfDebug( __METHOD__ . ": Found svg setting attibute to '$value' in uploaded file.\n" ); 01252 return true; 01253 } 01254 01255 01256 # use handler attribute with remote / data / script 01257 if( $stripped == 'handler' && preg_match( '!(http|https|data|script):!sim', $value ) ) { 01258 wfDebug( __METHOD__ . ": Found svg setting handler with remote/data/script '$attrib'='$value' in uploaded file.\n" ); 01259 return true; 01260 } 01261 01262 # use CSS styles to bring in remote code 01263 if ( $stripped == 'style' 01264 && self::checkCssFragment( Sanitizer::normalizeCss( $value ) ) 01265 ) { 01266 wfDebug( __METHOD__ . ": Found svg setting a style with " 01267 . "remote url '$attrib'='$value' in uploaded file.\n" ); 01268 return true; 01269 } 01270 01271 # Several attributes can include css, css character escaping isn't allowed 01272 $cssAttrs = array( 'font', 'clip-path', 'fill', 'filter', 'marker', 01273 'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke' ); 01274 if ( in_array( $stripped, $cssAttrs ) 01275 && self::checkCssFragment( $value ) 01276 ) { 01277 wfDebug( __METHOD__ . ": Found svg setting a style with " 01278 . "remote url '$attrib'='$value' in uploaded file.\n" ); 01279 return true; 01280 } 01281 01282 # image filters can pull in url, which could be svg that executes scripts 01283 if( $strippedElement == 'image' && $stripped == 'filter' && preg_match( '!url\s*\(!sim', $value ) ) { 01284 wfDebug( __METHOD__ . ": Found image filter with url: \"<$strippedElement $stripped='$value'...\" in uploaded file.\n" ); 01285 return true; 01286 } 01287 01288 } 01289 01290 return false; //No scripts detected 01291 } 01292 01300 private static function checkCssFragment( $value ) { 01301 01302 # Forbid external stylesheets, for both reliability and to protect viewer's privacy 01303 if ( strpos( $value, '@import' ) !== false ) { 01304 return true; 01305 } 01306 01307 # We allow @font-face to embed fonts with data: urls, so we snip the string 01308 # 'url' out so this case won't match when we check for urls below 01309 $pattern = '!(@font-face\s*{[^}]*src:)url(\("data:;base64,)!im'; 01310 $value = preg_replace( $pattern, '$1$2', $value ); 01311 01312 # Check for remote and executable CSS. Unlike in Sanitizer::checkCss, the CSS 01313 # properties filter and accelerator don't seem to be useful for xss in SVG files. 01314 # Expression and -o-link don't seem to work either, but filtering them here in case. 01315 # Additionally, we catch remote urls like url("http:..., url('http:..., url(http:..., 01316 # but not local ones such as url("#..., url('#..., url(#.... 01317 if ( preg_match( '!expression 01318 | -o-link\s*: 01319 | -o-link-source\s*: 01320 | -o-replace\s*:!imx', $value ) ) { 01321 return true; 01322 } 01323 01324 if ( preg_match_all( 01325 "!(\s*(url|image|image-set)\s*\(\s*[\"']?\s*[^#]+.*?\))!sim", 01326 $value, 01327 $matches 01328 ) !== 0 01329 ) { 01330 # TODO: redo this in one regex. Until then, url("#whatever") matches the first 01331 foreach ( $matches[1] as $match ) { 01332 if ( !preg_match( "!\s*(url|image|image-set)\s*\(\s*(#|'#|\"#)!im", $match ) ) { 01333 return true; 01334 } 01335 } 01336 } 01337 01338 if ( preg_match( '/[\000-\010\013\016-\037\177]/', $value ) ) { 01339 return true; 01340 } 01341 01342 return false; 01343 } 01344 01350 private static function splitXmlNamespace( $element ) { 01351 // 'http://www.w3.org/2000/svg:script' -> array( 'http://www.w3.org/2000/svg', 'script' ) 01352 $parts = explode( ':', strtolower( $element ) ); 01353 $name = array_pop( $parts ); 01354 $ns = implode( ':', $parts ); 01355 return array( $ns, $name ); 01356 } 01357 01364 public static function checkSvgPICallback( $target, $data ) { 01365 // Don't allow external stylesheets (bug 57550) 01366 if ( preg_match( '/xml-stylesheet/i', $target) ) { 01367 return true; 01368 } 01369 return false; 01370 } 01371 01372 private function stripXmlNamespace( $name ) { 01373 // 'http://www.w3.org/2000/svg:script' -> 'script' 01374 $parts = explode( ':', strtolower( $name ) ); 01375 return array_pop( $parts ); 01376 } 01377 01388 public static function detectVirus( $file ) { 01389 global $wgAntivirus, $wgAntivirusSetup, $wgAntivirusRequired, $wgOut; 01390 01391 if ( !$wgAntivirus ) { 01392 wfDebug( __METHOD__ . ": virus scanner disabled\n" ); 01393 return null; 01394 } 01395 01396 if ( !$wgAntivirusSetup[$wgAntivirus] ) { 01397 wfDebug( __METHOD__ . ": unknown virus scanner: $wgAntivirus\n" ); 01398 $wgOut->wrapWikiMsg( "<div class=\"error\">\n$1\n</div>", 01399 array( 'virus-badscanner', $wgAntivirus ) ); 01400 return wfMsg( 'virus-unknownscanner' ) . " $wgAntivirus"; 01401 } 01402 01403 # look up scanner configuration 01404 $command = $wgAntivirusSetup[$wgAntivirus]['command']; 01405 $exitCodeMap = $wgAntivirusSetup[$wgAntivirus]['codemap']; 01406 $msgPattern = isset( $wgAntivirusSetup[$wgAntivirus]['messagepattern'] ) ? 01407 $wgAntivirusSetup[$wgAntivirus]['messagepattern'] : null; 01408 01409 if ( strpos( $command, "%f" ) === false ) { 01410 # simple pattern: append file to scan 01411 $command .= " " . wfEscapeShellArg( $file ); 01412 } else { 01413 # complex pattern: replace "%f" with file to scan 01414 $command = str_replace( "%f", wfEscapeShellArg( $file ), $command ); 01415 } 01416 01417 wfDebug( __METHOD__ . ": running virus scan: $command \n" ); 01418 01419 # execute virus scanner 01420 $exitCode = false; 01421 01422 # NOTE: there's a 50 line workaround to make stderr redirection work on windows, too. 01423 # that does not seem to be worth the pain. 01424 # Ask me (Duesentrieb) about it if it's ever needed. 01425 $output = wfShellExec( "$command 2>&1", $exitCode ); 01426 01427 # map exit code to AV_xxx constants. 01428 $mappedCode = $exitCode; 01429 if ( $exitCodeMap ) { 01430 if ( isset( $exitCodeMap[$exitCode] ) ) { 01431 $mappedCode = $exitCodeMap[$exitCode]; 01432 } elseif ( isset( $exitCodeMap["*"] ) ) { 01433 $mappedCode = $exitCodeMap["*"]; 01434 } 01435 } 01436 01437 if ( $mappedCode === AV_SCAN_FAILED ) { 01438 # scan failed (code was mapped to false by $exitCodeMap) 01439 wfDebug( __METHOD__ . ": failed to scan $file (code $exitCode).\n" ); 01440 01441 if ( $wgAntivirusRequired ) { 01442 return wfMsg( 'virus-scanfailed', array( $exitCode ) ); 01443 } else { 01444 return null; 01445 } 01446 } elseif ( $mappedCode === AV_SCAN_ABORTED ) { 01447 # scan failed because filetype is unknown (probably imune) 01448 wfDebug( __METHOD__ . ": unsupported file type $file (code $exitCode).\n" ); 01449 return null; 01450 } elseif ( $mappedCode === AV_NO_VIRUS ) { 01451 # no virus found 01452 wfDebug( __METHOD__ . ": file passed virus scan.\n" ); 01453 return false; 01454 } else { 01455 $output = trim( $output ); 01456 01457 if ( !$output ) { 01458 $output = true; #if there's no output, return true 01459 } elseif ( $msgPattern ) { 01460 $groups = array(); 01461 if ( preg_match( $msgPattern, $output, $groups ) ) { 01462 if ( $groups[1] ) { 01463 $output = $groups[1]; 01464 } 01465 } 01466 } 01467 01468 wfDebug( __METHOD__ . ": FOUND VIRUS! scanner feedback: $output \n" ); 01469 return $output; 01470 } 01471 } 01472 01481 private function checkOverwrite( $user ) { 01482 // First check whether the local file can be overwritten 01483 $file = $this->getLocalFile(); 01484 if( $file->exists() ) { 01485 if( !self::userCanReUpload( $user, $file ) ) { 01486 return array( 'fileexists-forbidden', $file->getName() ); 01487 } else { 01488 return true; 01489 } 01490 } 01491 01492 /* Check shared conflicts: if the local file does not exist, but 01493 * wfFindFile finds a file, it exists in a shared repository. 01494 */ 01495 $file = wfFindFile( $this->getTitle() ); 01496 if ( $file && !$user->isAllowed( 'reupload-shared' ) ) { 01497 return array( 'fileexists-shared-forbidden', $file->getName() ); 01498 } 01499 01500 return true; 01501 } 01502 01510 public static function userCanReUpload( User $user, $img ) { 01511 if( $user->isAllowed( 'reupload' ) ) { 01512 return true; // non-conditional 01513 } 01514 if( !$user->isAllowed( 'reupload-own' ) ) { 01515 return false; 01516 } 01517 if( is_string( $img ) ) { 01518 $img = wfLocalFile( $img ); 01519 } 01520 if ( !( $img instanceof LocalFile ) ) { 01521 return false; 01522 } 01523 01524 return $user->getId() == $img->getUser( 'id' ); 01525 } 01526 01538 public static function getExistsWarning( $file ) { 01539 if( $file->exists() ) { 01540 return array( 'warning' => 'exists', 'file' => $file ); 01541 } 01542 01543 if( $file->getTitle()->getArticleID() ) { 01544 return array( 'warning' => 'page-exists', 'file' => $file ); 01545 } 01546 01547 if ( $file->wasDeleted() && !$file->exists() ) { 01548 return array( 'warning' => 'was-deleted', 'file' => $file ); 01549 } 01550 01551 if( strpos( $file->getName(), '.' ) == false ) { 01552 $partname = $file->getName(); 01553 $extension = ''; 01554 } else { 01555 $n = strrpos( $file->getName(), '.' ); 01556 $extension = substr( $file->getName(), $n + 1 ); 01557 $partname = substr( $file->getName(), 0, $n ); 01558 } 01559 $normalizedExtension = File::normalizeExtension( $extension ); 01560 01561 if ( $normalizedExtension != $extension ) { 01562 // We're not using the normalized form of the extension. 01563 // Normal form is lowercase, using most common of alternate 01564 // extensions (eg 'jpg' rather than 'JPEG'). 01565 // 01566 // Check for another file using the normalized form... 01567 $nt_lc = Title::makeTitle( NS_FILE, "{$partname}.{$normalizedExtension}" ); 01568 $file_lc = wfLocalFile( $nt_lc ); 01569 01570 if( $file_lc->exists() ) { 01571 return array( 01572 'warning' => 'exists-normalized', 01573 'file' => $file, 01574 'normalizedFile' => $file_lc 01575 ); 01576 } 01577 } 01578 01579 if ( self::isThumbName( $file->getName() ) ) { 01580 # Check for filenames like 50px- or 180px-, these are mostly thumbnails 01581 $nt_thb = Title::newFromText( substr( $partname , strpos( $partname , '-' ) +1 ) . '.' . $extension, NS_FILE ); 01582 $file_thb = wfLocalFile( $nt_thb ); 01583 if( $file_thb->exists() ) { 01584 return array( 01585 'warning' => 'thumb', 01586 'file' => $file, 01587 'thumbFile' => $file_thb 01588 ); 01589 } else { 01590 // File does not exist, but we just don't like the name 01591 return array( 01592 'warning' => 'thumb-name', 01593 'file' => $file, 01594 'thumbFile' => $file_thb 01595 ); 01596 } 01597 } 01598 01599 01600 foreach( self::getFilenamePrefixBlacklist() as $prefix ) { 01601 if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) { 01602 return array( 01603 'warning' => 'bad-prefix', 01604 'file' => $file, 01605 'prefix' => $prefix 01606 ); 01607 } 01608 } 01609 01610 return false; 01611 } 01612 01616 public static function isThumbName( $filename ) { 01617 $n = strrpos( $filename, '.' ); 01618 $partname = $n ? substr( $filename, 0, $n ) : $filename; 01619 return ( 01620 substr( $partname , 3, 3 ) == 'px-' || 01621 substr( $partname , 2, 3 ) == 'px-' 01622 ) && 01623 preg_match( "/[0-9]{2}/" , substr( $partname , 0, 2 ) ); 01624 } 01625 01631 public static function getFilenamePrefixBlacklist() { 01632 $blacklist = array(); 01633 $message = wfMessage( 'filename-prefix-blacklist' )->inContentLanguage(); 01634 if( !$message->isDisabled() ) { 01635 $lines = explode( "\n", $message->plain() ); 01636 foreach( $lines as $line ) { 01637 // Remove comment lines 01638 $comment = substr( trim( $line ), 0, 1 ); 01639 if ( $comment == '#' || $comment == '' ) { 01640 continue; 01641 } 01642 // Remove additional comments after a prefix 01643 $comment = strpos( $line, '#' ); 01644 if ( $comment > 0 ) { 01645 $line = substr( $line, 0, $comment-1 ); 01646 } 01647 $blacklist[] = trim( $line ); 01648 } 01649 } 01650 return $blacklist; 01651 } 01652 01663 public function getImageInfo( $result ) { 01664 $file = $this->getLocalFile(); 01665 // TODO This cries out for refactoring. We really want to say $file->getAllInfo(); here. 01666 // Perhaps "info" methods should be moved into files, and the API should just wrap them in queries. 01667 if ( $file instanceof UploadStashFile ) { 01668 $imParam = ApiQueryStashImageInfo::getPropertyNames(); 01669 $info = ApiQueryStashImageInfo::getInfo( $file, array_flip( $imParam ), $result ); 01670 } else { 01671 $imParam = ApiQueryImageInfo::getPropertyNames(); 01672 $info = ApiQueryImageInfo::getInfo( $file, array_flip( $imParam ), $result ); 01673 } 01674 return $info; 01675 } 01676 01677 01678 public function convertVerifyErrorToStatus( $error ) { 01679 $code = $error['status']; 01680 unset( $code['status'] ); 01681 return Status::newFatal( $this->getVerificationErrorCode( $code ), $error ); 01682 } 01683 01684 public static function getMaxUploadSize( $forType = null ) { 01685 global $wgMaxUploadSize; 01686 01687 if ( is_array( $wgMaxUploadSize ) ) { 01688 if ( !is_null( $forType ) && isset( $wgMaxUploadSize[$forType] ) ) { 01689 return $wgMaxUploadSize[$forType]; 01690 } else { 01691 return $wgMaxUploadSize['*']; 01692 } 01693 } else { 01694 return intval( $wgMaxUploadSize ); 01695 } 01696 01697 } 01698 }