MediaWiki  REL1_23
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         // this global, if it exists, points to a "scaler", as you might find in
00163         // the Wikimedia Foundation cluster. See outputRemoteScaledThumb(). This
00164         // is part of our horrible NFS-based system, we create a file on a mount
00165         // point here, but fetch the scaled file from somewhere else that
00166         // happens to share it over NFS.
00167         global $wgUploadStashScalerBaseUrl;
00168 
00169         $flags = 0;
00170         if ( $wgUploadStashScalerBaseUrl ) {
00171             $this->outputRemoteScaledThumb( $file, $params, $flags );
00172         } else {
00173             $this->outputLocallyScaledThumb( $file, $params, $flags );
00174         }
00175     }
00176 
00186     private function outputLocallyScaledThumb( $file, $params, $flags ) {
00187         // n.b. this is stupid, we insist on re-transforming the file every time we are invoked. We rely
00188         // on HTTP caching to ensure this doesn't happen.
00189 
00190         $flags |= File::RENDER_NOW;
00191 
00192         $thumbnailImage = $file->transform( $params, $flags );
00193         if ( !$thumbnailImage ) {
00194             throw new MWException( 'Could not obtain thumbnail' );
00195         }
00196 
00197         // we should have just generated it locally
00198         if ( !$thumbnailImage->getStoragePath() ) {
00199             throw new UploadStashFileNotFoundException( "no local path for scaled item" );
00200         }
00201 
00202         // now we should construct a File, so we can get mime and other such info in a standard way
00203         // n.b. mimetype may be different from original (ogx original -> jpeg thumb)
00204         $thumbFile = new UnregisteredLocalFile( false,
00205             $this->stash->repo, $thumbnailImage->getStoragePath(), false );
00206         if ( !$thumbFile ) {
00207             throw new UploadStashFileNotFoundException( "couldn't create local file object for thumbnail" );
00208         }
00209 
00210         return $this->outputLocalFile( $thumbFile );
00211     }
00212 
00232     private function outputRemoteScaledThumb( $file, $params, $flags ) {
00233         // This global probably looks something like
00234         // 'http://upload.wikimedia.org/wikipedia/test/thumb/temp'. Do not use
00235         // trailing slash.
00236         global $wgUploadStashScalerBaseUrl;
00237         $scalerBaseUrl = $wgUploadStashScalerBaseUrl;
00238 
00239         if ( preg_match( '/^\/\//', $scalerBaseUrl ) ) {
00240             // this is apparently a protocol-relative URL, which makes no sense in this context,
00241             // since this is used for communication that's internal to the application.
00242             // default to http.
00243             $scalerBaseUrl = wfExpandUrl( $scalerBaseUrl, PROTO_CANONICAL );
00244         }
00245 
00246         // We need to use generateThumbName() instead of thumbName(), because
00247         // the suffix needs to match the file name for the remote thumbnailer
00248         // to work
00249         $scalerThumbName = $file->generateThumbName( $file->getName(), $params );
00250         $scalerThumbUrl = $scalerBaseUrl . '/' . $file->getUrlRel() .
00251             '/' . rawurlencode( $scalerThumbName );
00252 
00253         // make a curl call to the scaler to create a thumbnail
00254         $httpOptions = array(
00255             'method' => 'GET',
00256             'timeout' => 'default'
00257         );
00258         $req = MWHttpRequest::factory( $scalerThumbUrl, $httpOptions );
00259         $status = $req->execute();
00260         if ( !$status->isOK() ) {
00261             $errors = $status->getErrorsArray();
00262             $errorStr = "Fetching thumbnail failed: " . print_r( $errors, 1 );
00263             $errorStr .= "\nurl = $scalerThumbUrl\n";
00264             throw new MWException( $errorStr );
00265         }
00266         $contentType = $req->getResponseHeader( "content-type" );
00267         if ( !$contentType ) {
00268             throw new MWException( "Missing content-type header" );
00269         }
00270 
00271         return $this->outputContents( $req->getContent(), $contentType );
00272     }
00273 
00283     private function outputLocalFile( File $file ) {
00284         if ( $file->getSize() > self::MAX_SERVE_BYTES ) {
00285             throw new SpecialUploadStashTooLargeException();
00286         }
00287 
00288         return $file->getRepo()->streamFile( $file->getPath(),
00289             array( 'Content-Transfer-Encoding: binary',
00290                 'Expires: Sun, 17-Jan-2038 19:14:07 GMT' )
00291         );
00292     }
00293 
00302     private function outputContents( $content, $contentType ) {
00303         $size = strlen( $content );
00304         if ( $size > self::MAX_SERVE_BYTES ) {
00305             throw new SpecialUploadStashTooLargeException();
00306         }
00307         self::outputFileHeaders( $contentType, $size );
00308         print $content;
00309 
00310         return true;
00311     }
00312 
00322     private static function outputFileHeaders( $contentType, $size ) {
00323         header( "Content-Type: $contentType", true );
00324         header( 'Content-Transfer-Encoding: binary', true );
00325         header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true );
00326         // Bug 53032 - It shouldn't be a problem here, but let's be safe and not cache
00327         header( 'Cache-Control: private' );
00328         header( "Content-Length: $size", true );
00329     }
00330 
00339     public static function tryClearStashedUploads( $formData ) {
00340         if ( isset( $formData['Clear'] ) ) {
00341             $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash();
00342             wfDebug( 'stash has: ' . print_r( $stash->listFiles(), true ) . "\n" );
00343 
00344             if ( !$stash->clear() ) {
00345                 return Status::newFatal( 'uploadstash-errclear' );
00346             }
00347         }
00348 
00349         return Status::newGood();
00350     }
00351 
00357     private function showUploads() {
00358         // sets the title, etc.
00359         $this->setHeaders();
00360         $this->outputHeader();
00361 
00362         // create the form, which will also be used to execute a callback to process incoming form data
00363         // this design is extremely dubious, but supposedly HTMLForm is our standard now?
00364 
00365         $context = new DerivativeContext( $this->getContext() );
00366         $context->setTitle( $this->getPageTitle() ); // Remove subpage
00367         $form = new HTMLForm( array(
00368             'Clear' => array(
00369                 'type' => 'hidden',
00370                 'default' => true,
00371                 'name' => 'clear',
00372             )
00373         ), $context, 'clearStashedUploads' );
00374         $form->setSubmitCallback( array( __CLASS__, 'tryClearStashedUploads' ) );
00375         $form->setSubmitTextMsg( 'uploadstash-clear' );
00376 
00377         $form->prepareForm();
00378         $formResult = $form->tryAuthorizedSubmit();
00379 
00380         // show the files + form, if there are any, or just say there are none
00381         $refreshHtml = Html::element( 'a',
00382             array( 'href' => $this->getPageTitle()->getLocalURL() ),
00383             $this->msg( 'uploadstash-refresh' )->text() );
00384         $files = $this->stash->listFiles();
00385         if ( $files && count( $files ) ) {
00386             sort( $files );
00387             $fileListItemsHtml = '';
00388             foreach ( $files as $file ) {
00389                 // TODO: Use Linker::link or even construct the list in plain wikitext
00390                 $fileListItemsHtml .= Html::rawElement( 'li', array(),
00391                     Html::element( 'a', array( 'href' =>
00392                         $this->getPageTitle( "file/$file" )->getLocalURL() ), $file )
00393                 );
00394             }
00395             $this->getOutput()->addHtml( Html::rawElement( 'ul', array(), $fileListItemsHtml ) );
00396             $form->displayForm( $formResult );
00397             $this->getOutput()->addHtml( Html::rawElement( 'p', array(), $refreshHtml ) );
00398         } else {
00399             $this->getOutput()->addHtml( Html::rawElement( 'p', array(),
00400                 Html::element( 'span', array(), $this->msg( 'uploadstash-nofiles' )->text() )
00401                 . ' '
00402                 . $refreshHtml
00403             ) );
00404         }
00405 
00406         return true;
00407     }
00408 }
00409 
00410 class SpecialUploadStashTooLargeException extends MWException {
00411 }