MediaWiki  REL1_24
SpecialUploadStash.php
Go to the documentation of this file.
00001 <?php
00035 class SpecialUploadStash extends UnlistedSpecialPage {
00036     // UploadStash
00037     private $stash;
00038 
00039     // Since we are directly writing the file to STDOUT,
00040     // we should not be reading in really big files and serving them out.
00041     //
00042     // We also don't want people using this as a file drop, even if they
00043     // share credentials.
00044     //
00045     // This service is really for thumbnails and other such previews while
00046     // uploading.
00047     const MAX_SERVE_BYTES = 1048576; // 1MB
00048 
00049     public function __construct() {
00050         parent::__construct( 'UploadStash', 'upload' );
00051         try {
00052             $this->stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash();
00053         } catch ( UploadStashNotAvailableException $e ) {
00054         }
00055     }
00056 
00064     public function execute( $subPage ) {
00065         $this->checkPermissions();
00066 
00067         if ( $subPage === null || $subPage === '' ) {
00068             return $this->showUploads();
00069         }
00070 
00071         return $this->showUpload( $subPage );
00072     }
00073 
00082     public function showUpload( $key ) {
00083         // prevent callers from doing standard HTML output -- we'll take it from here
00084         $this->getOutput()->disable();
00085 
00086         try {
00087             $params = $this->parseKey( $key );
00088             if ( $params['type'] === 'thumb' ) {
00089                 return $this->outputThumbFromStash( $params['file'], $params['params'] );
00090             } else {
00091                 return $this->outputLocalFile( $params['file'] );
00092             }
00093         } catch ( UploadStashFileNotFoundException $e ) {
00094             $code = 404;
00095             $message = $e->getMessage();
00096         } catch ( UploadStashZeroLengthFileException $e ) {
00097             $code = 500;
00098             $message = $e->getMessage();
00099         } catch ( UploadStashBadPathException $e ) {
00100             $code = 500;
00101             $message = $e->getMessage();
00102         } catch ( SpecialUploadStashTooLargeException $e ) {
00103             $code = 500;
00104             $message = 'Cannot serve a file larger than ' . self::MAX_SERVE_BYTES .
00105                 ' bytes. ' . $e->getMessage();
00106         } catch ( Exception $e ) {
00107             $code = 500;
00108             $message = $e->getMessage();
00109         }
00110 
00111         throw new HttpError( $code, $message );
00112     }
00113 
00123     private function parseKey( $key ) {
00124         $type = strtok( $key, '/' );
00125 
00126         if ( $type !== 'file' && $type !== 'thumb' ) {
00127             throw new UploadStashBadPathException( "Unknown type '$type'" );
00128         }
00129         $fileName = strtok( '/' );
00130         $thumbPart = strtok( '/' );
00131         $file = $this->stash->getFile( $fileName );
00132         if ( $type === 'thumb' ) {
00133             $srcNamePos = strrpos( $thumbPart, $fileName );
00134             if ( $srcNamePos === false || $srcNamePos < 1 ) {
00135                 throw new UploadStashBadPathException( 'Unrecognized thumb name' );
00136             }
00137             $paramString = substr( $thumbPart, 0, $srcNamePos - 1 );
00138 
00139             $handler = $file->getHandler();
00140             if ( $handler ) {
00141                 $params = $handler->parseParamString( $paramString );
00142 
00143                 return array( 'file' => $file, 'type' => $type, 'params' => $params );
00144             } else {
00145                 throw new UploadStashBadPathException( 'No handler found for ' .
00146                     "mime {$file->getMimeType()} of file {$file->getPath()}" );
00147             }
00148         }
00149 
00150         return array( 'file' => $file, 'type' => $type );
00151     }
00152 
00161     private function outputThumbFromStash( $file, $params ) {
00162         $flags = 0;
00163         // this config option, if it exists, points to a "scaler", as you might find in
00164         // the Wikimedia Foundation cluster. See outputRemoteScaledThumb(). This
00165         // is part of our horrible NFS-based system, we create a file on a mount
00166         // point here, but fetch the scaled file from somewhere else that
00167         // happens to share it over NFS.
00168         if ( $this->getConfig()->get( 'UploadStashScalerBaseUrl' ) ) {
00169             $this->outputRemoteScaledThumb( $file, $params, $flags );
00170         } else {
00171             $this->outputLocallyScaledThumb( $file, $params, $flags );
00172         }
00173     }
00174 
00184     private function outputLocallyScaledThumb( $file, $params, $flags ) {
00185         // n.b. this is stupid, we insist on re-transforming the file every time we are invoked. We rely
00186         // on HTTP caching to ensure this doesn't happen.
00187 
00188         $flags |= File::RENDER_NOW;
00189 
00190         $thumbnailImage = $file->transform( $params, $flags );
00191         if ( !$thumbnailImage ) {
00192             throw new MWException( 'Could not obtain thumbnail' );
00193         }
00194 
00195         // we should have just generated it locally
00196         if ( !$thumbnailImage->getStoragePath() ) {
00197             throw new UploadStashFileNotFoundException( "no local path for scaled item" );
00198         }
00199 
00200         // now we should construct a File, so we can get MIME and other such info in a standard way
00201         // n.b. MIME type may be different from original (ogx original -> jpeg thumb)
00202         $thumbFile = new UnregisteredLocalFile( false,
00203             $this->stash->repo, $thumbnailImage->getStoragePath(), false );
00204         if ( !$thumbFile ) {
00205             throw new UploadStashFileNotFoundException( "couldn't create local file object for thumbnail" );
00206         }
00207 
00208         return $this->outputLocalFile( $thumbFile );
00209     }
00210 
00230     private function outputRemoteScaledThumb( $file, $params, $flags ) {
00231         // This option probably looks something like
00232         // 'http://upload.wikimedia.org/wikipedia/test/thumb/temp'. Do not use
00233         // trailing slash.
00234         $scalerBaseUrl = $this->getConfig()->get( 'UploadStashScalerBaseUrl' );
00235 
00236         if ( preg_match( '/^\/\//', $scalerBaseUrl ) ) {
00237             // this is apparently a protocol-relative URL, which makes no sense in this context,
00238             // since this is used for communication that's internal to the application.
00239             // default to http.
00240             $scalerBaseUrl = wfExpandUrl( $scalerBaseUrl, PROTO_CANONICAL );
00241         }
00242 
00243         // We need to use generateThumbName() instead of thumbName(), because
00244         // the suffix needs to match the file name for the remote thumbnailer
00245         // to work
00246         $scalerThumbName = $file->generateThumbName( $file->getName(), $params );
00247         $scalerThumbUrl = $scalerBaseUrl . '/' . $file->getUrlRel() .
00248             '/' . rawurlencode( $scalerThumbName );
00249 
00250         // make a curl call to the scaler to create a thumbnail
00251         $httpOptions = array(
00252             'method' => 'GET',
00253             'timeout' => 'default'
00254         );
00255         $req = MWHttpRequest::factory( $scalerThumbUrl, $httpOptions );
00256         $status = $req->execute();
00257         if ( !$status->isOK() ) {
00258             $errors = $status->getErrorsArray();
00259             $errorStr = "Fetching thumbnail failed: " . print_r( $errors, 1 );
00260             $errorStr .= "\nurl = $scalerThumbUrl\n";
00261             throw new MWException( $errorStr );
00262         }
00263         $contentType = $req->getResponseHeader( "content-type" );
00264         if ( !$contentType ) {
00265             throw new MWException( "Missing content-type header" );
00266         }
00267 
00268         return $this->outputContents( $req->getContent(), $contentType );
00269     }
00270 
00280     private function outputLocalFile( File $file ) {
00281         if ( $file->getSize() > self::MAX_SERVE_BYTES ) {
00282             throw new SpecialUploadStashTooLargeException();
00283         }
00284 
00285         return $file->getRepo()->streamFile( $file->getPath(),
00286             array( 'Content-Transfer-Encoding: binary',
00287                 'Expires: Sun, 17-Jan-2038 19:14:07 GMT' )
00288         );
00289     }
00290 
00299     private function outputContents( $content, $contentType ) {
00300         $size = strlen( $content );
00301         if ( $size > self::MAX_SERVE_BYTES ) {
00302             throw new SpecialUploadStashTooLargeException();
00303         }
00304         self::outputFileHeaders( $contentType, $size );
00305         print $content;
00306 
00307         return true;
00308     }
00309 
00319     private static function outputFileHeaders( $contentType, $size ) {
00320         header( "Content-Type: $contentType", true );
00321         header( 'Content-Transfer-Encoding: binary', true );
00322         header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true );
00323         // Bug 53032 - It shouldn't be a problem here, but let's be safe and not cache
00324         header( 'Cache-Control: private' );
00325         header( "Content-Length: $size", true );
00326     }
00327 
00336     public static function tryClearStashedUploads( $formData ) {
00337         if ( isset( $formData['Clear'] ) ) {
00338             $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash();
00339             wfDebug( 'stash has: ' . print_r( $stash->listFiles(), true ) . "\n" );
00340 
00341             if ( !$stash->clear() ) {
00342                 return Status::newFatal( 'uploadstash-errclear' );
00343             }
00344         }
00345 
00346         return Status::newGood();
00347     }
00348 
00354     private function showUploads() {
00355         // sets the title, etc.
00356         $this->setHeaders();
00357         $this->outputHeader();
00358 
00359         // create the form, which will also be used to execute a callback to process incoming form data
00360         // this design is extremely dubious, but supposedly HTMLForm is our standard now?
00361 
00362         $context = new DerivativeContext( $this->getContext() );
00363         $context->setTitle( $this->getPageTitle() ); // Remove subpage
00364         $form = new HTMLForm( array(
00365             'Clear' => array(
00366                 'type' => 'hidden',
00367                 'default' => true,
00368                 'name' => 'clear',
00369             )
00370         ), $context, 'clearStashedUploads' );
00371         $form->setSubmitCallback( array( __CLASS__, 'tryClearStashedUploads' ) );
00372         $form->setSubmitTextMsg( 'uploadstash-clear' );
00373 
00374         $form->prepareForm();
00375         $formResult = $form->tryAuthorizedSubmit();
00376 
00377         // show the files + form, if there are any, or just say there are none
00378         $refreshHtml = Html::element( 'a',
00379             array( 'href' => $this->getPageTitle()->getLocalURL() ),
00380             $this->msg( 'uploadstash-refresh' )->text() );
00381         $files = $this->stash->listFiles();
00382         if ( $files && count( $files ) ) {
00383             sort( $files );
00384             $fileListItemsHtml = '';
00385             foreach ( $files as $file ) {
00386                 // TODO: Use Linker::link or even construct the list in plain wikitext
00387                 $fileListItemsHtml .= Html::rawElement( 'li', array(),
00388                     Html::element( 'a', array( 'href' =>
00389                         $this->getPageTitle( "file/$file" )->getLocalURL() ), $file )
00390                 );
00391             }
00392             $this->getOutput()->addHtml( Html::rawElement( 'ul', array(), $fileListItemsHtml ) );
00393             $form->displayForm( $formResult );
00394             $this->getOutput()->addHtml( Html::rawElement( 'p', array(), $refreshHtml ) );
00395         } else {
00396             $this->getOutput()->addHtml( Html::rawElement( 'p', array(),
00397                 Html::element( 'span', array(), $this->msg( 'uploadstash-nofiles' )->text() )
00398                 . ' '
00399                 . $refreshHtml
00400             ) );
00401         }
00402 
00403         return true;
00404     }
00405 }
00406 
00407 class SpecialUploadStashTooLargeException extends MWException {
00408 }