MediaWiki  REL1_23
UploadStash.php
Go to the documentation of this file.
00001 <?php
00044 class UploadStash {
00045 
00046     // Format of the key for files -- has to be suitable as a filename itself (e.g. ab12cd34ef.jpg)
00047     const KEY_FORMAT_REGEX = '/^[\w-\.]+\.\w*$/';
00048 
00055     public $repo;
00056 
00057     // array of initialized repo objects
00058     protected $files = array();
00059 
00060     // cache of the file metadata that's stored in the database
00061     protected $fileMetadata = array();
00062 
00063     // fileprops cache
00064     protected $fileProps = array();
00065 
00066     // current user
00067     protected $user, $userId, $isLoggedIn;
00068 
00077     public function __construct( FileRepo $repo, $user = null ) {
00078         // this might change based on wiki's configuration.
00079         $this->repo = $repo;
00080 
00081         // if a user was passed, use it. otherwise, attempt to use the global.
00082         // this keeps FileRepo from breaking when it creates an UploadStash object
00083         if ( $user ) {
00084             $this->user = $user;
00085         } else {
00086             global $wgUser;
00087             $this->user = $wgUser;
00088         }
00089 
00090         if ( is_object( $this->user ) ) {
00091             $this->userId = $this->user->getId();
00092             $this->isLoggedIn = $this->user->isLoggedIn();
00093         }
00094     }
00095 
00108     public function getFile( $key, $noAuth = false ) {
00109         if ( ! preg_match( self::KEY_FORMAT_REGEX, $key ) ) {
00110             throw new UploadStashBadPathException( "key '$key' is not in a proper format" );
00111         }
00112 
00113         if ( !$noAuth && !$this->isLoggedIn ) {
00114             throw new UploadStashNotLoggedInException( __METHOD__ .
00115                 ' No user is logged in, files must belong to users' );
00116         }
00117 
00118         if ( !isset( $this->fileMetadata[$key] ) ) {
00119             if ( !$this->fetchFileMetadata( $key ) ) {
00120                 // If nothing was received, it's likely due to replication lag.  Check the master to see if the record is there.
00121                 $this->fetchFileMetadata( $key, DB_MASTER );
00122             }
00123 
00124             if ( !isset( $this->fileMetadata[$key] ) ) {
00125                 throw new UploadStashFileNotFoundException( "key '$key' not found in stash" );
00126             }
00127 
00128             // create $this->files[$key]
00129             $this->initFile( $key );
00130 
00131             // fetch fileprops
00132             if ( strlen( $this->fileMetadata[$key]['us_props'] ) ) {
00133                 $this->fileProps[$key] = unserialize( $this->fileMetadata[$key]['us_props'] );
00134             } else { // b/c for rows with no us_props
00135                 wfDebug( __METHOD__ . " fetched props for $key from file\n" );
00136                 $path = $this->fileMetadata[$key]['us_path'];
00137                 $this->fileProps[$key] = $this->repo->getFileProps( $path );
00138             }
00139         }
00140 
00141         if ( ! $this->files[$key]->exists() ) {
00142             wfDebug( __METHOD__ . " tried to get file at $key, but it doesn't exist\n" );
00143             throw new UploadStashBadPathException( "path doesn't exist" );
00144         }
00145 
00146         if ( !$noAuth ) {
00147             if ( $this->fileMetadata[$key]['us_user'] != $this->userId ) {
00148                 throw new UploadStashWrongOwnerException( "This file ($key) doesn't belong to the current user." );
00149             }
00150         }
00151 
00152         return $this->files[$key];
00153     }
00154 
00161     public function getMetadata( $key ) {
00162         $this->getFile( $key );
00163         return $this->fileMetadata[$key];
00164     }
00165 
00172     public function getFileProps( $key ) {
00173         $this->getFile( $key );
00174         return $this->fileProps[$key];
00175     }
00176 
00187     public function stashFile( $path, $sourceType = null ) {
00188         if ( !is_file( $path ) ) {
00189             wfDebug( __METHOD__ . " tried to stash file at '$path', but it doesn't exist\n" );
00190             throw new UploadStashBadPathException( "path doesn't exist" );
00191         }
00192         $fileProps = FSFile::getPropsFromPath( $path );
00193         wfDebug( __METHOD__ . " stashing file at '$path'\n" );
00194 
00195         // we will be initializing from some tmpnam files that don't have extensions.
00196         // most of MediaWiki assumes all uploaded files have good extensions. So, we fix this.
00197         $extension = self::getExtensionForPath( $path );
00198         if ( !preg_match( "/\\.\\Q$extension\\E$/", $path ) ) {
00199             $pathWithGoodExtension = "$path.$extension";
00200         } else {
00201             $pathWithGoodExtension = $path;
00202         }
00203 
00204         // If no key was supplied, make one.  a mysql insertid would be totally reasonable here, except
00205         // that for historical reasons, the key is this random thing instead.  At least it's not guessable.
00206         //
00207         // some things that when combined will make a suitably unique key.
00208         // see: http://www.jwz.org/doc/mid.html
00209         list( $usec, $sec ) = explode( ' ', microtime() );
00210         $usec = substr( $usec, 2 );
00211         $key = wfBaseConvert( $sec . $usec, 10, 36 ) . '.' .
00212             wfBaseConvert( mt_rand(), 10, 36 ) . '.' .
00213             $this->userId . '.' .
00214             $extension;
00215 
00216         $this->fileProps[$key] = $fileProps;
00217 
00218         if ( ! preg_match( self::KEY_FORMAT_REGEX, $key ) ) {
00219             throw new UploadStashBadPathException( "key '$key' is not in a proper format" );
00220         }
00221 
00222         wfDebug( __METHOD__ . " key for '$path': $key\n" );
00223 
00224         // if not already in a temporary area, put it there
00225         $storeStatus = $this->repo->storeTemp( basename( $pathWithGoodExtension ), $path );
00226 
00227         if ( ! $storeStatus->isOK() ) {
00228             // It is a convention in MediaWiki to only return one error per API exception, even if multiple errors
00229             // are available. We use reset() to pick the "first" thing that was wrong, preferring errors to warnings.
00230             // This is a bit lame, as we may have more info in the $storeStatus and we're throwing it away, but to fix it means
00231             // redesigning API errors significantly.
00232             // $storeStatus->value just contains the virtual URL (if anything) which is probably useless to the caller
00233             $error = $storeStatus->getErrorsArray();
00234             $error = reset( $error );
00235             if ( ! count( $error ) ) {
00236                 $error = $storeStatus->getWarningsArray();
00237                 $error = reset( $error );
00238                 if ( ! count( $error ) ) {
00239                     $error = array( 'unknown', 'no error recorded' );
00240                 }
00241             }
00242             // at this point, $error should contain the single "most important" error, plus any parameters.
00243             $errorMsg = array_shift( $error );
00244             throw new UploadStashFileException( "Error storing file in '$path': " . wfMessage( $errorMsg, $error )->text() );
00245         }
00246         $stashPath = $storeStatus->value;
00247 
00248         // fetch the current user ID
00249         if ( !$this->isLoggedIn ) {
00250             throw new UploadStashNotLoggedInException( __METHOD__ . ' No user is logged in, files must belong to users' );
00251         }
00252 
00253         // insert the file metadata into the db.
00254         wfDebug( __METHOD__ . " inserting $stashPath under $key\n" );
00255         $dbw = $this->repo->getMasterDb();
00256 
00257         $this->fileMetadata[$key] = array(
00258             'us_id' => $dbw->nextSequenceValue( 'uploadstash_us_id_seq' ),
00259             'us_user' => $this->userId,
00260             'us_key' => $key,
00261             'us_orig_path' => $path,
00262             'us_path' => $stashPath, // virtual URL
00263             'us_props' => $dbw->encodeBlob( serialize( $fileProps ) ),
00264             'us_size' => $fileProps['size'],
00265             'us_sha1' => $fileProps['sha1'],
00266             'us_mime' => $fileProps['mime'],
00267             'us_media_type' => $fileProps['media_type'],
00268             'us_image_width' => $fileProps['width'],
00269             'us_image_height' => $fileProps['height'],
00270             'us_image_bits' => $fileProps['bits'],
00271             'us_source_type' => $sourceType,
00272             'us_timestamp' => $dbw->timestamp(),
00273             'us_status' => 'finished'
00274         );
00275 
00276         $dbw->insert(
00277             'uploadstash',
00278             $this->fileMetadata[$key],
00279             __METHOD__
00280         );
00281 
00282         // store the insertid in the class variable so immediate retrieval (possibly laggy) isn't necesary.
00283         $this->fileMetadata[$key]['us_id'] = $dbw->insertId();
00284 
00285         # create the UploadStashFile object for this file.
00286         $this->initFile( $key );
00287 
00288         return $this->getFile( $key );
00289     }
00290 
00298     public function clear() {
00299         if ( !$this->isLoggedIn ) {
00300             throw new UploadStashNotLoggedInException( __METHOD__ . ' No user is logged in, files must belong to users' );
00301         }
00302 
00303         wfDebug( __METHOD__ . ' clearing all rows for user ' . $this->userId . "\n" );
00304         $dbw = $this->repo->getMasterDb();
00305         $dbw->delete(
00306             'uploadstash',
00307             array( 'us_user' => $this->userId ),
00308             __METHOD__
00309         );
00310 
00311         # destroy objects.
00312         $this->files = array();
00313         $this->fileMetadata = array();
00314 
00315         return true;
00316     }
00317 
00325     public function removeFile( $key ) {
00326         if ( !$this->isLoggedIn ) {
00327             throw new UploadStashNotLoggedInException( __METHOD__ . ' No user is logged in, files must belong to users' );
00328         }
00329 
00330         $dbw = $this->repo->getMasterDb();
00331 
00332         // this is a cheap query. it runs on the master so that this function still works when there's lag.
00333         // it won't be called all that often.
00334         $row = $dbw->selectRow(
00335             'uploadstash',
00336             'us_user',
00337             array( 'us_key' => $key ),
00338             __METHOD__
00339         );
00340 
00341         if ( !$row ) {
00342             throw new UploadStashNoSuchKeyException( "No such key ($key), cannot remove" );
00343         }
00344 
00345         if ( $row->us_user != $this->userId ) {
00346             throw new UploadStashWrongOwnerException( "Can't delete: the file ($key) doesn't belong to this user." );
00347         }
00348 
00349         return $this->removeFileNoAuth( $key );
00350     }
00351 
00358     public function removeFileNoAuth( $key ) {
00359         wfDebug( __METHOD__ . " clearing row $key\n" );
00360 
00361         // Ensure we have the UploadStashFile loaded for this key
00362         $this->getFile( $key, true );
00363 
00364         $dbw = $this->repo->getMasterDb();
00365 
00366         $dbw->delete(
00367             'uploadstash',
00368             array( 'us_key' => $key ),
00369             __METHOD__
00370         );
00371 
00372         // TODO: look into UnregisteredLocalFile and find out why the rv here is sometimes wrong (false when file was removed)
00373         // for now, ignore.
00374         $this->files[$key]->remove();
00375 
00376         unset( $this->files[$key] );
00377         unset( $this->fileMetadata[$key] );
00378 
00379         return true;
00380     }
00381 
00388     public function listFiles() {
00389         if ( !$this->isLoggedIn ) {
00390             throw new UploadStashNotLoggedInException( __METHOD__ . ' No user is logged in, files must belong to users' );
00391         }
00392 
00393         $dbr = $this->repo->getSlaveDb();
00394         $res = $dbr->select(
00395             'uploadstash',
00396             'us_key',
00397             array( 'us_user' => $this->userId ),
00398             __METHOD__
00399         );
00400 
00401         if ( !is_object( $res ) || $res->numRows() == 0 ) {
00402             // nothing to do.
00403             return false;
00404         }
00405 
00406         // finish the read before starting writes.
00407         $keys = array();
00408         foreach ( $res as $row ) {
00409             array_push( $keys, $row->us_key );
00410         }
00411 
00412         return $keys;
00413     }
00414 
00425     public static function getExtensionForPath( $path ) {
00426         global $wgFileBlacklist;
00427         // Does this have an extension?
00428         $n = strrpos( $path, '.' );
00429         $extension = null;
00430         if ( $n !== false ) {
00431             $extension = $n ? substr( $path, $n + 1 ) : '';
00432         } else {
00433             // If not, assume that it should be related to the mime type of the original file.
00434             $magic = MimeMagic::singleton();
00435             $mimeType = $magic->guessMimeType( $path );
00436             $extensions = explode( ' ', MimeMagic::singleton()->getExtensionsForType( $mimeType ) );
00437             if ( count( $extensions ) ) {
00438                 $extension = $extensions[0];
00439             }
00440         }
00441 
00442         if ( is_null( $extension ) ) {
00443             throw new UploadStashFileException( "extension is null" );
00444         }
00445 
00446         $extension = File::normalizeExtension( $extension );
00447         if ( in_array( $extension, $wgFileBlacklist ) ) {
00448             // The file should already be checked for being evil.
00449             // However, if somehow we got here, we definitely
00450             // don't want to give it an extension of .php and
00451             // put it in a web accesible directory.
00452             return '';
00453         }
00454         return $extension;
00455     }
00456 
00464     protected function fetchFileMetadata( $key, $readFromDB = DB_SLAVE ) {
00465         // populate $fileMetadata[$key]
00466         $dbr = null;
00467         if ( $readFromDB === DB_MASTER ) {
00468             // sometimes reading from the master is necessary, if there's replication lag.
00469             $dbr = $this->repo->getMasterDb();
00470         } else {
00471             $dbr = $this->repo->getSlaveDb();
00472         }
00473 
00474         $row = $dbr->selectRow(
00475             'uploadstash',
00476             '*',
00477             array( 'us_key' => $key ),
00478             __METHOD__
00479         );
00480 
00481         if ( !is_object( $row ) ) {
00482             // key wasn't present in the database. this will happen sometimes.
00483             return false;
00484         }
00485 
00486         $this->fileMetadata[$key] = (array)$row;
00487         $this->fileMetadata[$key]['us_props'] = $dbr->decodeBlob( $row->us_props );
00488 
00489         return true;
00490     }
00491 
00499     protected function initFile( $key ) {
00500         $file = new UploadStashFile( $this->repo, $this->fileMetadata[$key]['us_path'], $key );
00501         if ( $file->getSize() === 0 ) {
00502             throw new UploadStashZeroLengthFileException( "File is zero length" );
00503         }
00504         $this->files[$key] = $file;
00505         return true;
00506     }
00507 }
00508 
00509 class UploadStashFile extends UnregisteredLocalFile {
00510     private $fileKey;
00511     private $urlName;
00512     protected $url;
00513 
00524     public function __construct( $repo, $path, $key ) {
00525         $this->fileKey = $key;
00526 
00527         // resolve mwrepo:// urls
00528         if ( $repo->isVirtualUrl( $path ) ) {
00529             $path = $repo->resolveVirtualUrl( $path );
00530         } else {
00531 
00532             // check if path appears to be sane, no parent traversals, and is in this repo's temp zone.
00533             $repoTempPath = $repo->getZonePath( 'temp' );
00534             if ( ( ! $repo->validateFilename( $path ) ) ||
00535                     ( strpos( $path, $repoTempPath ) !== 0 ) ) {
00536                 wfDebug( "UploadStash: tried to construct an UploadStashFile from a file that should already exist at '$path', but path is not valid\n" );
00537                 throw new UploadStashBadPathException( 'path is not valid' );
00538             }
00539 
00540             // check if path exists! and is a plain file.
00541             if ( ! $repo->fileExists( $path ) ) {
00542                 wfDebug( "UploadStash: tried to construct an UploadStashFile from a file that should already exist at '$path', but path is not found\n" );
00543                 throw new UploadStashFileNotFoundException( 'cannot find path, or not a plain file' );
00544             }
00545         }
00546 
00547         parent::__construct( false, $repo, $path, false );
00548 
00549         $this->name = basename( $this->path );
00550     }
00551 
00560     public function getDescriptionUrl() {
00561         return $this->getUrl();
00562     }
00563 
00572     public function getThumbPath( $thumbName = false ) {
00573         $path = dirname( $this->path );
00574         if ( $thumbName !== false ) {
00575             $path .= "/$thumbName";
00576         }
00577         return $path;
00578     }
00579 
00589     function thumbName( $params, $flags = 0 ) {
00590         return $this->generateThumbName( $this->getUrlName(), $params );
00591     }
00592 
00598     private function getSpecialUrl( $subPage ) {
00599         return SpecialPage::getTitleFor( 'UploadStash', $subPage )->getLocalURL();
00600     }
00601 
00611     public function getThumbUrl( $thumbName = false ) {
00612         wfDebug( __METHOD__ . " getting for $thumbName \n" );
00613         return $this->getSpecialUrl( 'thumb/' . $this->getUrlName() . '/' . $thumbName );
00614     }
00615 
00622     public function getUrlName() {
00623         if ( ! $this->urlName ) {
00624             $this->urlName = $this->fileKey;
00625         }
00626         return $this->urlName;
00627     }
00628 
00635     public function getUrl() {
00636         if ( !isset( $this->url ) ) {
00637             $this->url = $this->getSpecialUrl( 'file/' . $this->getUrlName() );
00638         }
00639         return $this->url;
00640     }
00641 
00648     public function getFullUrl() {
00649         return $this->getUrl();
00650     }
00651 
00657     public function getFileKey() {
00658         return $this->fileKey;
00659     }
00660 
00665     public function remove() {
00666         if ( !$this->repo->fileExists( $this->path ) ) {
00667             // Maybe the file's already been removed? This could totally happen in UploadBase.
00668             return true;
00669         }
00670 
00671         return $this->repo->freeTemp( $this->path );
00672     }
00673 
00674     public function exists() {
00675         return $this->repo->fileExists( $this->path );
00676     }
00677 
00678 }
00679 
00680 class UploadStashException extends MWException {};
00681 class UploadStashNotAvailableException extends UploadStashException {};
00682 class UploadStashFileNotFoundException extends UploadStashException {};
00683 class UploadStashBadPathException extends UploadStashException {};
00684 class UploadStashFileException extends UploadStashException {};
00685 class UploadStashZeroLengthFileException extends UploadStashException {};
00686 class UploadStashNotLoggedInException extends UploadStashException {};
00687 class UploadStashWrongOwnerException extends UploadStashException {};
00688 class UploadStashNoSuchKeyException extends UploadStashException {};