MediaWiki
master
|
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 {};