MediaWiki  master
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 
00110                 if ( ! preg_match( self::KEY_FORMAT_REGEX, $key ) ) {
00111                         throw new UploadStashBadPathException( "key '$key' is not in a proper format" );
00112                 }
00113 
00114                 if ( !$noAuth ) {
00115                         if ( !$this->isLoggedIn ) {
00116                                 throw new UploadStashNotLoggedInException( __METHOD__ . ' No user is logged in, files must belong to users' );
00117                         }
00118                 }
00119 
00120                 if ( !isset( $this->fileMetadata[$key] ) ) {
00121                         if ( !$this->fetchFileMetadata( $key ) ) {
00122                                 // If nothing was received, it's likely due to replication lag.  Check the master to see if the record is there.
00123                                 $this->fetchFileMetadata( $key, DB_MASTER );
00124                         }
00125 
00126                         if ( !isset( $this->fileMetadata[$key] ) ) {
00127                                 throw new UploadStashFileNotFoundException( "key '$key' not found in stash" );
00128                         }
00129 
00130                         // create $this->files[$key]
00131                         $this->initFile( $key );
00132 
00133                         // fetch fileprops
00134                         $path = $this->fileMetadata[$key]['us_path'];
00135                         $this->fileProps[$key] = $this->repo->getFileProps( $path );
00136                 }
00137 
00138                 if ( ! $this->files[$key]->exists() ) {
00139                         wfDebug( __METHOD__ . " tried to get file at $key, but it doesn't exist\n" );
00140                         throw new UploadStashBadPathException( "path doesn't exist" );
00141                 }
00142 
00143                 if ( !$noAuth ) {
00144                         if ( $this->fileMetadata[$key]['us_user'] != $this->userId ) {
00145                                 throw new UploadStashWrongOwnerException( "This file ($key) doesn't belong to the current user." );
00146                         }
00147                 }
00148 
00149                 return $this->files[$key];
00150         }
00151 
00158         public function getMetadata ( $key ) {
00159                 $this->getFile( $key );
00160                 return $this->fileMetadata[$key];
00161         }
00162 
00169         public function getFileProps ( $key ) {
00170                 $this->getFile( $key );
00171                 return $this->fileProps[$key];
00172         }
00173 
00184         public function stashFile( $path, $sourceType = null ) {
00185                 if ( ! file_exists( $path ) ) {
00186                         wfDebug( __METHOD__ . " tried to stash file at '$path', but it doesn't exist\n" );
00187                         throw new UploadStashBadPathException( "path doesn't exist" );
00188                 }
00189                 $fileProps = FSFile::getPropsFromPath( $path );
00190                 wfDebug( __METHOD__ . " stashing file at '$path'\n" );
00191 
00192                 // we will be initializing from some tmpnam files that don't have extensions.
00193                 // most of MediaWiki assumes all uploaded files have good extensions. So, we fix this.
00194                 $extension = self::getExtensionForPath( $path );
00195                 if ( ! preg_match( "/\\.\\Q$extension\\E$/", $path ) ) {
00196                         $pathWithGoodExtension = "$path.$extension";
00197                         if ( ! rename( $path, $pathWithGoodExtension ) ) {
00198                                 throw new UploadStashFileException( "couldn't rename $path to have a better extension at $pathWithGoodExtension" );
00199                         }
00200                         $path = $pathWithGoodExtension;
00201                 }
00202 
00203                 // If no key was supplied, make one.  a mysql insertid would be totally reasonable here, except
00204                 // that for historical reasons, the key is this random thing instead.  At least it's not guessable.
00205                 //
00206                 // some things that when combined will make a suitably unique key.
00207                 // see: http://www.jwz.org/doc/mid.html
00208                 list ($usec, $sec) = explode( ' ', microtime() );
00209                 $usec = substr($usec, 2);
00210                 $key = wfBaseConvert( $sec . $usec, 10, 36 ) . '.' .
00211                         wfBaseConvert( mt_rand(), 10, 36 ) . '.'.
00212                         $this->userId . '.' .
00213                         $extension;
00214 
00215                 $this->fileProps[$key] = $fileProps;
00216 
00217                 if ( ! preg_match( self::KEY_FORMAT_REGEX, $key ) ) {
00218                         throw new UploadStashBadPathException( "key '$key' is not in a proper format" );
00219                 }
00220 
00221                 wfDebug( __METHOD__ . " key for '$path': $key\n" );
00222 
00223                 // if not already in a temporary area, put it there
00224                 $storeStatus = $this->repo->storeTemp( basename( $path ), $path );
00225 
00226                 if ( ! $storeStatus->isOK() ) {
00227                         // It is a convention in MediaWiki to only return one error per API exception, even if multiple errors
00228                         // are available. We use reset() to pick the "first" thing that was wrong, preferring errors to warnings.
00229                         // 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
00230                         // redesigning API errors significantly.
00231                         // $storeStatus->value just contains the virtual URL (if anything) which is probably useless to the caller
00232                         $error = $storeStatus->getErrorsArray();
00233                         $error = reset( $error );
00234                         if ( ! count( $error ) ) {
00235                                 $error = $storeStatus->getWarningsArray();
00236                                 $error = reset( $error );
00237                                 if ( ! count( $error ) ) {
00238                                         $error = array( 'unknown', 'no error recorded' );
00239                                 }
00240                         }
00241                         // at this point, $error should contain the single "most important" error, plus any parameters.
00242                         $errorMsg = array_shift( $error );
00243                         throw new UploadStashFileException( "Error storing file in '$path': " . wfMessage( $errorMsg, $error )->text() );
00244                 }
00245                 $stashPath = $storeStatus->value;
00246 
00247                 // we have renamed the file so we have to cleanup once done
00248                 unlink($path);
00249 
00250                 // fetch the current user ID
00251                 if ( !$this->isLoggedIn ) {
00252                         throw new UploadStashNotLoggedInException( __METHOD__ . ' No user is logged in, files must belong to users' );
00253                 }
00254 
00255                 // insert the file metadata into the db.
00256                 wfDebug( __METHOD__ . " inserting $stashPath under $key\n" );
00257                 $dbw = $this->repo->getMasterDb();
00258 
00259                 $this->fileMetadata[$key] = array(
00260                         'us_id' => $dbw->nextSequenceValue( 'uploadstash_us_id_seq' ),
00261                         'us_user' => $this->userId,
00262                         'us_key' => $key,
00263                         'us_orig_path' => $path,
00264                         'us_path' => $stashPath, // virtual URL
00265                         'us_size' => $fileProps['size'],
00266                         'us_sha1' => $fileProps['sha1'],
00267                         'us_mime' => $fileProps['mime'],
00268                         'us_media_type' => $fileProps['media_type'],
00269                         'us_image_width' => $fileProps['width'],
00270                         'us_image_height' => $fileProps['height'],
00271                         'us_image_bits' => $fileProps['bits'],
00272                         'us_source_type' => $sourceType,
00273                         'us_timestamp' => $dbw->timestamp(),
00274                         'us_status' => 'finished'
00275                 );
00276 
00277                 $dbw->insert(
00278                         'uploadstash',
00279                         $this->fileMetadata[$key],
00280                         __METHOD__
00281                 );
00282 
00283                 // store the insertid in the class variable so immediate retrieval (possibly laggy) isn't necesary.
00284                 $this->fileMetadata[$key]['us_id'] = $dbw->insertId();
00285 
00286                 # create the UploadStashFile object for this file.
00287                 $this->initFile( $key );
00288 
00289                 return $this->getFile( $key );
00290         }
00291 
00299         public function clear() {
00300                 if ( !$this->isLoggedIn ) {
00301                         throw new UploadStashNotLoggedInException( __METHOD__ . ' No user is logged in, files must belong to users' );
00302                 }
00303 
00304                 wfDebug( __METHOD__ . ' clearing all rows for user ' . $this->userId . "\n" );
00305                 $dbw = $this->repo->getMasterDb();
00306                 $dbw->delete(
00307                         'uploadstash',
00308                         array( 'us_user' => $this->userId ),
00309                         __METHOD__
00310                 );
00311 
00312                 # destroy objects.
00313                 $this->files = array();
00314                 $this->fileMetadata = array();
00315 
00316                 return true;
00317         }
00318 
00326         public function removeFile( $key ) {
00327                 if ( !$this->isLoggedIn ) {
00328                         throw new UploadStashNotLoggedInException( __METHOD__ . ' No user is logged in, files must belong to users' );
00329                 }
00330 
00331                 $dbw = $this->repo->getMasterDb();
00332 
00333                 // this is a cheap query. it runs on the master so that this function still works when there's lag.
00334                 // it won't be called all that often.
00335                 $row = $dbw->selectRow(
00336                         'uploadstash',
00337                         'us_user',
00338                         array( 'us_key' => $key ),
00339                         __METHOD__
00340                 );
00341 
00342                 if( !$row ) {
00343                         throw new UploadStashNoSuchKeyException( "No such key ($key), cannot remove" );
00344                 }
00345 
00346                 if ( $row->us_user != $this->userId ) {
00347                         throw new UploadStashWrongOwnerException( "Can't delete: the file ($key) doesn't belong to this user." );
00348                 }
00349 
00350                 return $this->removeFileNoAuth( $key );
00351         }
00352 
00353 
00359         public function removeFileNoAuth( $key ) {
00360                 wfDebug( __METHOD__ . " clearing row $key\n" );
00361 
00362                 $dbw = $this->repo->getMasterDb();
00363 
00364                 $dbw->delete(
00365                         'uploadstash',
00366                         array( 'us_key' => $key ),
00367                         __METHOD__
00368                 );
00369 
00370                 // TODO: look into UnregisteredLocalFile and find out why the rv here is sometimes wrong (false when file was removed)
00371                 // for now, ignore.
00372                 $this->files[$key]->remove();
00373 
00374                 unset( $this->files[$key] );
00375                 unset( $this->fileMetadata[$key] );
00376 
00377                 return true;
00378         }
00379 
00386         public function listFiles() {
00387                 if ( !$this->isLoggedIn ) {
00388                         throw new UploadStashNotLoggedInException( __METHOD__ . ' No user is logged in, files must belong to users' );
00389                 }
00390 
00391                 $dbr = $this->repo->getSlaveDb();
00392                 $res = $dbr->select(
00393                         'uploadstash',
00394                         'us_key',
00395                         array( 'us_user' => $this->userId ),
00396                         __METHOD__
00397                 );
00398 
00399                 if ( !is_object( $res ) || $res->numRows() == 0 ) {
00400                         // nothing to do.
00401                         return false;
00402                 }
00403 
00404                 // finish the read before starting writes.
00405                 $keys = array();
00406                 foreach ( $res as $row ) {
00407                         array_push( $keys, $row->us_key );
00408                 }
00409 
00410                 return $keys;
00411         }
00412 
00423         public static function getExtensionForPath( $path ) {
00424                 // Does this have an extension?
00425                 $n = strrpos( $path, '.' );
00426                 $extension = null;
00427                 if ( $n !== false ) {
00428                         $extension = $n ? substr( $path, $n + 1 ) : '';
00429                 } else {
00430                         // If not, assume that it should be related to the mime type of the original file.
00431                         $magic = MimeMagic::singleton();
00432                         $mimeType = $magic->guessMimeType( $path );
00433                         $extensions = explode( ' ', MimeMagic::singleton()->getExtensionsForType( $mimeType ) );
00434                         if ( count( $extensions ) ) {
00435                                 $extension = $extensions[0];
00436                         }
00437                 }
00438 
00439                 if ( is_null( $extension ) ) {
00440                         throw new UploadStashFileException( "extension is null" );
00441                 }
00442 
00443                 return File::normalizeExtension( $extension );
00444         }
00445 
00453         protected function fetchFileMetadata( $key, $readFromDB = DB_SLAVE ) {
00454                 // populate $fileMetadata[$key]
00455                 $dbr = null;
00456                 if( $readFromDB === DB_MASTER ) {
00457                         // sometimes reading from the master is necessary, if there's replication lag.
00458                         $dbr = $this->repo->getMasterDb();
00459                 } else {
00460                         $dbr = $this->repo->getSlaveDb();
00461                 }
00462 
00463                 $row = $dbr->selectRow(
00464                         'uploadstash',
00465                         '*',
00466                         array( 'us_key' => $key ),
00467                         __METHOD__
00468                 );
00469 
00470                 if ( !is_object( $row ) ) {
00471                         // key wasn't present in the database. this will happen sometimes.
00472                         return false;
00473                 }
00474 
00475                 $this->fileMetadata[$key] = (array)$row;
00476 
00477                 return true;
00478         }
00479 
00487         protected function initFile( $key ) {
00488                 $file = new UploadStashFile( $this->repo, $this->fileMetadata[$key]['us_path'], $key );
00489                 if ( $file->getSize() === 0 ) {
00490                         throw new UploadStashZeroLengthFileException( "File is zero length" );
00491                 }
00492                 $this->files[$key] = $file;
00493                 return true;
00494         }
00495 }
00496 
00497 class UploadStashFile extends UnregisteredLocalFile {
00498         private $fileKey;
00499         private $urlName;
00500         protected $url;
00501 
00512         public function __construct( $repo, $path, $key ) {
00513                 $this->fileKey = $key;
00514 
00515                 // resolve mwrepo:// urls
00516                 if ( $repo->isVirtualUrl( $path ) ) {
00517                         $path = $repo->resolveVirtualUrl( $path );
00518                 } else {
00519 
00520                         // check if path appears to be sane, no parent traversals, and is in this repo's temp zone.
00521                         $repoTempPath = $repo->getZonePath( 'temp' );
00522                         if ( ( ! $repo->validateFilename( $path ) ) ||
00523                                         ( strpos( $path, $repoTempPath ) !== 0 ) ) {
00524                                 wfDebug( "UploadStash: tried to construct an UploadStashFile from a file that should already exist at '$path', but path is not valid\n" );
00525                                 throw new UploadStashBadPathException( 'path is not valid' );
00526                         }
00527 
00528                         // check if path exists! and is a plain file.
00529                         if ( ! $repo->fileExists( $path ) ) {
00530                                 wfDebug( "UploadStash: tried to construct an UploadStashFile from a file that should already exist at '$path', but path is not found\n" );
00531                                 throw new UploadStashFileNotFoundException( 'cannot find path, or not a plain file' );
00532                         }
00533                 }
00534 
00535                 parent::__construct( false, $repo, $path, false );
00536 
00537                 $this->name = basename( $this->path );
00538         }
00539 
00548         public function getDescriptionUrl() {
00549                 return $this->getUrl();
00550         }
00551 
00560         public function getThumbPath( $thumbName = false ) {
00561                 $path = dirname( $this->path );
00562                 if ( $thumbName !== false ) {
00563                         $path .= "/$thumbName";
00564                 }
00565                 return $path;
00566         }
00567 
00577         function thumbName( $params, $flags = 0 ) {
00578                 return $this->generateThumbName( $this->getUrlName(), $params );
00579         }
00580 
00586         private function getSpecialUrl( $subPage ) {
00587                 return SpecialPage::getTitleFor( 'UploadStash', $subPage )->getLocalURL();
00588         }
00589 
00599         public function getThumbUrl( $thumbName = false ) {
00600                 wfDebug( __METHOD__ . " getting for $thumbName \n" );
00601                 return $this->getSpecialUrl( 'thumb/' . $this->getUrlName() . '/' . $thumbName );
00602         }
00603 
00610         public function getUrlName() {
00611                 if ( ! $this->urlName ) {
00612                         $this->urlName = $this->fileKey;
00613                 }
00614                 return $this->urlName;
00615         }
00616 
00623         public function getUrl() {
00624                 if ( !isset( $this->url ) ) {
00625                         $this->url = $this->getSpecialUrl( 'file/' . $this->getUrlName() );
00626                 }
00627                 return $this->url;
00628         }
00629 
00636         public function getFullUrl() {
00637                 return $this->getUrl();
00638         }
00639 
00645         public function getFileKey() {
00646                 return $this->fileKey;
00647         }
00648 
00653         public function remove() {
00654                 if ( !$this->repo->fileExists( $this->path ) ) {
00655                         // Maybe the file's already been removed? This could totally happen in UploadBase.
00656                         return true;
00657                 }
00658 
00659                 return $this->repo->freeTemp( $this->path );
00660         }
00661 
00662         public function exists() {
00663                 return $this->repo->fileExists( $this->path );
00664         }
00665 
00666 }
00667 
00668 class UploadStashNotAvailableException extends MWException {};
00669 class UploadStashFileNotFoundException extends MWException {};
00670 class UploadStashBadPathException extends MWException {};
00671 class UploadStashFileException extends MWException {};
00672 class UploadStashZeroLengthFileException extends MWException {};
00673 class UploadStashNotLoggedInException extends MWException {};
00674 class UploadStashWrongOwnerException extends MWException {};
00675 class UploadStashNoSuchKeyException extends MWException {};