MediaWiki
REL1_22
|
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' => $dbw->encodeBlob( 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, true ); 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 UploadStashException extends MWException {}; 00679 class UploadStashNotAvailableException extends UploadStashException {}; 00680 class UploadStashFileNotFoundException extends UploadStashException {}; 00681 class UploadStashBadPathException extends UploadStashException {}; 00682 class UploadStashFileException extends UploadStashException {}; 00683 class UploadStashZeroLengthFileException extends UploadStashException {}; 00684 class UploadStashNotLoggedInException extends UploadStashException {}; 00685 class UploadStashWrongOwnerException extends UploadStashException {}; 00686 class UploadStashNoSuchKeyException extends UploadStashException {};