[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/specials/ -> SpecialUploadStash.php (source)

   1  <?php
   2  /**
   3   * Implements Special:UploadStash.
   4   *
   5   * This program is free software; you can redistribute it and/or modify
   6   * it under the terms of the GNU General Public License as published by
   7   * the Free Software Foundation; either version 2 of the License, or
   8   * (at your option) any later version.
   9   *
  10   * This program is distributed in the hope that it will be useful,
  11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13   * GNU General Public License for more details.
  14   *
  15   * You should have received a copy of the GNU General Public License along
  16   * with this program; if not, write to the Free Software Foundation, Inc.,
  17   * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18   * http://www.gnu.org/copyleft/gpl.html
  19   *
  20   * @file
  21   * @ingroup SpecialPage
  22   * @ingroup Upload
  23   */
  24  
  25  /**
  26   * Web access for files temporarily stored by UploadStash.
  27   *
  28   * For example -- files that were uploaded with the UploadWizard extension are stored temporarily
  29   * before committing them to the db. But we want to see their thumbnails and get other information
  30   * about them.
  31   *
  32   * Since this is based on the user's session, in effect this creates a private temporary file area.
  33   * However, the URLs for the files cannot be shared.
  34   */
  35  class SpecialUploadStash extends UnlistedSpecialPage {
  36      // UploadStash
  37      private $stash;
  38  
  39      // Since we are directly writing the file to STDOUT,
  40      // we should not be reading in really big files and serving them out.
  41      //
  42      // We also don't want people using this as a file drop, even if they
  43      // share credentials.
  44      //
  45      // This service is really for thumbnails and other such previews while
  46      // uploading.
  47      const MAX_SERVE_BYTES = 1048576; // 1MB
  48  
  49  	public function __construct() {
  50          parent::__construct( 'UploadStash', 'upload' );
  51          try {
  52              $this->stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash();
  53          } catch ( UploadStashNotAvailableException $e ) {
  54          }
  55      }
  56  
  57      /**
  58       * Execute page -- can output a file directly or show a listing of them.
  59       *
  60       * @param string $subPage Subpage, e.g. in
  61       *   http://example.com/wiki/Special:UploadStash/foo.jpg, the "foo.jpg" part
  62       * @return bool Success
  63       */
  64  	public function execute( $subPage ) {
  65          $this->checkPermissions();
  66  
  67          if ( $subPage === null || $subPage === '' ) {
  68              return $this->showUploads();
  69          }
  70  
  71          return $this->showUpload( $subPage );
  72      }
  73  
  74      /**
  75       * If file available in stash, cats it out to the client as a simple HTTP response.
  76       * n.b. Most sanity checking done in UploadStashLocalFile, so this is straightforward.
  77       *
  78       * @param string $key The key of a particular requested file
  79       * @throws HttpError
  80       * @return bool
  81       */
  82  	public function showUpload( $key ) {
  83          // prevent callers from doing standard HTML output -- we'll take it from here
  84          $this->getOutput()->disable();
  85  
  86          try {
  87              $params = $this->parseKey( $key );
  88              if ( $params['type'] === 'thumb' ) {
  89                  return $this->outputThumbFromStash( $params['file'], $params['params'] );
  90              } else {
  91                  return $this->outputLocalFile( $params['file'] );
  92              }
  93          } catch ( UploadStashFileNotFoundException $e ) {
  94              $code = 404;
  95              $message = $e->getMessage();
  96          } catch ( UploadStashZeroLengthFileException $e ) {
  97              $code = 500;
  98              $message = $e->getMessage();
  99          } catch ( UploadStashBadPathException $e ) {
 100              $code = 500;
 101              $message = $e->getMessage();
 102          } catch ( SpecialUploadStashTooLargeException $e ) {
 103              $code = 500;
 104              $message = 'Cannot serve a file larger than ' . self::MAX_SERVE_BYTES .
 105                  ' bytes. ' . $e->getMessage();
 106          } catch ( Exception $e ) {
 107              $code = 500;
 108              $message = $e->getMessage();
 109          }
 110  
 111          throw new HttpError( $code, $message );
 112      }
 113  
 114      /**
 115       * Parse the key passed to the SpecialPage. Returns an array containing
 116       * the associated file object, the type ('file' or 'thumb') and if
 117       * application the transform parameters
 118       *
 119       * @param string $key
 120       * @throws UploadStashBadPathException
 121       * @return array
 122       */
 123  	private function parseKey( $key ) {
 124          $type = strtok( $key, '/' );
 125  
 126          if ( $type !== 'file' && $type !== 'thumb' ) {
 127              throw new UploadStashBadPathException( "Unknown type '$type'" );
 128          }
 129          $fileName = strtok( '/' );
 130          $thumbPart = strtok( '/' );
 131          $file = $this->stash->getFile( $fileName );
 132          if ( $type === 'thumb' ) {
 133              $srcNamePos = strrpos( $thumbPart, $fileName );
 134              if ( $srcNamePos === false || $srcNamePos < 1 ) {
 135                  throw new UploadStashBadPathException( 'Unrecognized thumb name' );
 136              }
 137              $paramString = substr( $thumbPart, 0, $srcNamePos - 1 );
 138  
 139              $handler = $file->getHandler();
 140              if ( $handler ) {
 141                  $params = $handler->parseParamString( $paramString );
 142  
 143                  return array( 'file' => $file, 'type' => $type, 'params' => $params );
 144              } else {
 145                  throw new UploadStashBadPathException( 'No handler found for ' .
 146                      "mime {$file->getMimeType()} of file {$file->getPath()}" );
 147              }
 148          }
 149  
 150          return array( 'file' => $file, 'type' => $type );
 151      }
 152  
 153      /**
 154       * Get a thumbnail for file, either generated locally or remotely, and stream it out
 155       *
 156       * @param File $file
 157       * @param array $params
 158       *
 159       * @return bool Success
 160       */
 161  	private function outputThumbFromStash( $file, $params ) {
 162          $flags = 0;
 163          // this config option, if it exists, points to a "scaler", as you might find in
 164          // the Wikimedia Foundation cluster. See outputRemoteScaledThumb(). This
 165          // is part of our horrible NFS-based system, we create a file on a mount
 166          // point here, but fetch the scaled file from somewhere else that
 167          // happens to share it over NFS.
 168          if ( $this->getConfig()->get( 'UploadStashScalerBaseUrl' ) ) {
 169              $this->outputRemoteScaledThumb( $file, $params, $flags );
 170          } else {
 171              $this->outputLocallyScaledThumb( $file, $params, $flags );
 172          }
 173      }
 174  
 175      /**
 176       * Scale a file (probably with a locally installed imagemagick, or similar)
 177       * and output it to STDOUT.
 178       * @param File $file
 179       * @param array $params Scaling parameters ( e.g. array( width => '50' ) );
 180       * @param int $flags Scaling flags ( see File:: constants )
 181       * @throws MWException|UploadStashFileNotFoundException
 182       * @return bool Success
 183       */
 184  	private function outputLocallyScaledThumb( $file, $params, $flags ) {
 185          // n.b. this is stupid, we insist on re-transforming the file every time we are invoked. We rely
 186          // on HTTP caching to ensure this doesn't happen.
 187  
 188          $flags |= File::RENDER_NOW;
 189  
 190          $thumbnailImage = $file->transform( $params, $flags );
 191          if ( !$thumbnailImage ) {
 192              throw new MWException( 'Could not obtain thumbnail' );
 193          }
 194  
 195          // we should have just generated it locally
 196          if ( !$thumbnailImage->getStoragePath() ) {
 197              throw new UploadStashFileNotFoundException( "no local path for scaled item" );
 198          }
 199  
 200          // now we should construct a File, so we can get MIME and other such info in a standard way
 201          // n.b. MIME type may be different from original (ogx original -> jpeg thumb)
 202          $thumbFile = new UnregisteredLocalFile( false,
 203              $this->stash->repo, $thumbnailImage->getStoragePath(), false );
 204          if ( !$thumbFile ) {
 205              throw new UploadStashFileNotFoundException( "couldn't create local file object for thumbnail" );
 206          }
 207  
 208          return $this->outputLocalFile( $thumbFile );
 209      }
 210  
 211      /**
 212       * Scale a file with a remote "scaler", as exists on the Wikimedia Foundation
 213       * cluster, and output it to STDOUT.
 214       * Note: Unlike the usual thumbnail process, the web client never sees the
 215       * cluster URL; we do the whole HTTP transaction to the scaler ourselves
 216       * and cat the results out.
 217       * Note: We rely on NFS to have propagated the file contents to the scaler.
 218       * However, we do not rely on the thumbnail being created in NFS and then
 219       * propagated back to our filesystem. Instead we take the results of the
 220       * HTTP request instead.
 221       * Note: No caching is being done here, although we are instructing the
 222       * client to cache it forever.
 223       *
 224       * @param File $file
 225       * @param array $params Scaling parameters ( e.g. array( width => '50' ) );
 226       * @param int $flags Scaling flags ( see File:: constants )
 227       * @throws MWException
 228       * @return bool Success
 229       */
 230  	private function outputRemoteScaledThumb( $file, $params, $flags ) {
 231          // This option probably looks something like
 232          // 'http://upload.wikimedia.org/wikipedia/test/thumb/temp'. Do not use
 233          // trailing slash.
 234          $scalerBaseUrl = $this->getConfig()->get( 'UploadStashScalerBaseUrl' );
 235  
 236          if ( preg_match( '/^\/\//', $scalerBaseUrl ) ) {
 237              // this is apparently a protocol-relative URL, which makes no sense in this context,
 238              // since this is used for communication that's internal to the application.
 239              // default to http.
 240              $scalerBaseUrl = wfExpandUrl( $scalerBaseUrl, PROTO_CANONICAL );
 241          }
 242  
 243          // We need to use generateThumbName() instead of thumbName(), because
 244          // the suffix needs to match the file name for the remote thumbnailer
 245          // to work
 246          $scalerThumbName = $file->generateThumbName( $file->getName(), $params );
 247          $scalerThumbUrl = $scalerBaseUrl . '/' . $file->getUrlRel() .
 248              '/' . rawurlencode( $scalerThumbName );
 249  
 250          // make a curl call to the scaler to create a thumbnail
 251          $httpOptions = array(
 252              'method' => 'GET',
 253              'timeout' => 'default'
 254          );
 255          $req = MWHttpRequest::factory( $scalerThumbUrl, $httpOptions );
 256          $status = $req->execute();
 257          if ( !$status->isOK() ) {
 258              $errors = $status->getErrorsArray();
 259              $errorStr = "Fetching thumbnail failed: " . print_r( $errors, 1 );
 260              $errorStr .= "\nurl = $scalerThumbUrl\n";
 261              throw new MWException( $errorStr );
 262          }
 263          $contentType = $req->getResponseHeader( "content-type" );
 264          if ( !$contentType ) {
 265              throw new MWException( "Missing content-type header" );
 266          }
 267  
 268          return $this->outputContents( $req->getContent(), $contentType );
 269      }
 270  
 271      /**
 272       * Output HTTP response for file
 273       * Side effect: writes HTTP response to STDOUT.
 274       *
 275       * @param File $file File object with a local path (e.g. UnregisteredLocalFile,
 276       *   LocalFile. Oddly these don't share an ancestor!)
 277       * @throws SpecialUploadStashTooLargeException
 278       * @return bool
 279       */
 280  	private function outputLocalFile( File $file ) {
 281          if ( $file->getSize() > self::MAX_SERVE_BYTES ) {
 282              throw new SpecialUploadStashTooLargeException();
 283          }
 284  
 285          return $file->getRepo()->streamFile( $file->getPath(),
 286              array( 'Content-Transfer-Encoding: binary',
 287                  'Expires: Sun, 17-Jan-2038 19:14:07 GMT' )
 288          );
 289      }
 290  
 291      /**
 292       * Output HTTP response of raw content
 293       * Side effect: writes HTTP response to STDOUT.
 294       * @param string $content Content
 295       * @param string $contentType MIME type
 296       * @throws SpecialUploadStashTooLargeException
 297       * @return bool
 298       */
 299  	private function outputContents( $content, $contentType ) {
 300          $size = strlen( $content );
 301          if ( $size > self::MAX_SERVE_BYTES ) {
 302              throw new SpecialUploadStashTooLargeException();
 303          }
 304          self::outputFileHeaders( $contentType, $size );
 305          print $content;
 306  
 307          return true;
 308      }
 309  
 310      /**
 311       * Output headers for streaming
 312       * @todo Unsure about encoding as binary; if we received from HTTP perhaps
 313       * we should use that encoding, concatenated with semicolon to `$contentType` as it
 314       * usually is.
 315       * Side effect: preps PHP to write headers to STDOUT.
 316       * @param string $contentType String suitable for content-type header
 317       * @param string $size Length in bytes
 318       */
 319  	private static function outputFileHeaders( $contentType, $size ) {
 320          header( "Content-Type: $contentType", true );
 321          header( 'Content-Transfer-Encoding: binary', true );
 322          header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true );
 323          // Bug 53032 - It shouldn't be a problem here, but let's be safe and not cache
 324          header( 'Cache-Control: private' );
 325          header( "Content-Length: $size", true );
 326      }
 327  
 328      /**
 329       * Static callback for the HTMLForm in showUploads, to process
 330       * Note the stash has to be recreated since this is being called in a static context.
 331       * This works, because there really is only one stash per logged-in user, despite appearances.
 332       *
 333       * @param array $formData
 334       * @return Status
 335       */
 336  	public static function tryClearStashedUploads( $formData ) {
 337          if ( isset( $formData['Clear'] ) ) {
 338              $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash();
 339              wfDebug( 'stash has: ' . print_r( $stash->listFiles(), true ) . "\n" );
 340  
 341              if ( !$stash->clear() ) {
 342                  return Status::newFatal( 'uploadstash-errclear' );
 343              }
 344          }
 345  
 346          return Status::newGood();
 347      }
 348  
 349      /**
 350       * Default action when we don't have a subpage -- just show links to the uploads we have,
 351       * Also show a button to clear stashed files
 352       * @return bool
 353       */
 354  	private function showUploads() {
 355          // sets the title, etc.
 356          $this->setHeaders();
 357          $this->outputHeader();
 358  
 359          // create the form, which will also be used to execute a callback to process incoming form data
 360          // this design is extremely dubious, but supposedly HTMLForm is our standard now?
 361  
 362          $context = new DerivativeContext( $this->getContext() );
 363          $context->setTitle( $this->getPageTitle() ); // Remove subpage
 364          $form = new HTMLForm( array(
 365              'Clear' => array(
 366                  'type' => 'hidden',
 367                  'default' => true,
 368                  'name' => 'clear',
 369              )
 370          ), $context, 'clearStashedUploads' );
 371          $form->setSubmitCallback( array( __CLASS__, 'tryClearStashedUploads' ) );
 372          $form->setSubmitTextMsg( 'uploadstash-clear' );
 373  
 374          $form->prepareForm();
 375          $formResult = $form->tryAuthorizedSubmit();
 376  
 377          // show the files + form, if there are any, or just say there are none
 378          $refreshHtml = Html::element( 'a',
 379              array( 'href' => $this->getPageTitle()->getLocalURL() ),
 380              $this->msg( 'uploadstash-refresh' )->text() );
 381          $files = $this->stash->listFiles();
 382          if ( $files && count( $files ) ) {
 383              sort( $files );
 384              $fileListItemsHtml = '';
 385              foreach ( $files as $file ) {
 386                  // TODO: Use Linker::link or even construct the list in plain wikitext
 387                  $fileListItemsHtml .= Html::rawElement( 'li', array(),
 388                      Html::element( 'a', array( 'href' =>
 389                          $this->getPageTitle( "file/$file" )->getLocalURL() ), $file )
 390                  );
 391              }
 392              $this->getOutput()->addHtml( Html::rawElement( 'ul', array(), $fileListItemsHtml ) );
 393              $form->displayForm( $formResult );
 394              $this->getOutput()->addHtml( Html::rawElement( 'p', array(), $refreshHtml ) );
 395          } else {
 396              $this->getOutput()->addHtml( Html::rawElement( 'p', array(),
 397                  Html::element( 'span', array(), $this->msg( 'uploadstash-nofiles' )->text() )
 398                  . ' '
 399                  . $refreshHtml
 400              ) );
 401          }
 402  
 403          return true;
 404      }
 405  }
 406  
 407  class SpecialUploadStashTooLargeException extends MWException {
 408  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1