MediaWiki
REL1_20
|
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 // this gets its own transaction since it's called serially by the cleanupUploadStash maintenance script 00365 $dbw->begin( __METHOD__ ); 00366 $dbw->delete( 00367 'uploadstash', 00368 array( 'us_key' => $key ), 00369 __METHOD__ 00370 ); 00371 $dbw->commit( __METHOD__ ); 00372 00373 // TODO: look into UnregisteredLocalFile and find out why the rv here is sometimes wrong (false when file was removed) 00374 // for now, ignore. 00375 $this->files[$key]->remove(); 00376 00377 unset( $this->files[$key] ); 00378 unset( $this->fileMetadata[$key] ); 00379 00380 return true; 00381 } 00382 00389 public function listFiles() { 00390 if ( !$this->isLoggedIn ) { 00391 throw new UploadStashNotLoggedInException( __METHOD__ . ' No user is logged in, files must belong to users' ); 00392 } 00393 00394 $dbr = $this->repo->getSlaveDb(); 00395 $res = $dbr->select( 00396 'uploadstash', 00397 'us_key', 00398 array( 'us_user' => $this->userId ), 00399 __METHOD__ 00400 ); 00401 00402 if ( !is_object( $res ) || $res->numRows() == 0 ) { 00403 // nothing to do. 00404 return false; 00405 } 00406 00407 // finish the read before starting writes. 00408 $keys = array(); 00409 foreach ( $res as $row ) { 00410 array_push( $keys, $row->us_key ); 00411 } 00412 00413 return $keys; 00414 } 00415 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 {};