MediaWiki  REL1_22
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 
00063     public function execute( $subPage ) {
00064         $this->checkPermissions();
00065 
00066         if ( $subPage === null || $subPage === '' ) {
00067             return $this->showUploads();
00068         }
00069         return $this->showUpload( $subPage );
00070     }
00071 
00080     public function showUpload( $key ) {
00081         // prevent callers from doing standard HTML output -- we'll take it from here
00082         $this->getOutput()->disable();
00083 
00084         try {
00085             $params = $this->parseKey( $key );
00086             if ( $params['type'] === 'thumb' ) {
00087                 return $this->outputThumbFromStash( $params['file'], $params['params'] );
00088             } else {
00089                 return $this->outputLocalFile( $params['file'] );
00090             }
00091         } catch ( UploadStashFileNotFoundException $e ) {
00092             $code = 404;
00093             $message = $e->getMessage();
00094         } catch ( UploadStashZeroLengthFileException $e ) {
00095             $code = 500;
00096             $message = $e->getMessage();
00097         } catch ( UploadStashBadPathException $e ) {
00098             $code = 500;
00099             $message = $e->getMessage();
00100         } catch ( SpecialUploadStashTooLargeException $e ) {
00101             $code = 500;
00102             $message = 'Cannot serve a file larger than ' . self::MAX_SERVE_BYTES . ' bytes. ' . $e->getMessage();
00103         } catch ( Exception $e ) {
00104             $code = 500;
00105             $message = $e->getMessage();
00106         }
00107 
00108         throw new HttpError( $code, $message );
00109     }
00110 
00120     private function parseKey( $key ) {
00121         $type = strtok( $key, '/' );
00122 
00123         if ( $type !== 'file' && $type !== 'thumb' ) {
00124             throw new UploadStashBadPathException( "Unknown type '$type'" );
00125         }
00126         $fileName = strtok( '/' );
00127         $thumbPart = strtok( '/' );
00128         $file = $this->stash->getFile( $fileName );
00129         if ( $type === 'thumb' ) {
00130             $srcNamePos = strrpos( $thumbPart, $fileName );
00131             if ( $srcNamePos === false || $srcNamePos < 1 ) {
00132                 throw new UploadStashBadPathException( 'Unrecognized thumb name' );
00133             }
00134             $paramString = substr( $thumbPart, 0, $srcNamePos - 1 );
00135 
00136             $handler = $file->getHandler();
00137             if ( $handler ) {
00138                 $params = $handler->parseParamString( $paramString );
00139                 return array( 'file' => $file, 'type' => $type, 'params' => $params );
00140             } else {
00141                 throw new UploadStashBadPathException( 'No handler found for ' .
00142                         "mime {$file->getMimeType()} of file {$file->getPath()}" );
00143             }
00144         }
00145 
00146         return array( 'file' => $file, 'type' => $type );
00147     }
00148 
00157     private function outputThumbFromStash( $file, $params ) {
00158 
00159         // this global, if it exists, points to a "scaler", as you might find in the Wikimedia Foundation cluster. See outputRemoteScaledThumb()
00160         // this is part of our horrible NFS-based system, we create a file on a mount point here, but fetch the scaled file from somewhere else that
00161         // happens to share it over NFS
00162         global $wgUploadStashScalerBaseUrl;
00163 
00164         $flags = 0;
00165         if ( $wgUploadStashScalerBaseUrl ) {
00166             $this->outputRemoteScaledThumb( $file, $params, $flags );
00167         } else {
00168             $this->outputLocallyScaledThumb( $file, $params, $flags );
00169         }
00170     }
00171 
00181     private function outputLocallyScaledThumb( $file, $params, $flags ) {
00182 
00183         // n.b. this is stupid, we insist on re-transforming the file every time we are invoked. We rely
00184         // on HTTP caching to ensure this doesn't happen.
00185 
00186         $flags |= File::RENDER_NOW;
00187 
00188         $thumbnailImage = $file->transform( $params, $flags );
00189         if ( !$thumbnailImage ) {
00190             throw new MWException( 'Could not obtain thumbnail' );
00191         }
00192 
00193         // we should have just generated it locally
00194         if ( !$thumbnailImage->getStoragePath() ) {
00195             throw new UploadStashFileNotFoundException( "no local path for scaled item" );
00196         }
00197 
00198         // now we should construct a File, so we can get mime and other such info in a standard way
00199         // n.b. mimetype may be different from original (ogx original -> jpeg thumb)
00200         $thumbFile = new UnregisteredLocalFile( false,
00201             $this->stash->repo, $thumbnailImage->getStoragePath(), false );
00202         if ( !$thumbFile ) {
00203             throw new UploadStashFileNotFoundException( "couldn't create local file object for thumbnail" );
00204         }
00205 
00206         return $this->outputLocalFile( $thumbFile );
00207 
00208     }
00209 
00223     private function outputRemoteScaledThumb( $file, $params, $flags ) {
00224 
00225         // this global probably looks something like 'http://upload.wikimedia.org/wikipedia/test/thumb/temp'
00226         // do not use trailing slash
00227         global $wgUploadStashScalerBaseUrl;
00228         $scalerBaseUrl = $wgUploadStashScalerBaseUrl;
00229 
00230         if ( preg_match( '/^\/\//', $scalerBaseUrl ) ) {
00231             // this is apparently a protocol-relative URL, which makes no sense in this context,
00232             // since this is used for communication that's internal to the application.
00233             // default to http.
00234             $scalerBaseUrl = wfExpandUrl( $scalerBaseUrl, PROTO_CANONICAL );
00235         }
00236 
00237         // We need to use generateThumbName() instead of thumbName(), because
00238         // the suffix needs to match the file name for the remote thumbnailer
00239         // to work
00240         $scalerThumbName = $file->generateThumbName( $file->getName(), $params );
00241         $scalerThumbUrl = $scalerBaseUrl . '/' . $file->getUrlRel() .
00242             '/' . rawurlencode( $scalerThumbName );
00243 
00244         // make a curl call to the scaler to create a thumbnail
00245         $httpOptions = array(
00246             'method' => 'GET',
00247             'timeout' => 'default'
00248         );
00249         $req = MWHttpRequest::factory( $scalerThumbUrl, $httpOptions );
00250         $status = $req->execute();
00251         if ( ! $status->isOK() ) {
00252             $errors = $status->getErrorsArray();
00253             $errorStr = "Fetching thumbnail failed: " . print_r( $errors, 1 );
00254             $errorStr .= "\nurl = $scalerThumbUrl\n";
00255             throw new MWException( $errorStr );
00256         }
00257         $contentType = $req->getResponseHeader( "content-type" );
00258         if ( ! $contentType ) {
00259             throw new MWException( "Missing content-type header" );
00260         }
00261         return $this->outputContents( $req->getContent(), $contentType );
00262     }
00263 
00272     private function outputLocalFile( File $file ) {
00273         if ( $file->getSize() > self::MAX_SERVE_BYTES ) {
00274             throw new SpecialUploadStashTooLargeException();
00275         }
00276         return $file->getRepo()->streamFile( $file->getPath(),
00277             array( 'Content-Transfer-Encoding: binary',
00278                 'Expires: Sun, 17-Jan-2038 19:14:07 GMT' )
00279         );
00280     }
00281 
00290     private function outputContents( $content, $contentType ) {
00291         $size = strlen( $content );
00292         if ( $size > self::MAX_SERVE_BYTES ) {
00293             throw new SpecialUploadStashTooLargeException();
00294         }
00295         self::outputFileHeaders( $contentType, $size );
00296         print $content;
00297         return true;
00298     }
00299 
00307     private static function outputFileHeaders( $contentType, $size ) {
00308         header( "Content-Type: $contentType", true );
00309         header( 'Content-Transfer-Encoding: binary', true );
00310         header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true );
00311         // Bug 53032 - It shouldn't be a problem here, but let's be safe and not cache
00312         header( 'Cache-Control: private' );
00313         header( "Content-Length: $size", true );
00314     }
00315 
00324     public static function tryClearStashedUploads( $formData ) {
00325         if ( isset( $formData['Clear'] ) ) {
00326             $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash();
00327             wfDebug( "stash has: " . print_r( $stash->listFiles(), true ) );
00328             if ( ! $stash->clear() ) {
00329                 return Status::newFatal( 'uploadstash-errclear' );
00330             }
00331         }
00332         return Status::newGood();
00333     }
00334 
00340     private function showUploads() {
00341         // sets the title, etc.
00342         $this->setHeaders();
00343         $this->outputHeader();
00344 
00345         // create the form, which will also be used to execute a callback to process incoming form data
00346         // this design is extremely dubious, but supposedly HTMLForm is our standard now?
00347 
00348         $context = new DerivativeContext( $this->getContext() );
00349         $context->setTitle( $this->getTitle() ); // Remove subpage
00350         $form = new HTMLForm( array(
00351             'Clear' => array(
00352                 'type' => 'hidden',
00353                 'default' => true,
00354                 'name' => 'clear',
00355             )
00356         ), $context, 'clearStashedUploads' );
00357         $form->setSubmitCallback( array( __CLASS__, 'tryClearStashedUploads' ) );
00358         $form->setSubmitTextMsg( 'uploadstash-clear' );
00359 
00360         $form->prepareForm();
00361         $formResult = $form->tryAuthorizedSubmit();
00362 
00363         // show the files + form, if there are any, or just say there are none
00364         $refreshHtml = Html::element( 'a',
00365             array( 'href' => $this->getTitle()->getLocalURL() ),
00366             $this->msg( 'uploadstash-refresh' )->text() );
00367         $files = $this->stash->listFiles();
00368         if ( $files && count( $files ) ) {
00369             sort( $files );
00370             $fileListItemsHtml = '';
00371             foreach ( $files as $file ) {
00372                 // TODO: Use Linker::link or even construct the list in plain wikitext
00373                 $fileListItemsHtml .= Html::rawElement( 'li', array(),
00374                     Html::element( 'a', array( 'href' =>
00375                         $this->getTitle( "file/$file" )->getLocalURL() ), $file )
00376                 );
00377             }
00378             $this->getOutput()->addHtml( Html::rawElement( 'ul', array(), $fileListItemsHtml ) );
00379             $form->displayForm( $formResult );
00380             $this->getOutput()->addHtml( Html::rawElement( 'p', array(), $refreshHtml ) );
00381         } else {
00382             $this->getOutput()->addHtml( Html::rawElement( 'p', array(),
00383                 Html::element( 'span', array(), $this->msg( 'uploadstash-nofiles' )->text() )
00384                 . ' '
00385                 . $refreshHtml
00386             ) );
00387         }
00388 
00389         return true;
00390     }
00391 }
00392 
00393 class SpecialUploadStashTooLargeException extends MWException {};