MediaWiki
REL1_24
|
00001 <?php 00054 class UploadStash { 00055 // Format of the key for files -- has to be suitable as a filename itself (e.g. ab12cd34ef.jpg) 00056 const KEY_FORMAT_REGEX = '/^[\w-\.]+\.\w*$/'; 00057 00064 public $repo; 00065 00066 // array of initialized repo objects 00067 protected $files = array(); 00068 00069 // cache of the file metadata that's stored in the database 00070 protected $fileMetadata = array(); 00071 00072 // fileprops cache 00073 protected $fileProps = array(); 00074 00075 // current user 00076 protected $user, $userId, $isLoggedIn; 00077 00086 public function __construct( FileRepo $repo, $user = null ) { 00087 // this might change based on wiki's configuration. 00088 $this->repo = $repo; 00089 00090 // if a user was passed, use it. otherwise, attempt to use the global. 00091 // this keeps FileRepo from breaking when it creates an UploadStash object 00092 if ( $user ) { 00093 $this->user = $user; 00094 } else { 00095 global $wgUser; 00096 $this->user = $wgUser; 00097 } 00098 00099 if ( is_object( $this->user ) ) { 00100 $this->userId = $this->user->getId(); 00101 $this->isLoggedIn = $this->user->isLoggedIn(); 00102 } 00103 } 00104 00118 public function getFile( $key, $noAuth = false ) { 00119 if ( !preg_match( self::KEY_FORMAT_REGEX, $key ) ) { 00120 throw new UploadStashBadPathException( "key '$key' is not in a proper format" ); 00121 } 00122 00123 if ( !$noAuth && !$this->isLoggedIn ) { 00124 throw new UploadStashNotLoggedInException( __METHOD__ . 00125 ' No user is logged in, files must belong to users' ); 00126 } 00127 00128 if ( !isset( $this->fileMetadata[$key] ) ) { 00129 if ( !$this->fetchFileMetadata( $key ) ) { 00130 // If nothing was received, it's likely due to replication lag. 00131 // Check the master to see if the record is there. 00132 $this->fetchFileMetadata( $key, DB_MASTER ); 00133 } 00134 00135 if ( !isset( $this->fileMetadata[$key] ) ) { 00136 throw new UploadStashFileNotFoundException( "key '$key' not found in stash" ); 00137 } 00138 00139 // create $this->files[$key] 00140 $this->initFile( $key ); 00141 00142 // fetch fileprops 00143 if ( strlen( $this->fileMetadata[$key]['us_props'] ) ) { 00144 $this->fileProps[$key] = unserialize( $this->fileMetadata[$key]['us_props'] ); 00145 } else { // b/c for rows with no us_props 00146 wfDebug( __METHOD__ . " fetched props for $key from file\n" ); 00147 $path = $this->fileMetadata[$key]['us_path']; 00148 $this->fileProps[$key] = $this->repo->getFileProps( $path ); 00149 } 00150 } 00151 00152 if ( !$this->files[$key]->exists() ) { 00153 wfDebug( __METHOD__ . " tried to get file at $key, but it doesn't exist\n" ); 00154 throw new UploadStashBadPathException( "path doesn't exist" ); 00155 } 00156 00157 if ( !$noAuth ) { 00158 if ( $this->fileMetadata[$key]['us_user'] != $this->userId ) { 00159 throw new UploadStashWrongOwnerException( "This file ($key) doesn't " 00160 . "belong to the current user." ); 00161 } 00162 } 00163 00164 return $this->files[$key]; 00165 } 00166 00173 public function getMetadata( $key ) { 00174 $this->getFile( $key ); 00175 00176 return $this->fileMetadata[$key]; 00177 } 00178 00185 public function getFileProps( $key ) { 00186 $this->getFile( $key ); 00187 00188 return $this->fileProps[$key]; 00189 } 00190 00203 public function stashFile( $path, $sourceType = null ) { 00204 if ( !is_file( $path ) ) { 00205 wfDebug( __METHOD__ . " tried to stash file at '$path', but it doesn't exist\n" ); 00206 throw new UploadStashBadPathException( "path doesn't exist" ); 00207 } 00208 $fileProps = FSFile::getPropsFromPath( $path ); 00209 wfDebug( __METHOD__ . " stashing file at '$path'\n" ); 00210 00211 // we will be initializing from some tmpnam files that don't have extensions. 00212 // most of MediaWiki assumes all uploaded files have good extensions. So, we fix this. 00213 $extension = self::getExtensionForPath( $path ); 00214 if ( !preg_match( "/\\.\\Q$extension\\E$/", $path ) ) { 00215 $pathWithGoodExtension = "$path.$extension"; 00216 } else { 00217 $pathWithGoodExtension = $path; 00218 } 00219 00220 // If no key was supplied, make one. a mysql insertid would be totally 00221 // reasonable here, except that for historical reasons, the key is this 00222 // random thing instead. At least it's not guessable. 00223 // 00224 // Some things that when combined will make a suitably unique key. 00225 // see: http://www.jwz.org/doc/mid.html 00226 list( $usec, $sec ) = explode( ' ', microtime() ); 00227 $usec = substr( $usec, 2 ); 00228 $key = wfBaseConvert( $sec . $usec, 10, 36 ) . '.' . 00229 wfBaseConvert( mt_rand(), 10, 36 ) . '.' . 00230 $this->userId . '.' . 00231 $extension; 00232 00233 $this->fileProps[$key] = $fileProps; 00234 00235 if ( !preg_match( self::KEY_FORMAT_REGEX, $key ) ) { 00236 throw new UploadStashBadPathException( "key '$key' is not in a proper format" ); 00237 } 00238 00239 wfDebug( __METHOD__ . " key for '$path': $key\n" ); 00240 00241 // if not already in a temporary area, put it there 00242 $storeStatus = $this->repo->storeTemp( basename( $pathWithGoodExtension ), $path ); 00243 00244 if ( !$storeStatus->isOK() ) { 00245 // It is a convention in MediaWiki to only return one error per API 00246 // exception, even if multiple errors are available. We use reset() 00247 // to pick the "first" thing that was wrong, preferring errors to 00248 // warnings. This is a bit lame, as we may have more info in the 00249 // $storeStatus and we're throwing it away, but to fix it means 00250 // redesigning API errors significantly. 00251 // $storeStatus->value just contains the virtual URL (if anything) 00252 // which is probably useless to the caller. 00253 $error = $storeStatus->getErrorsArray(); 00254 $error = reset( $error ); 00255 if ( !count( $error ) ) { 00256 $error = $storeStatus->getWarningsArray(); 00257 $error = reset( $error ); 00258 if ( !count( $error ) ) { 00259 $error = array( 'unknown', 'no error recorded' ); 00260 } 00261 } 00262 // At this point, $error should contain the single "most important" 00263 // error, plus any parameters. 00264 $errorMsg = array_shift( $error ); 00265 throw new UploadStashFileException( "Error storing file in '$path': " 00266 . wfMessage( $errorMsg, $error )->text() ); 00267 } 00268 $stashPath = $storeStatus->value; 00269 00270 // fetch the current user ID 00271 if ( !$this->isLoggedIn ) { 00272 throw new UploadStashNotLoggedInException( __METHOD__ 00273 . ' No user is logged in, files must belong to users' ); 00274 } 00275 00276 // insert the file metadata into the db. 00277 wfDebug( __METHOD__ . " inserting $stashPath under $key\n" ); 00278 $dbw = $this->repo->getMasterDb(); 00279 00280 $this->fileMetadata[$key] = array( 00281 'us_id' => $dbw->nextSequenceValue( 'uploadstash_us_id_seq' ), 00282 'us_user' => $this->userId, 00283 'us_key' => $key, 00284 'us_orig_path' => $path, 00285 'us_path' => $stashPath, // virtual URL 00286 'us_props' => $dbw->encodeBlob( serialize( $fileProps ) ), 00287 'us_size' => $fileProps['size'], 00288 'us_sha1' => $fileProps['sha1'], 00289 'us_mime' => $fileProps['mime'], 00290 'us_media_type' => $fileProps['media_type'], 00291 'us_image_width' => $fileProps['width'], 00292 'us_image_height' => $fileProps['height'], 00293 'us_image_bits' => $fileProps['bits'], 00294 'us_source_type' => $sourceType, 00295 'us_timestamp' => $dbw->timestamp(), 00296 'us_status' => 'finished' 00297 ); 00298 00299 $dbw->insert( 00300 'uploadstash', 00301 $this->fileMetadata[$key], 00302 __METHOD__ 00303 ); 00304 00305 // store the insertid in the class variable so immediate retrieval 00306 // (possibly laggy) isn't necesary. 00307 $this->fileMetadata[$key]['us_id'] = $dbw->insertId(); 00308 00309 # create the UploadStashFile object for this file. 00310 $this->initFile( $key ); 00311 00312 return $this->getFile( $key ); 00313 } 00314 00322 public function clear() { 00323 if ( !$this->isLoggedIn ) { 00324 throw new UploadStashNotLoggedInException( __METHOD__ 00325 . ' No user is logged in, files must belong to users' ); 00326 } 00327 00328 wfDebug( __METHOD__ . ' clearing all rows for user ' . $this->userId . "\n" ); 00329 $dbw = $this->repo->getMasterDb(); 00330 $dbw->delete( 00331 'uploadstash', 00332 array( 'us_user' => $this->userId ), 00333 __METHOD__ 00334 ); 00335 00336 # destroy objects. 00337 $this->files = array(); 00338 $this->fileMetadata = array(); 00339 00340 return true; 00341 } 00342 00351 public function removeFile( $key ) { 00352 if ( !$this->isLoggedIn ) { 00353 throw new UploadStashNotLoggedInException( __METHOD__ 00354 . ' No user is logged in, files must belong to users' ); 00355 } 00356 00357 $dbw = $this->repo->getMasterDb(); 00358 00359 // this is a cheap query. it runs on the master so that this function 00360 // still works when there's lag. It won't be called all that often. 00361 $row = $dbw->selectRow( 00362 'uploadstash', 00363 'us_user', 00364 array( 'us_key' => $key ), 00365 __METHOD__ 00366 ); 00367 00368 if ( !$row ) { 00369 throw new UploadStashNoSuchKeyException( "No such key ($key), cannot remove" ); 00370 } 00371 00372 if ( $row->us_user != $this->userId ) { 00373 throw new UploadStashWrongOwnerException( "Can't delete: " 00374 . "the file ($key) doesn't belong to this user." ); 00375 } 00376 00377 return $this->removeFileNoAuth( $key ); 00378 } 00379 00386 public function removeFileNoAuth( $key ) { 00387 wfDebug( __METHOD__ . " clearing row $key\n" ); 00388 00389 // Ensure we have the UploadStashFile loaded for this key 00390 $this->getFile( $key, true ); 00391 00392 $dbw = $this->repo->getMasterDb(); 00393 00394 $dbw->delete( 00395 'uploadstash', 00396 array( 'us_key' => $key ), 00397 __METHOD__ 00398 ); 00399 00403 $this->files[$key]->remove(); 00404 00405 unset( $this->files[$key] ); 00406 unset( $this->fileMetadata[$key] ); 00407 00408 return true; 00409 } 00410 00417 public function listFiles() { 00418 if ( !$this->isLoggedIn ) { 00419 throw new UploadStashNotLoggedInException( __METHOD__ 00420 . ' No user is logged in, files must belong to users' ); 00421 } 00422 00423 $dbr = $this->repo->getSlaveDb(); 00424 $res = $dbr->select( 00425 'uploadstash', 00426 'us_key', 00427 array( 'us_user' => $this->userId ), 00428 __METHOD__ 00429 ); 00430 00431 if ( !is_object( $res ) || $res->numRows() == 0 ) { 00432 // nothing to do. 00433 return false; 00434 } 00435 00436 // finish the read before starting writes. 00437 $keys = array(); 00438 foreach ( $res as $row ) { 00439 array_push( $keys, $row->us_key ); 00440 } 00441 00442 return $keys; 00443 } 00444 00455 public static function getExtensionForPath( $path ) { 00456 global $wgFileBlacklist; 00457 // Does this have an extension? 00458 $n = strrpos( $path, '.' ); 00459 $extension = null; 00460 if ( $n !== false ) { 00461 $extension = $n ? substr( $path, $n + 1 ) : ''; 00462 } else { 00463 // If not, assume that it should be related to the MIME type of the original file. 00464 $magic = MimeMagic::singleton(); 00465 $mimeType = $magic->guessMimeType( $path ); 00466 $extensions = explode( ' ', MimeMagic::singleton()->getExtensionsForType( $mimeType ) ); 00467 if ( count( $extensions ) ) { 00468 $extension = $extensions[0]; 00469 } 00470 } 00471 00472 if ( is_null( $extension ) ) { 00473 throw new UploadStashFileException( "extension is null" ); 00474 } 00475 00476 $extension = File::normalizeExtension( $extension ); 00477 if ( in_array( $extension, $wgFileBlacklist ) ) { 00478 // The file should already be checked for being evil. 00479 // However, if somehow we got here, we definitely 00480 // don't want to give it an extension of .php and 00481 // put it in a web accesible directory. 00482 return ''; 00483 } 00484 00485 return $extension; 00486 } 00487 00495 protected function fetchFileMetadata( $key, $readFromDB = DB_SLAVE ) { 00496 // populate $fileMetadata[$key] 00497 $dbr = null; 00498 if ( $readFromDB === DB_MASTER ) { 00499 // sometimes reading from the master is necessary, if there's replication lag. 00500 $dbr = $this->repo->getMasterDb(); 00501 } else { 00502 $dbr = $this->repo->getSlaveDb(); 00503 } 00504 00505 $row = $dbr->selectRow( 00506 'uploadstash', 00507 '*', 00508 array( 'us_key' => $key ), 00509 __METHOD__ 00510 ); 00511 00512 if ( !is_object( $row ) ) { 00513 // key wasn't present in the database. this will happen sometimes. 00514 return false; 00515 } 00516 00517 $this->fileMetadata[$key] = (array)$row; 00518 $this->fileMetadata[$key]['us_props'] = $dbr->decodeBlob( $row->us_props ); 00519 00520 return true; 00521 } 00522 00530 protected function initFile( $key ) { 00531 $file = new UploadStashFile( $this->repo, $this->fileMetadata[$key]['us_path'], $key ); 00532 if ( $file->getSize() === 0 ) { 00533 throw new UploadStashZeroLengthFileException( "File is zero length" ); 00534 } 00535 $this->files[$key] = $file; 00536 00537 return true; 00538 } 00539 } 00540 00541 class UploadStashFile extends UnregisteredLocalFile { 00542 private $fileKey; 00543 private $urlName; 00544 protected $url; 00545 00558 public function __construct( $repo, $path, $key ) { 00559 $this->fileKey = $key; 00560 00561 // resolve mwrepo:// urls 00562 if ( $repo->isVirtualUrl( $path ) ) { 00563 $path = $repo->resolveVirtualUrl( $path ); 00564 } else { 00565 // check if path appears to be sane, no parent traversals, 00566 // and is in this repo's temp zone. 00567 $repoTempPath = $repo->getZonePath( 'temp' ); 00568 if ( ( !$repo->validateFilename( $path ) ) || 00569 ( strpos( $path, $repoTempPath ) !== 0 ) 00570 ) { 00571 wfDebug( "UploadStash: tried to construct an UploadStashFile " 00572 . "from a file that should already exist at '$path', but path is not valid\n" ); 00573 throw new UploadStashBadPathException( 'path is not valid' ); 00574 } 00575 00576 // check if path exists! and is a plain file. 00577 if ( !$repo->fileExists( $path ) ) { 00578 wfDebug( "UploadStash: tried to construct an UploadStashFile from " 00579 . "a file that should already exist at '$path', but path is not found\n" ); 00580 throw new UploadStashFileNotFoundException( 'cannot find path, or not a plain file' ); 00581 } 00582 } 00583 00584 parent::__construct( false, $repo, $path, false ); 00585 00586 $this->name = basename( $this->path ); 00587 } 00588 00597 public function getDescriptionUrl() { 00598 return $this->getUrl(); 00599 } 00600 00611 public function getThumbPath( $thumbName = false ) { 00612 $path = dirname( $this->path ); 00613 if ( $thumbName !== false ) { 00614 $path .= "/$thumbName"; 00615 } 00616 00617 return $path; 00618 } 00619 00629 function thumbName( $params, $flags = 0 ) { 00630 return $this->generateThumbName( $this->getUrlName(), $params ); 00631 } 00632 00639 private function getSpecialUrl( $subPage ) { 00640 return SpecialPage::getTitleFor( 'UploadStash', $subPage )->getLocalURL(); 00641 } 00642 00653 public function getThumbUrl( $thumbName = false ) { 00654 wfDebug( __METHOD__ . " getting for $thumbName \n" ); 00655 00656 return $this->getSpecialUrl( 'thumb/' . $this->getUrlName() . '/' . $thumbName ); 00657 } 00658 00665 public function getUrlName() { 00666 if ( !$this->urlName ) { 00667 $this->urlName = $this->fileKey; 00668 } 00669 00670 return $this->urlName; 00671 } 00672 00679 public function getUrl() { 00680 if ( !isset( $this->url ) ) { 00681 $this->url = $this->getSpecialUrl( 'file/' . $this->getUrlName() ); 00682 } 00683 00684 return $this->url; 00685 } 00686 00694 public function getFullUrl() { 00695 return $this->getUrl(); 00696 } 00697 00704 public function getFileKey() { 00705 return $this->fileKey; 00706 } 00707 00712 public function remove() { 00713 if ( !$this->repo->fileExists( $this->path ) ) { 00714 // Maybe the file's already been removed? This could totally happen in UploadBase. 00715 return true; 00716 } 00717 00718 return $this->repo->freeTemp( $this->path ); 00719 } 00720 00721 public function exists() { 00722 return $this->repo->fileExists( $this->path ); 00723 } 00724 } 00725 00726 class UploadStashException extends MWException { 00727 } 00728 00729 class UploadStashNotAvailableException extends UploadStashException { 00730 } 00731 00732 class UploadStashFileNotFoundException extends UploadStashException { 00733 } 00734 00735 class UploadStashBadPathException extends UploadStashException { 00736 } 00737 00738 class UploadStashFileException extends UploadStashException { 00739 } 00740 00741 class UploadStashZeroLengthFileException extends UploadStashException { 00742 } 00743 00744 class UploadStashNotLoggedInException extends UploadStashException { 00745 } 00746 00747 class UploadStashWrongOwnerException extends UploadStashException { 00748 } 00749 00750 class UploadStashNoSuchKeyException extends UploadStashException { 00751 } 00752