MediaWiki  REL1_19
UploadStash.php
Go to the documentation of this file.
00001 <?php
00022 class UploadStash {
00023 
00024         // Format of the key for files -- has to be suitable as a filename itself (e.g. ab12cd34ef.jpg)
00025         const KEY_FORMAT_REGEX = '/^[\w-\.]+\.\w*$/';
00026 
00033         public $repo;
00034 
00035         // array of initialized repo objects
00036         protected $files = array();
00037 
00038         // cache of the file metadata that's stored in the database
00039         protected $fileMetadata = array();
00040 
00041         // fileprops cache
00042         protected $fileProps = array();
00043 
00044         // current user
00045         protected $user, $userId, $isLoggedIn;
00046 
00053         public function __construct( FileRepo $repo, $user = null ) {
00054                 // this might change based on wiki's configuration.
00055                 $this->repo = $repo;
00056 
00057                 // if a user was passed, use it. otherwise, attempt to use the global.
00058                 // this keeps FileRepo from breaking when it creates an UploadStash object
00059                 if ( $user ) {
00060                         $this->user = $user;
00061                 } else {
00062                         global $wgUser;
00063                         $this->user = $wgUser;
00064                 }
00065 
00066                 if ( is_object( $this->user ) ) {
00067                         $this->userId = $this->user->getId();
00068                         $this->isLoggedIn = $this->user->isLoggedIn();
00069                 }
00070         }
00071 
00084         public function getFile( $key, $noAuth = false ) {
00085 
00086                 if ( ! preg_match( self::KEY_FORMAT_REGEX, $key ) ) {
00087                         throw new UploadStashBadPathException( "key '$key' is not in a proper format" );
00088                 }
00089 
00090                 if ( !$noAuth ) {
00091                         if ( !$this->isLoggedIn ) {
00092                                 throw new UploadStashNotLoggedInException( __METHOD__ . ' No user is logged in, files must belong to users' );
00093                         }
00094                 }
00095 
00096                 if ( !isset( $this->fileMetadata[$key] ) ) {
00097                         if ( !$this->fetchFileMetadata( $key ) ) {
00098                                 // If nothing was received, it's likely due to replication lag.  Check the master to see if the record is there.
00099                                 $this->fetchFileMetadata( $key, DB_MASTER );
00100                         }
00101 
00102                         if ( !isset( $this->fileMetadata[$key] ) ) {
00103                                 throw new UploadStashFileNotFoundException( "key '$key' not found in stash" );
00104                         }
00105 
00106                         // create $this->files[$key]
00107                         $this->initFile( $key );
00108 
00109                         // fetch fileprops
00110                         $path = $this->fileMetadata[$key]['us_path'];
00111                         $this->fileProps[$key] = $this->repo->getFileProps( $path );
00112                 }
00113 
00114                 if ( ! $this->files[$key]->exists() ) {
00115                         wfDebug( __METHOD__ . " tried to get file at $key, but it doesn't exist\n" );
00116                         throw new UploadStashBadPathException( "path doesn't exist" );
00117                 }
00118 
00119                 if ( !$noAuth ) {
00120                         if ( $this->fileMetadata[$key]['us_user'] != $this->userId ) {
00121                                 throw new UploadStashWrongOwnerException( "This file ($key) doesn't belong to the current user." );
00122                         }
00123                 }
00124 
00125                 return $this->files[$key];
00126         }
00127 
00134         public function getMetadata ( $key ) {
00135                 $this->getFile( $key );
00136                 return $this->fileMetadata[$key];
00137         }
00138 
00145         public function getFileProps ( $key ) {
00146                 $this->getFile( $key );
00147                 return $this->fileProps[$key];
00148         }
00149 
00160         public function stashFile( $path, $sourceType = null ) {
00161                 if ( ! file_exists( $path ) ) {
00162                         wfDebug( __METHOD__ . " tried to stash file at '$path', but it doesn't exist\n" );
00163                         throw new UploadStashBadPathException( "path doesn't exist" );
00164                 }
00165                 $fileProps = FSFile::getPropsFromPath( $path );
00166                 wfDebug( __METHOD__ . " stashing file at '$path'\n" );
00167 
00168                 // we will be initializing from some tmpnam files that don't have extensions.
00169                 // most of MediaWiki assumes all uploaded files have good extensions. So, we fix this.
00170                 $extension = self::getExtensionForPath( $path );
00171                 if ( ! preg_match( "/\\.\\Q$extension\\E$/", $path ) ) {
00172                         $pathWithGoodExtension = "$path.$extension";
00173                         if ( ! rename( $path, $pathWithGoodExtension ) ) {
00174                                 throw new UploadStashFileException( "couldn't rename $path to have a better extension at $pathWithGoodExtension" );
00175                         }
00176                         $path = $pathWithGoodExtension;
00177                 }
00178 
00179                 // If no key was supplied, make one.  a mysql insertid would be totally reasonable here, except
00180                 // that for historical reasons, the key is this random thing instead.  At least it's not guessable.
00181                 //
00182                 // some things that when combined will make a suitably unique key.
00183                 // see: http://www.jwz.org/doc/mid.html
00184                 list ($usec, $sec) = explode( ' ', microtime() );
00185                 $usec = substr($usec, 2);
00186                 $key = wfBaseConvert( $sec . $usec, 10, 36 ) . '.' .
00187                         wfBaseConvert( mt_rand(), 10, 36 ) . '.'.
00188                         $this->userId . '.' .
00189                         $extension;
00190 
00191                 $this->fileProps[$key] = $fileProps;
00192 
00193                 if ( ! preg_match( self::KEY_FORMAT_REGEX, $key ) ) {
00194                         throw new UploadStashBadPathException( "key '$key' is not in a proper format" );
00195                 }
00196 
00197                 wfDebug( __METHOD__ . " key for '$path': $key\n" );
00198 
00199                 // if not already in a temporary area, put it there
00200                 $storeStatus = $this->repo->storeTemp( basename( $path ), $path );
00201 
00202                 if ( ! $storeStatus->isOK() ) {
00203                         // It is a convention in MediaWiki to only return one error per API exception, even if multiple errors
00204                         // are available. We use reset() to pick the "first" thing that was wrong, preferring errors to warnings.
00205                         // 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
00206                         // redesigning API errors significantly.
00207                         // $storeStatus->value just contains the virtual URL (if anything) which is probably useless to the caller
00208                         $error = $storeStatus->getErrorsArray();
00209                         $error = reset( $error );
00210                         if ( ! count( $error ) ) {
00211                                 $error = $storeStatus->getWarningsArray();
00212                                 $error = reset( $error );
00213                                 if ( ! count( $error ) ) {
00214                                         $error = array( 'unknown', 'no error recorded' );
00215                                 }
00216                         }
00217                         // at this point, $error should contain the single "most important" error, plus any parameters.
00218                         throw new UploadStashFileException( "Error storing file in '$path': " . wfMessage( $error )->text() );
00219                 }
00220                 $stashPath = $storeStatus->value;
00221 
00222                 // fetch the current user ID
00223                 if ( !$this->isLoggedIn ) {
00224                         throw new UploadStashNotLoggedInException( __METHOD__ . ' No user is logged in, files must belong to users' );
00225                 }
00226 
00227                 // insert the file metadata into the db.
00228                 wfDebug( __METHOD__ . " inserting $stashPath under $key\n" );
00229                 $dbw = $this->repo->getMasterDb();
00230 
00231                 $this->fileMetadata[$key] = array(
00232                         'us_id' => $dbw->nextSequenceValue( 'uploadstash_us_id_seq' ),
00233                         'us_user' => $this->userId,
00234                         'us_key' => $key,
00235                         'us_orig_path' => $path,
00236                         'us_path' => $stashPath, // virtual URL
00237                         'us_size' => $fileProps['size'],
00238                         'us_sha1' => $fileProps['sha1'],
00239                         'us_mime' => $fileProps['mime'],
00240                         'us_media_type' => $fileProps['media_type'],
00241                         'us_image_width' => $fileProps['width'],
00242                         'us_image_height' => $fileProps['height'],
00243                         'us_image_bits' => $fileProps['bits'],
00244                         'us_source_type' => $sourceType,
00245                         'us_timestamp' => $dbw->timestamp(),
00246                         'us_status' => 'finished'
00247                 );
00248 
00249                 $dbw->insert(
00250                         'uploadstash',
00251                         $this->fileMetadata[$key],
00252                         __METHOD__
00253                 );
00254 
00255                 // store the insertid in the class variable so immediate retrieval (possibly laggy) isn't necesary.
00256                 $this->fileMetadata[$key]['us_id'] = $dbw->insertId();
00257 
00258                 # create the UploadStashFile object for this file.
00259                 $this->initFile( $key );
00260 
00261                 return $this->getFile( $key );
00262         }
00263 
00271         public function clear() {
00272                 if ( !$this->isLoggedIn ) {
00273                         throw new UploadStashNotLoggedInException( __METHOD__ . ' No user is logged in, files must belong to users' );
00274                 }
00275 
00276                 wfDebug( __METHOD__ . ' clearing all rows for user ' . $this->userId . "\n" );
00277                 $dbw = $this->repo->getMasterDb();
00278                 $dbw->delete(
00279                         'uploadstash',
00280                         array( 'us_user' => $this->userId ),
00281                         __METHOD__
00282                 );
00283 
00284                 # destroy objects.
00285                 $this->files = array();
00286                 $this->fileMetadata = array();
00287 
00288                 return true;
00289         }
00290 
00298         public function removeFile( $key ) {
00299                 if ( !$this->isLoggedIn ) {
00300                         throw new UploadStashNotLoggedInException( __METHOD__ . ' No user is logged in, files must belong to users' );
00301                 }
00302 
00303                 $dbw = $this->repo->getMasterDb();
00304 
00305                 // this is a cheap query. it runs on the master so that this function still works when there's lag.
00306                 // it won't be called all that often.
00307                 $row = $dbw->selectRow(
00308                         'uploadstash',
00309                         'us_user',
00310                         array( 'us_key' => $key ),
00311                         __METHOD__
00312                 );
00313 
00314                 if( !$row ) {
00315                         throw new UploadStashNoSuchKeyException( "No such key ($key), cannot remove" );
00316                 }
00317 
00318                 if ( $row->us_user != $this->userId ) {
00319                         throw new UploadStashWrongOwnerException( "Can't delete: the file ($key) doesn't belong to this user." );
00320                 }
00321 
00322                 return $this->removeFileNoAuth( $key );
00323         }
00324 
00325 
00331         public function removeFileNoAuth( $key ) {
00332                 wfDebug( __METHOD__ . " clearing row $key\n" );
00333 
00334                 $dbw = $this->repo->getMasterDb();
00335 
00336                 // this gets its own transaction since it's called serially by the cleanupUploadStash maintenance script
00337                 $dbw->begin( __METHOD__ );
00338                 $dbw->delete(
00339                         'uploadstash',
00340                         array( 'us_key' => $key ),
00341                         __METHOD__
00342                 );
00343                 $dbw->commit( __METHOD__ );
00344 
00345                 // TODO: look into UnregisteredLocalFile and find out why the rv here is sometimes wrong (false when file was removed)
00346                 // for now, ignore.
00347                 $this->files[$key]->remove();
00348 
00349                 unset( $this->files[$key] );
00350                 unset( $this->fileMetadata[$key] );
00351 
00352                 return true;
00353         }
00354 
00361         public function listFiles() {
00362                 if ( !$this->isLoggedIn ) {
00363                         throw new UploadStashNotLoggedInException( __METHOD__ . ' No user is logged in, files must belong to users' );
00364                 }
00365 
00366                 $dbr = $this->repo->getSlaveDb();
00367                 $res = $dbr->select(
00368                         'uploadstash',
00369                         'us_key',
00370                         array( 'us_user' => $this->userId ),
00371                         __METHOD__
00372                 );
00373 
00374                 if ( !is_object( $res ) || $res->numRows() == 0 ) {
00375                         // nothing to do.
00376                         return false;
00377                 }
00378 
00379                 // finish the read before starting writes.
00380                 $keys = array();
00381                 foreach ( $res as $row ) {
00382                         array_push( $keys, $row->us_key );
00383                 }
00384 
00385                 return $keys;
00386         }
00387 
00395         public static function getExtensionForPath( $path ) {
00396                 global $wgFileBlacklist;
00397                 // Does this have an extension?
00398                 $n = strrpos( $path, '.' );
00399                 $extension = null;
00400                 if ( $n !== false ) {
00401                         $extension = $n ? substr( $path, $n + 1 ) : '';
00402                 } else {
00403                         // If not, assume that it should be related to the mime type of the original file.
00404                         $magic = MimeMagic::singleton();
00405                         $mimeType = $magic->guessMimeType( $path );
00406                         $extensions = explode( ' ', MimeMagic::singleton()->getExtensionsForType( $mimeType ) );
00407                         if ( count( $extensions ) ) {
00408                                 $extension = $extensions[0];
00409                         }
00410                 }
00411 
00412                 if ( is_null( $extension ) ) {
00413                         throw new UploadStashFileException( "extension is null" );
00414                 }
00415 
00416                 $extension = File::normalizeExtension( $extension );
00417                 if ( in_array( $extension, $wgFileBlacklist ) ) {
00418                         // The file should already be checked for being evil.
00419                         // However, if somehow we got here, we definitely
00420                         // don't want to give it an extension of .php and
00421                         // put it in a web accesible directory.
00422                         return '';
00423                 }
00424                 return $extension;
00425         }
00426 
00433         protected function fetchFileMetadata( $key, $readFromDB = DB_SLAVE ) {
00434                 // populate $fileMetadata[$key]
00435                 $dbr = null;
00436                 if( $readFromDB === DB_MASTER ) {
00437                         // sometimes reading from the master is necessary, if there's replication lag.
00438                         $dbr = $this->repo->getMasterDb();
00439                 } else {
00440                         $dbr = $this->repo->getSlaveDb();
00441                 }
00442 
00443                 $row = $dbr->selectRow(
00444                         'uploadstash',
00445                         '*',
00446                         array( 'us_key' => $key ),
00447                         __METHOD__
00448                 );
00449 
00450                 if ( !is_object( $row ) ) {
00451                         // key wasn't present in the database. this will happen sometimes.
00452                         return false;
00453                 }
00454 
00455                 $this->fileMetadata[$key] = (array)$row;
00456 
00457                 return true;
00458         }
00459 
00468         protected function initFile( $key ) {
00469                 $file = new UploadStashFile( $this->repo, $this->fileMetadata[$key]['us_path'], $key );
00470                 if ( $file->getSize() === 0 ) {
00471                         throw new UploadStashZeroLengthFileException( "File is zero length" );
00472                 }
00473                 $this->files[$key] = $file;
00474                 return true;
00475         }
00476 }
00477 
00478 class UploadStashFile extends UnregisteredLocalFile {
00479         private $fileKey;
00480         private $urlName;
00481         protected $url;
00482 
00493         public function __construct( $repo, $path, $key ) {
00494                 $this->fileKey = $key;
00495 
00496                 // resolve mwrepo:// urls
00497                 if ( $repo->isVirtualUrl( $path ) ) {
00498                         $path = $repo->resolveVirtualUrl( $path );
00499                 } else {
00500 
00501                         // check if path appears to be sane, no parent traversals, and is in this repo's temp zone.
00502                         $repoTempPath = $repo->getZonePath( 'temp' );
00503                         if ( ( ! $repo->validateFilename( $path ) ) ||
00504                                         ( strpos( $path, $repoTempPath ) !== 0 ) ) {
00505                                 wfDebug( "UploadStash: tried to construct an UploadStashFile from a file that should already exist at '$path', but path is not valid\n" );
00506                                 throw new UploadStashBadPathException( 'path is not valid' );
00507                         }
00508 
00509                         // check if path exists! and is a plain file.
00510                         if ( ! $repo->fileExists( $path, FileRepo::FILES_ONLY ) ) {
00511                                 wfDebug( "UploadStash: tried to construct an UploadStashFile from a file that should already exist at '$path', but path is not found\n" );
00512                                 throw new UploadStashFileNotFoundException( 'cannot find path, or not a plain file' );
00513                         }
00514                 }
00515 
00516                 parent::__construct( false, $repo, $path, false );
00517 
00518                 $this->name = basename( $this->path );
00519         }
00520 
00529         public function getDescriptionUrl() {
00530                 return $this->getUrl();
00531         }
00532 
00541         public function getThumbPath( $thumbName = false ) {
00542                 $path = dirname( $this->path );
00543                 if ( $thumbName !== false ) {
00544                         $path .= "/$thumbName";
00545                 }
00546                 return $path;
00547         }
00548 
00557         function thumbName( $params ) {
00558                 return $this->generateThumbName( $this->getUrlName(), $params );
00559         }
00560 
00566         private function getSpecialUrl( $subPage ) {
00567                 return SpecialPage::getTitleFor( 'UploadStash', $subPage )->getLocalURL();
00568         }
00569 
00579         public function getThumbUrl( $thumbName = false ) {
00580                 wfDebug( __METHOD__ . " getting for $thumbName \n" );
00581                 return $this->getSpecialUrl( 'thumb/' . $this->getUrlName() . '/' . $thumbName );
00582         }
00583 
00590         public function getUrlName() {
00591                 if ( ! $this->urlName ) {
00592                         $this->urlName = $this->fileKey;
00593                 }
00594                 return $this->urlName;
00595         }
00596 
00603         public function getUrl() {
00604                 if ( !isset( $this->url ) ) {
00605                         $this->url = $this->getSpecialUrl( 'file/' . $this->getUrlName() );
00606                 }
00607                 return $this->url;
00608         }
00609 
00616         public function getFullUrl() {
00617                 return $this->getUrl();
00618         }
00619 
00625         public function getFileKey() {
00626                 return $this->fileKey;
00627         }
00628 
00633         public function remove() {
00634                 if ( !$this->repo->fileExists( $this->path, FileRepo::FILES_ONLY ) ) {
00635                         // Maybe the file's already been removed? This could totally happen in UploadBase.
00636                         return true;
00637                 }
00638 
00639                 return $this->repo->freeTemp( $this->path );
00640         }
00641 
00642         public function exists() {
00643                 return $this->repo->fileExists( $this->path, FileRepo::FILES_ONLY );
00644         }
00645 
00646 }
00647 
00648 class UploadStashNotAvailableException extends MWException {};
00649 class UploadStashFileNotFoundException extends MWException {};
00650 class UploadStashBadPathException extends MWException {};
00651 class UploadStashFileException extends MWException {};
00652 class UploadStashZeroLengthFileException extends MWException {};
00653 class UploadStashNotLoggedInException extends MWException {};
00654 class UploadStashWrongOwnerException extends MWException {};
00655 class UploadStashNoSuchKeyException extends MWException {};