MediaWiki  master
SpecialUploadStash.php
Go to the documentation of this file.
1 <?php
37  // UploadStash
38  private $stash;
39 
50  const MAX_SERVE_BYTES = 1048576; // 1MB
51 
52  public function __construct() {
53  parent::__construct( 'UploadStash', 'upload' );
54  }
55 
56  public function doesWrites() {
57  return true;
58  }
59 
66  public function execute( $subPage ) {
68 
69  $this->stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash( $this->getUser() );
70  $this->checkPermissions();
71 
72  if ( $subPage === null || $subPage === '' ) {
73  $this->showUploads();
74  } else {
75  $this->showUpload( $subPage );
76  }
77  }
78 
86  public function showUpload( $key ) {
87  // prevent callers from doing standard HTML output -- we'll take it from here
88  $this->getOutput()->disable();
89 
90  try {
91  $params = $this->parseKey( $key );
92  if ( $params['type'] === 'thumb' ) {
93  $this->outputThumbFromStash( $params['file'], $params['params'] );
94  } else {
95  $this->outputLocalFile( $params['file'] );
96  }
97  return;
98  } catch ( UploadStashFileNotFoundException $e ) {
99  $code = 404;
100  $message = $e->getMessage();
101  } catch ( UploadStashZeroLengthFileException $e ) {
102  $code = 500;
103  $message = $e->getMessage();
104  } catch ( UploadStashBadPathException $e ) {
105  $code = 500;
106  $message = $e->getMessage();
107  } catch ( SpecialUploadStashTooLargeException $e ) {
108  $code = 500;
109  $message = $e->getMessage();
110  } catch ( Exception $e ) {
111  $code = 500;
112  $message = $e->getMessage();
113  }
114 
115  throw new HttpError( $code, $message );
116  }
117 
127  private function parseKey( $key ) {
128  $type = strtok( $key, '/' );
129 
130  if ( $type !== 'file' && $type !== 'thumb' ) {
131  throw new UploadStashBadPathException(
132  $this->msg( 'uploadstash-bad-path-unknown-type', $type )
133  );
134  }
135  $fileName = strtok( '/' );
136  $thumbPart = strtok( '/' );
137  $file = $this->stash->getFile( $fileName );
138  if ( $type === 'thumb' ) {
139  $srcNamePos = strrpos( $thumbPart, $fileName );
140  if ( $srcNamePos === false || $srcNamePos < 1 ) {
141  throw new UploadStashBadPathException(
142  $this->msg( 'uploadstash-bad-path-unrecognized-thumb-name' )
143  );
144  }
145  $paramString = substr( $thumbPart, 0, $srcNamePos - 1 );
146 
147  $handler = $file->getHandler();
148  if ( $handler ) {
149  $params = $handler->parseParamString( $paramString );
150 
151  return [ 'file' => $file, 'type' => $type, 'params' => $params ];
152  } else {
153  throw new UploadStashBadPathException(
154  $this->msg( 'uploadstash-bad-path-no-handler', $file->getMimeType(), $file->getPath() )
155  );
156  }
157  }
158 
159  return [ 'file' => $file, 'type' => $type ];
160  }
161 
168  private function outputThumbFromStash( $file, $params ) {
169  $flags = 0;
170  // this config option, if it exists, points to a "scaler", as you might find in
171  // the Wikimedia Foundation cluster. See outputRemoteScaledThumb(). This
172  // is part of our horrible NFS-based system, we create a file on a mount
173  // point here, but fetch the scaled file from somewhere else that
174  // happens to share it over NFS.
175  if ( $this->getConfig()->get( 'UploadStashScalerBaseUrl' ) ) {
176  $this->outputRemoteScaledThumb( $file, $params, $flags );
177  } else {
178  $this->outputLocallyScaledThumb( $file, $params, $flags );
179  }
180  }
181 
190  private function outputLocallyScaledThumb( $file, $params, $flags ) {
191  // n.b. this is stupid, we insist on re-transforming the file every time we are invoked. We rely
192  // on HTTP caching to ensure this doesn't happen.
193 
194  $flags |= File::RENDER_NOW;
195 
196  $thumbnailImage = $file->transform( $params, $flags );
197  if ( !$thumbnailImage ) {
199  $this->msg( 'uploadstash-file-not-found-no-thumb' )
200  );
201  }
202 
203  // we should have just generated it locally
204  if ( !$thumbnailImage->getStoragePath() ) {
206  $this->msg( 'uploadstash-file-not-found-no-local-path' )
207  );
208  }
209 
210  // now we should construct a File, so we can get MIME and other such info in a standard way
211  // n.b. MIME type may be different from original (ogx original -> jpeg thumb)
212  $thumbFile = new UnregisteredLocalFile( false,
213  $this->stash->repo, $thumbnailImage->getStoragePath(), false );
214  if ( !$thumbFile ) {
216  $this->msg( 'uploadstash-file-not-found-no-object' )
217  );
218  }
219 
220  $this->outputLocalFile( $thumbFile );
221  }
222 
241  private function outputRemoteScaledThumb( $file, $params, $flags ) {
242  // This option probably looks something like
243  // '//upload.wikimedia.org/wikipedia/test/thumb/temp'. Do not use
244  // trailing slash.
245  $scalerBaseUrl = $this->getConfig()->get( 'UploadStashScalerBaseUrl' );
246 
247  if ( preg_match( '/^\/\//', $scalerBaseUrl ) ) {
248  // this is apparently a protocol-relative URL, which makes no sense in this context,
249  // since this is used for communication that's internal to the application.
250  // default to http.
251  $scalerBaseUrl = wfExpandUrl( $scalerBaseUrl, PROTO_CANONICAL );
252  }
253 
254  // We need to use generateThumbName() instead of thumbName(), because
255  // the suffix needs to match the file name for the remote thumbnailer
256  // to work
257  $scalerThumbName = $file->generateThumbName( $file->getName(), $params );
258  $scalerThumbUrl = $scalerBaseUrl . '/' . $file->getUrlRel() .
259  '/' . rawurlencode( $scalerThumbName );
260 
261  // If a thumb proxy is set up for the repo, we favor that, as that will
262  // keep the request internal
263  $thumbProxyUrl = $file->getRepo()->getThumbProxyUrl();
264 
265  if ( strlen( $thumbProxyUrl ) ) {
266  $scalerThumbUrl = $thumbProxyUrl . 'temp/' . $file->getUrlRel() .
267  '/' . rawurlencode( $scalerThumbName );
268  }
269 
270  // make an http request based on wgUploadStashScalerBaseUrl to lazy-create
271  // a thumbnail
272  $httpOptions = [
273  'method' => 'GET',
274  'timeout' => 5 // T90599 attempt to time out cleanly
275  ];
276  $req = MWHttpRequest::factory( $scalerThumbUrl, $httpOptions, __METHOD__ );
277 
278  $secret = $file->getRepo()->getThumbProxySecret();
279 
280  // Pass a secret key shared with the proxied service if any
281  if ( strlen( $secret ) ) {
282  $req->setHeader( 'X-Swift-Secret', $secret );
283  }
284 
285  $status = $req->execute();
286  if ( !$status->isOK() ) {
287  $errors = $status->getErrorsArray();
289  $this->msg(
290  'uploadstash-file-not-found-no-remote-thumb',
291  print_r( $errors, 1 ),
292  $scalerThumbUrl
293  )
294  );
295  }
296  $contentType = $req->getResponseHeader( "content-type" );
297  if ( !$contentType ) {
299  $this->msg( 'uploadstash-file-not-found-missing-content-type' )
300  );
301  }
302 
303  $this->outputContents( $req->getContent(), $contentType );
304  }
305 
314  private function outputLocalFile( File $file ) {
315  if ( $file->getSize() > self::MAX_SERVE_BYTES ) {
317  $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
318  );
319  }
320 
321  $file->getRepo()->streamFileWithStatus( $file->getPath(),
322  [ 'Content-Transfer-Encoding: binary',
323  'Expires: Sun, 17-Jan-2038 19:14:07 GMT' ]
324  );
325  }
326 
334  private function outputContents( $content, $contentType ) {
335  $size = strlen( $content );
336  if ( $size > self::MAX_SERVE_BYTES ) {
338  $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
339  );
340  }
341  // Cancel output buffering and gzipping if set
343  self::outputFileHeaders( $contentType, $size );
344  print $content;
345  }
346 
356  private static function outputFileHeaders( $contentType, $size ) {
357  header( "Content-Type: $contentType", true );
358  header( 'Content-Transfer-Encoding: binary', true );
359  header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true );
360  // T55032 - It shouldn't be a problem here, but let's be safe and not cache
361  header( 'Cache-Control: private' );
362  header( "Content-Length: $size", true );
363  }
364 
374  public static function tryClearStashedUploads( $formData, $form ) {
375  if ( isset( $formData['Clear'] ) ) {
376  $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash( $form->getUser() );
377  wfDebug( 'stash has: ' . print_r( $stash->listFiles(), true ) . "\n" );
378 
379  if ( !$stash->clear() ) {
380  return Status::newFatal( 'uploadstash-errclear' );
381  }
382  }
383 
384  return Status::newGood();
385  }
386 
391  private function showUploads() {
392  // sets the title, etc.
393  $this->setHeaders();
394  $this->outputHeader();
395 
396  // create the form, which will also be used to execute a callback to process incoming form data
397  // this design is extremely dubious, but supposedly HTMLForm is our standard now?
398 
399  $context = new DerivativeContext( $this->getContext() );
400  $context->setTitle( $this->getPageTitle() ); // Remove subpage
401  $form = HTMLForm::factory( 'ooui', [
402  'Clear' => [
403  'type' => 'hidden',
404  'default' => true,
405  'name' => 'clear',
406  ]
407  ], $context, 'clearStashedUploads' );
408  $form->setSubmitDestructive();
409  $form->setSubmitCallback( [ __CLASS__, 'tryClearStashedUploads' ] );
410  $form->setSubmitTextMsg( 'uploadstash-clear' );
411 
412  $form->prepareForm();
413  $formResult = $form->tryAuthorizedSubmit();
414 
415  // show the files + form, if there are any, or just say there are none
416  $refreshHtml = Html::element( 'a',
417  [ 'href' => $this->getPageTitle()->getLocalURL() ],
418  $this->msg( 'uploadstash-refresh' )->text() );
419  $files = $this->stash->listFiles();
420  if ( $files && count( $files ) ) {
421  sort( $files );
422  $fileListItemsHtml = '';
423  $linkRenderer = $this->getLinkRenderer();
424  foreach ( $files as $file ) {
425  $itemHtml = $linkRenderer->makeKnownLink(
426  $this->getPageTitle( "file/$file" ),
427  $file
428  );
429  try {
430  $fileObj = $this->stash->getFile( $file );
431  $thumb = $fileObj->generateThumbName( $file, [ 'width' => 220 ] );
432  $itemHtml .=
433  $this->msg( 'word-separator' )->escaped() .
434  $this->msg( 'parentheses' )->rawParams(
435  $linkRenderer->makeKnownLink(
436  $this->getPageTitle( "thumb/$file/$thumb" ),
437  $this->msg( 'uploadstash-thumbnail' )->text()
438  )
439  )->escaped();
440  } catch ( Exception $e ) {
441  }
442  $fileListItemsHtml .= Html::rawElement( 'li', [], $itemHtml );
443  }
444  $this->getOutput()->addHTML( Html::rawElement( 'ul', [], $fileListItemsHtml ) );
445  $form->displayForm( $formResult );
446  $this->getOutput()->addHTML( Html::rawElement( 'p', [], $refreshHtml ) );
447  } else {
448  $this->getOutput()->addHTML( Html::rawElement( 'p', [],
449  Html::element( 'span', [], $this->msg( 'uploadstash-nofiles' )->text() )
450  . ' '
451  . $refreshHtml
452  ) );
453  }
454  }
455 }
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:69
outputThumbFromStash( $file, $params)
Get a thumbnail for file, either generated locally or remotely, and stream it out.
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
static factory( $url, array $options=null, $caller=__METHOD__)
Generate a new request object.
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:231
Shortcut to construct a special page which is unlisted by default.
$context
Definition: load.php:45
getContext()
Gets the context this SpecialPage is executed in.
static tryClearStashedUploads( $formData, $form)
Static callback for the HTMLForm in showUploads, to process Note the stash has to be recreated since ...
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
outputLocallyScaledThumb( $file, $params, $flags)
Scale a file (probably with a locally installed imagemagick, or similar) and output it to STDOUT...
outputRemoteScaledThumb( $file, $params, $flags)
Scale a file with a remote "scaler", as exists on the Wikimedia Foundation cluster, and output it to STDOUT.
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
An IContextSource implementation which will inherit context from another source but allow individual ...
getRepo()
Returns the repository.
Definition: File.php:1879
getOutput()
Get the OutputPage being used for this instance.
wfResetOutputBuffers( $resetGzipEncoding=true)
Clear away any user-level output buffers, discarding contents.
A file object referring to either a standalone local file, or a file in a local repository with no da...
execute( $subPage)
Execute page – can output a file directly or show a listing of them.
Web access for files temporarily stored by UploadStash.
Show an error that looks like an HTTP server error.
Definition: HttpError.php:30
getPath()
Return the storage path to the file.
Definition: File.php:427
const RENDER_NOW
Force rendering in the current process.
Definition: File.php:69
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
static factory( $displayFormat,... $arguments)
Construct a HTMLForm object for given display type.
Definition: HTMLForm.php:307
getSize()
Return the size of the image file, in bytes Overridden by LocalFile, UnregisteredLocalFile STUB...
Definition: File.php:720
static singleton()
Definition: RepoGroup.php:60
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes! ...
outputLocalFile(File $file)
Output HTTP response for file Side effect: writes HTTP response to STDOUT.
showUploads()
Default action when we don&#39;t have a subpage – just show links to the uploads we have, Also show a button to clear stashed files.
showUpload( $key)
If file available in stash, cats it out to the client as a simple HTTP response.
outputContents( $content, $contentType)
Output HTTP response of raw content Side effect: writes HTTP response to STDOUT.
parseKey( $key)
Parse the key passed to the SpecialPage.
const PROTO_CANONICAL
Definition: Defines.php:203
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
getUser()
Shortcut to get the User executing this instance.
getConfig()
Shortcut to get main config object.
static outputFileHeaders( $contentType, $size)
Output headers for streaming.
$content
Definition: router.php:78
checkPermissions()
Checks if userCanExecute, and if not throws a PermissionsError.
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition: File.php:61
const MAX_SERVE_BYTES
Since we are directly writing the file to STDOUT, we should not be reading in really big files and se...
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
getPageTitle( $subpage=false)
Get a self-referential title object.
return true
Definition: router.php:92
MediaWiki Linker LinkRenderer null $linkRenderer
Definition: SpecialPage.php:67