MediaWiki  REL1_21
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' => 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 
00357         public function removeFileNoAuth( $key ) {
00358                 wfDebug( __METHOD__ . " clearing row $key\n" );
00359 
00360                 // Ensure we have the UploadStashFile loaded for this key
00361                 $this->getFile( $key );
00362 
00363                 $dbw = $this->repo->getMasterDb();
00364 
00365                 $dbw->delete(
00366                         'uploadstash',
00367                         array( 'us_key' => $key ),
00368                         __METHOD__
00369                 );
00370 
00371                 // TODO: look into UnregisteredLocalFile and find out why the rv here is sometimes wrong (false when file was removed)
00372                 // for now, ignore.
00373                 $this->files[$key]->remove();
00374 
00375                 unset( $this->files[$key] );
00376                 unset( $this->fileMetadata[$key] );
00377 
00378                 return true;
00379         }
00380 
00387         public function listFiles() {
00388                 if ( !$this->isLoggedIn ) {
00389                         throw new UploadStashNotLoggedInException( __METHOD__ . ' No user is logged in, files must belong to users' );
00390                 }
00391 
00392                 $dbr = $this->repo->getSlaveDb();
00393                 $res = $dbr->select(
00394                         'uploadstash',
00395                         'us_key',
00396                         array( 'us_user' => $this->userId ),
00397                         __METHOD__
00398                 );
00399 
00400                 if ( !is_object( $res ) || $res->numRows() == 0 ) {
00401                         // nothing to do.
00402                         return false;
00403                 }
00404 
00405                 // finish the read before starting writes.
00406                 $keys = array();
00407                 foreach ( $res as $row ) {
00408                         array_push( $keys, $row->us_key );
00409                 }
00410 
00411                 return $keys;
00412         }
00413 
00424         public static function getExtensionForPath( $path ) {
00425                 global $wgFileBlacklist;
00426                 // Does this have an extension?
00427                 $n = strrpos( $path, '.' );
00428                 $extension = null;
00429                 if ( $n !== false ) {
00430                         $extension = $n ? substr( $path, $n + 1 ) : '';
00431                 } else {
00432                         // If not, assume that it should be related to the mime type of the original file.
00433                         $magic = MimeMagic::singleton();
00434                         $mimeType = $magic->guessMimeType( $path );
00435                         $extensions = explode( ' ', MimeMagic::singleton()->getExtensionsForType( $mimeType ) );
00436                         if ( count( $extensions ) ) {
00437                                 $extension = $extensions[0];
00438                         }
00439                 }
00440 
00441                 if ( is_null( $extension ) ) {
00442                         throw new UploadStashFileException( "extension is null" );
00443                 }
00444 
00445                 $extension = File::normalizeExtension( $extension );
00446                 if ( in_array( $extension, $wgFileBlacklist ) ) {
00447                         // The file should already be checked for being evil.
00448                         // However, if somehow we got here, we definitely
00449                         // don't want to give it an extension of .php and
00450                         // put it in a web accesible directory.
00451                         return '';
00452                 }
00453                 return $extension;
00454         }
00455 
00463         protected function fetchFileMetadata( $key, $readFromDB = DB_SLAVE ) {
00464                 // populate $fileMetadata[$key]
00465                 $dbr = null;
00466                 if( $readFromDB === DB_MASTER ) {
00467                         // sometimes reading from the master is necessary, if there's replication lag.
00468                         $dbr = $this->repo->getMasterDb();
00469                 } else {
00470                         $dbr = $this->repo->getSlaveDb();
00471                 }
00472 
00473                 $row = $dbr->selectRow(
00474                         'uploadstash',
00475                         '*',
00476                         array( 'us_key' => $key ),
00477                         __METHOD__
00478                 );
00479 
00480                 if ( !is_object( $row ) ) {
00481                         // key wasn't present in the database. this will happen sometimes.
00482                         return false;
00483                 }
00484 
00485                 $this->fileMetadata[$key] = (array)$row;
00486 
00487                 return true;
00488         }
00489 
00497         protected function initFile( $key ) {
00498                 $file = new UploadStashFile( $this->repo, $this->fileMetadata[$key]['us_path'], $key );
00499                 if ( $file->getSize() === 0 ) {
00500                         throw new UploadStashZeroLengthFileException( "File is zero length" );
00501                 }
00502                 $this->files[$key] = $file;
00503                 return true;
00504         }
00505 }
00506 
00507 class UploadStashFile extends UnregisteredLocalFile {
00508         private $fileKey;
00509         private $urlName;
00510         protected $url;
00511 
00522         public function __construct( $repo, $path, $key ) {
00523                 $this->fileKey = $key;
00524 
00525                 // resolve mwrepo:// urls
00526                 if ( $repo->isVirtualUrl( $path ) ) {
00527                         $path = $repo->resolveVirtualUrl( $path );
00528                 } else {
00529 
00530                         // check if path appears to be sane, no parent traversals, and is in this repo's temp zone.
00531                         $repoTempPath = $repo->getZonePath( 'temp' );
00532                         if ( ( ! $repo->validateFilename( $path ) ) ||
00533                                         ( strpos( $path, $repoTempPath ) !== 0 ) ) {
00534                                 wfDebug( "UploadStash: tried to construct an UploadStashFile from a file that should already exist at '$path', but path is not valid\n" );
00535                                 throw new UploadStashBadPathException( 'path is not valid' );
00536                         }
00537 
00538                         // check if path exists! and is a plain file.
00539                         if ( ! $repo->fileExists( $path ) ) {
00540                                 wfDebug( "UploadStash: tried to construct an UploadStashFile from a file that should already exist at '$path', but path is not found\n" );
00541                                 throw new UploadStashFileNotFoundException( 'cannot find path, or not a plain file' );
00542                         }
00543                 }
00544 
00545                 parent::__construct( false, $repo, $path, false );
00546 
00547                 $this->name = basename( $this->path );
00548         }
00549 
00558         public function getDescriptionUrl() {
00559                 return $this->getUrl();
00560         }
00561 
00570         public function getThumbPath( $thumbName = false ) {
00571                 $path = dirname( $this->path );
00572                 if ( $thumbName !== false ) {
00573                         $path .= "/$thumbName";
00574                 }
00575                 return $path;
00576         }
00577 
00587         function thumbName( $params, $flags = 0 ) {
00588                 return $this->generateThumbName( $this->getUrlName(), $params );
00589         }
00590 
00596         private function getSpecialUrl( $subPage ) {
00597                 return SpecialPage::getTitleFor( 'UploadStash', $subPage )->getLocalURL();
00598         }
00599 
00609         public function getThumbUrl( $thumbName = false ) {
00610                 wfDebug( __METHOD__ . " getting for $thumbName \n" );
00611                 return $this->getSpecialUrl( 'thumb/' . $this->getUrlName() . '/' . $thumbName );
00612         }
00613 
00620         public function getUrlName() {
00621                 if ( ! $this->urlName ) {
00622                         $this->urlName = $this->fileKey;
00623                 }
00624                 return $this->urlName;
00625         }
00626 
00633         public function getUrl() {
00634                 if ( !isset( $this->url ) ) {
00635                         $this->url = $this->getSpecialUrl( 'file/' . $this->getUrlName() );
00636                 }
00637                 return $this->url;
00638         }
00639 
00646         public function getFullUrl() {
00647                 return $this->getUrl();
00648         }
00649 
00655         public function getFileKey() {
00656                 return $this->fileKey;
00657         }
00658 
00663         public function remove() {
00664                 if ( !$this->repo->fileExists( $this->path ) ) {
00665                         // Maybe the file's already been removed? This could totally happen in UploadBase.
00666                         return true;
00667                 }
00668 
00669                 return $this->repo->freeTemp( $this->path );
00670         }
00671 
00672         public function exists() {
00673                 return $this->repo->fileExists( $this->path );
00674         }
00675 
00676 }
00677 
00678 class UploadStashNotAvailableException extends MWException {};
00679 class UploadStashFileNotFoundException extends MWException {};
00680 class UploadStashBadPathException extends MWException {};
00681 class UploadStashFileException extends MWException {};
00682 class UploadStashZeroLengthFileException extends MWException {};
00683 class UploadStashNotLoggedInException extends MWException {};
00684 class UploadStashWrongOwnerException extends MWException {};
00685 class UploadStashNoSuchKeyException extends MWException {};