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