MediaWiki  REL1_24
UploadStash.php
Go to the documentation of this file.
00001 <?php
00054 class UploadStash {
00055     // Format of the key for files -- has to be suitable as a filename itself (e.g. ab12cd34ef.jpg)
00056     const KEY_FORMAT_REGEX = '/^[\w-\.]+\.\w*$/';
00057 
00064     public $repo;
00065 
00066     // array of initialized repo objects
00067     protected $files = array();
00068 
00069     // cache of the file metadata that's stored in the database
00070     protected $fileMetadata = array();
00071 
00072     // fileprops cache
00073     protected $fileProps = array();
00074 
00075     // current user
00076     protected $user, $userId, $isLoggedIn;
00077 
00086     public function __construct( FileRepo $repo, $user = null ) {
00087         // this might change based on wiki's configuration.
00088         $this->repo = $repo;
00089 
00090         // if a user was passed, use it. otherwise, attempt to use the global.
00091         // this keeps FileRepo from breaking when it creates an UploadStash object
00092         if ( $user ) {
00093             $this->user = $user;
00094         } else {
00095             global $wgUser;
00096             $this->user = $wgUser;
00097         }
00098 
00099         if ( is_object( $this->user ) ) {
00100             $this->userId = $this->user->getId();
00101             $this->isLoggedIn = $this->user->isLoggedIn();
00102         }
00103     }
00104 
00118     public function getFile( $key, $noAuth = false ) {
00119         if ( !preg_match( self::KEY_FORMAT_REGEX, $key ) ) {
00120             throw new UploadStashBadPathException( "key '$key' is not in a proper format" );
00121         }
00122 
00123         if ( !$noAuth && !$this->isLoggedIn ) {
00124             throw new UploadStashNotLoggedInException( __METHOD__ .
00125                 ' No user is logged in, files must belong to users' );
00126         }
00127 
00128         if ( !isset( $this->fileMetadata[$key] ) ) {
00129             if ( !$this->fetchFileMetadata( $key ) ) {
00130                 // If nothing was received, it's likely due to replication lag.
00131                 // Check the master to see if the record is there.
00132                 $this->fetchFileMetadata( $key, DB_MASTER );
00133             }
00134 
00135             if ( !isset( $this->fileMetadata[$key] ) ) {
00136                 throw new UploadStashFileNotFoundException( "key '$key' not found in stash" );
00137             }
00138 
00139             // create $this->files[$key]
00140             $this->initFile( $key );
00141 
00142             // fetch fileprops
00143             if ( strlen( $this->fileMetadata[$key]['us_props'] ) ) {
00144                 $this->fileProps[$key] = unserialize( $this->fileMetadata[$key]['us_props'] );
00145             } else { // b/c for rows with no us_props
00146                 wfDebug( __METHOD__ . " fetched props for $key from file\n" );
00147                 $path = $this->fileMetadata[$key]['us_path'];
00148                 $this->fileProps[$key] = $this->repo->getFileProps( $path );
00149             }
00150         }
00151 
00152         if ( !$this->files[$key]->exists() ) {
00153             wfDebug( __METHOD__ . " tried to get file at $key, but it doesn't exist\n" );
00154             throw new UploadStashBadPathException( "path doesn't exist" );
00155         }
00156 
00157         if ( !$noAuth ) {
00158             if ( $this->fileMetadata[$key]['us_user'] != $this->userId ) {
00159                 throw new UploadStashWrongOwnerException( "This file ($key) doesn't "
00160                     . "belong to the current user." );
00161             }
00162         }
00163 
00164         return $this->files[$key];
00165     }
00166 
00173     public function getMetadata( $key ) {
00174         $this->getFile( $key );
00175 
00176         return $this->fileMetadata[$key];
00177     }
00178 
00185     public function getFileProps( $key ) {
00186         $this->getFile( $key );
00187 
00188         return $this->fileProps[$key];
00189     }
00190 
00203     public function stashFile( $path, $sourceType = null ) {
00204         if ( !is_file( $path ) ) {
00205             wfDebug( __METHOD__ . " tried to stash file at '$path', but it doesn't exist\n" );
00206             throw new UploadStashBadPathException( "path doesn't exist" );
00207         }
00208         $fileProps = FSFile::getPropsFromPath( $path );
00209         wfDebug( __METHOD__ . " stashing file at '$path'\n" );
00210 
00211         // we will be initializing from some tmpnam files that don't have extensions.
00212         // most of MediaWiki assumes all uploaded files have good extensions. So, we fix this.
00213         $extension = self::getExtensionForPath( $path );
00214         if ( !preg_match( "/\\.\\Q$extension\\E$/", $path ) ) {
00215             $pathWithGoodExtension = "$path.$extension";
00216         } else {
00217             $pathWithGoodExtension = $path;
00218         }
00219 
00220         // If no key was supplied, make one.  a mysql insertid would be totally
00221         // reasonable here, except that for historical reasons, the key is this
00222         // random thing instead.  At least it's not guessable.
00223         //
00224         // Some things that when combined will make a suitably unique key.
00225         // see: http://www.jwz.org/doc/mid.html
00226         list( $usec, $sec ) = explode( ' ', microtime() );
00227         $usec = substr( $usec, 2 );
00228         $key = wfBaseConvert( $sec . $usec, 10, 36 ) . '.' .
00229             wfBaseConvert( mt_rand(), 10, 36 ) . '.' .
00230             $this->userId . '.' .
00231             $extension;
00232 
00233         $this->fileProps[$key] = $fileProps;
00234 
00235         if ( !preg_match( self::KEY_FORMAT_REGEX, $key ) ) {
00236             throw new UploadStashBadPathException( "key '$key' is not in a proper format" );
00237         }
00238 
00239         wfDebug( __METHOD__ . " key for '$path': $key\n" );
00240 
00241         // if not already in a temporary area, put it there
00242         $storeStatus = $this->repo->storeTemp( basename( $pathWithGoodExtension ), $path );
00243 
00244         if ( !$storeStatus->isOK() ) {
00245             // It is a convention in MediaWiki to only return one error per API
00246             // exception, even if multiple errors are available. We use reset()
00247             // to pick the "first" thing that was wrong, preferring errors to
00248             // warnings. This is a bit lame, as we may have more info in the
00249             // $storeStatus and we're throwing it away, but to fix it means
00250             // redesigning API errors significantly.
00251             // $storeStatus->value just contains the virtual URL (if anything)
00252             // which is probably useless to the caller.
00253             $error = $storeStatus->getErrorsArray();
00254             $error = reset( $error );
00255             if ( !count( $error ) ) {
00256                 $error = $storeStatus->getWarningsArray();
00257                 $error = reset( $error );
00258                 if ( !count( $error ) ) {
00259                     $error = array( 'unknown', 'no error recorded' );
00260                 }
00261             }
00262             // At this point, $error should contain the single "most important"
00263             // error, plus any parameters.
00264             $errorMsg = array_shift( $error );
00265             throw new UploadStashFileException( "Error storing file in '$path': "
00266                 . wfMessage( $errorMsg, $error )->text() );
00267         }
00268         $stashPath = $storeStatus->value;
00269 
00270         // fetch the current user ID
00271         if ( !$this->isLoggedIn ) {
00272             throw new UploadStashNotLoggedInException( __METHOD__
00273                 . ' No user is logged in, files must belong to users' );
00274         }
00275 
00276         // insert the file metadata into the db.
00277         wfDebug( __METHOD__ . " inserting $stashPath under $key\n" );
00278         $dbw = $this->repo->getMasterDb();
00279 
00280         $this->fileMetadata[$key] = array(
00281             'us_id' => $dbw->nextSequenceValue( 'uploadstash_us_id_seq' ),
00282             'us_user' => $this->userId,
00283             'us_key' => $key,
00284             'us_orig_path' => $path,
00285             'us_path' => $stashPath, // virtual URL
00286             'us_props' => $dbw->encodeBlob( serialize( $fileProps ) ),
00287             'us_size' => $fileProps['size'],
00288             'us_sha1' => $fileProps['sha1'],
00289             'us_mime' => $fileProps['mime'],
00290             'us_media_type' => $fileProps['media_type'],
00291             'us_image_width' => $fileProps['width'],
00292             'us_image_height' => $fileProps['height'],
00293             'us_image_bits' => $fileProps['bits'],
00294             'us_source_type' => $sourceType,
00295             'us_timestamp' => $dbw->timestamp(),
00296             'us_status' => 'finished'
00297         );
00298 
00299         $dbw->insert(
00300             'uploadstash',
00301             $this->fileMetadata[$key],
00302             __METHOD__
00303         );
00304 
00305         // store the insertid in the class variable so immediate retrieval
00306         // (possibly laggy) isn't necesary.
00307         $this->fileMetadata[$key]['us_id'] = $dbw->insertId();
00308 
00309         # create the UploadStashFile object for this file.
00310         $this->initFile( $key );
00311 
00312         return $this->getFile( $key );
00313     }
00314 
00322     public function clear() {
00323         if ( !$this->isLoggedIn ) {
00324             throw new UploadStashNotLoggedInException( __METHOD__
00325                 . ' No user is logged in, files must belong to users' );
00326         }
00327 
00328         wfDebug( __METHOD__ . ' clearing all rows for user ' . $this->userId . "\n" );
00329         $dbw = $this->repo->getMasterDb();
00330         $dbw->delete(
00331             'uploadstash',
00332             array( 'us_user' => $this->userId ),
00333             __METHOD__
00334         );
00335 
00336         # destroy objects.
00337         $this->files = array();
00338         $this->fileMetadata = array();
00339 
00340         return true;
00341     }
00342 
00351     public function removeFile( $key ) {
00352         if ( !$this->isLoggedIn ) {
00353             throw new UploadStashNotLoggedInException( __METHOD__
00354                 . ' No user is logged in, files must belong to users' );
00355         }
00356 
00357         $dbw = $this->repo->getMasterDb();
00358 
00359         // this is a cheap query. it runs on the master so that this function
00360         // still works when there's lag. It won't be called all that often.
00361         $row = $dbw->selectRow(
00362             'uploadstash',
00363             'us_user',
00364             array( 'us_key' => $key ),
00365             __METHOD__
00366         );
00367 
00368         if ( !$row ) {
00369             throw new UploadStashNoSuchKeyException( "No such key ($key), cannot remove" );
00370         }
00371 
00372         if ( $row->us_user != $this->userId ) {
00373             throw new UploadStashWrongOwnerException( "Can't delete: "
00374                 . "the file ($key) doesn't belong to this user." );
00375         }
00376 
00377         return $this->removeFileNoAuth( $key );
00378     }
00379 
00386     public function removeFileNoAuth( $key ) {
00387         wfDebug( __METHOD__ . " clearing row $key\n" );
00388 
00389         // Ensure we have the UploadStashFile loaded for this key
00390         $this->getFile( $key, true );
00391 
00392         $dbw = $this->repo->getMasterDb();
00393 
00394         $dbw->delete(
00395             'uploadstash',
00396             array( 'us_key' => $key ),
00397             __METHOD__
00398         );
00399 
00403         $this->files[$key]->remove();
00404 
00405         unset( $this->files[$key] );
00406         unset( $this->fileMetadata[$key] );
00407 
00408         return true;
00409     }
00410 
00417     public function listFiles() {
00418         if ( !$this->isLoggedIn ) {
00419             throw new UploadStashNotLoggedInException( __METHOD__
00420                 . ' No user is logged in, files must belong to users' );
00421         }
00422 
00423         $dbr = $this->repo->getSlaveDb();
00424         $res = $dbr->select(
00425             'uploadstash',
00426             'us_key',
00427             array( 'us_user' => $this->userId ),
00428             __METHOD__
00429         );
00430 
00431         if ( !is_object( $res ) || $res->numRows() == 0 ) {
00432             // nothing to do.
00433             return false;
00434         }
00435 
00436         // finish the read before starting writes.
00437         $keys = array();
00438         foreach ( $res as $row ) {
00439             array_push( $keys, $row->us_key );
00440         }
00441 
00442         return $keys;
00443     }
00444 
00455     public static function getExtensionForPath( $path ) {
00456         global $wgFileBlacklist;
00457         // Does this have an extension?
00458         $n = strrpos( $path, '.' );
00459         $extension = null;
00460         if ( $n !== false ) {
00461             $extension = $n ? substr( $path, $n + 1 ) : '';
00462         } else {
00463             // If not, assume that it should be related to the MIME type of the original file.
00464             $magic = MimeMagic::singleton();
00465             $mimeType = $magic->guessMimeType( $path );
00466             $extensions = explode( ' ', MimeMagic::singleton()->getExtensionsForType( $mimeType ) );
00467             if ( count( $extensions ) ) {
00468                 $extension = $extensions[0];
00469             }
00470         }
00471 
00472         if ( is_null( $extension ) ) {
00473             throw new UploadStashFileException( "extension is null" );
00474         }
00475 
00476         $extension = File::normalizeExtension( $extension );
00477         if ( in_array( $extension, $wgFileBlacklist ) ) {
00478             // The file should already be checked for being evil.
00479             // However, if somehow we got here, we definitely
00480             // don't want to give it an extension of .php and
00481             // put it in a web accesible directory.
00482             return '';
00483         }
00484 
00485         return $extension;
00486     }
00487 
00495     protected function fetchFileMetadata( $key, $readFromDB = DB_SLAVE ) {
00496         // populate $fileMetadata[$key]
00497         $dbr = null;
00498         if ( $readFromDB === DB_MASTER ) {
00499             // sometimes reading from the master is necessary, if there's replication lag.
00500             $dbr = $this->repo->getMasterDb();
00501         } else {
00502             $dbr = $this->repo->getSlaveDb();
00503         }
00504 
00505         $row = $dbr->selectRow(
00506             'uploadstash',
00507             '*',
00508             array( 'us_key' => $key ),
00509             __METHOD__
00510         );
00511 
00512         if ( !is_object( $row ) ) {
00513             // key wasn't present in the database. this will happen sometimes.
00514             return false;
00515         }
00516 
00517         $this->fileMetadata[$key] = (array)$row;
00518         $this->fileMetadata[$key]['us_props'] = $dbr->decodeBlob( $row->us_props );
00519 
00520         return true;
00521     }
00522 
00530     protected function initFile( $key ) {
00531         $file = new UploadStashFile( $this->repo, $this->fileMetadata[$key]['us_path'], $key );
00532         if ( $file->getSize() === 0 ) {
00533             throw new UploadStashZeroLengthFileException( "File is zero length" );
00534         }
00535         $this->files[$key] = $file;
00536 
00537         return true;
00538     }
00539 }
00540 
00541 class UploadStashFile extends UnregisteredLocalFile {
00542     private $fileKey;
00543     private $urlName;
00544     protected $url;
00545 
00558     public function __construct( $repo, $path, $key ) {
00559         $this->fileKey = $key;
00560 
00561         // resolve mwrepo:// urls
00562         if ( $repo->isVirtualUrl( $path ) ) {
00563             $path = $repo->resolveVirtualUrl( $path );
00564         } else {
00565             // check if path appears to be sane, no parent traversals,
00566             // and is in this repo's temp zone.
00567             $repoTempPath = $repo->getZonePath( 'temp' );
00568             if ( ( !$repo->validateFilename( $path ) ) ||
00569                 ( strpos( $path, $repoTempPath ) !== 0 )
00570             ) {
00571                 wfDebug( "UploadStash: tried to construct an UploadStashFile "
00572                     . "from a file that should already exist at '$path', but path is not valid\n" );
00573                 throw new UploadStashBadPathException( 'path is not valid' );
00574             }
00575 
00576             // check if path exists! and is a plain file.
00577             if ( !$repo->fileExists( $path ) ) {
00578                 wfDebug( "UploadStash: tried to construct an UploadStashFile from "
00579                     . "a file that should already exist at '$path', but path is not found\n" );
00580                 throw new UploadStashFileNotFoundException( 'cannot find path, or not a plain file' );
00581             }
00582         }
00583 
00584         parent::__construct( false, $repo, $path, false );
00585 
00586         $this->name = basename( $this->path );
00587     }
00588 
00597     public function getDescriptionUrl() {
00598         return $this->getUrl();
00599     }
00600 
00611     public function getThumbPath( $thumbName = false ) {
00612         $path = dirname( $this->path );
00613         if ( $thumbName !== false ) {
00614             $path .= "/$thumbName";
00615         }
00616 
00617         return $path;
00618     }
00619 
00629     function thumbName( $params, $flags = 0 ) {
00630         return $this->generateThumbName( $this->getUrlName(), $params );
00631     }
00632 
00639     private function getSpecialUrl( $subPage ) {
00640         return SpecialPage::getTitleFor( 'UploadStash', $subPage )->getLocalURL();
00641     }
00642 
00653     public function getThumbUrl( $thumbName = false ) {
00654         wfDebug( __METHOD__ . " getting for $thumbName \n" );
00655 
00656         return $this->getSpecialUrl( 'thumb/' . $this->getUrlName() . '/' . $thumbName );
00657     }
00658 
00665     public function getUrlName() {
00666         if ( !$this->urlName ) {
00667             $this->urlName = $this->fileKey;
00668         }
00669 
00670         return $this->urlName;
00671     }
00672 
00679     public function getUrl() {
00680         if ( !isset( $this->url ) ) {
00681             $this->url = $this->getSpecialUrl( 'file/' . $this->getUrlName() );
00682         }
00683 
00684         return $this->url;
00685     }
00686 
00694     public function getFullUrl() {
00695         return $this->getUrl();
00696     }
00697 
00704     public function getFileKey() {
00705         return $this->fileKey;
00706     }
00707 
00712     public function remove() {
00713         if ( !$this->repo->fileExists( $this->path ) ) {
00714             // Maybe the file's already been removed? This could totally happen in UploadBase.
00715             return true;
00716         }
00717 
00718         return $this->repo->freeTemp( $this->path );
00719     }
00720 
00721     public function exists() {
00722         return $this->repo->fileExists( $this->path );
00723     }
00724 }
00725 
00726 class UploadStashException extends MWException {
00727 }
00728 
00729 class UploadStashNotAvailableException extends UploadStashException {
00730 }
00731 
00732 class UploadStashFileNotFoundException extends UploadStashException {
00733 }
00734 
00735 class UploadStashBadPathException extends UploadStashException {
00736 }
00737 
00738 class UploadStashFileException extends UploadStashException {
00739 }
00740 
00741 class UploadStashZeroLengthFileException extends UploadStashException {
00742 }
00743 
00744 class UploadStashNotLoggedInException extends UploadStashException {
00745 }
00746 
00747 class UploadStashWrongOwnerException extends UploadStashException {
00748 }
00749 
00750 class UploadStashNoSuchKeyException extends UploadStashException {
00751 }
00752