MediaWiki  master
SpecialUploadStash.php
Go to the documentation of this file.
1 <?php
25 use Wikimedia\RequestTimeout\TimeoutException;
26 
42  private $stash;
43 
45  private $localRepo;
46 
49 
60  private const MAX_SERVE_BYTES = 1048576; // 1 MiB
61 
66  public function __construct(
67  RepoGroup $repoGroup,
69  ) {
70  parent::__construct( 'UploadStash', 'upload' );
71  $this->localRepo = $repoGroup->getLocalRepo();
72  $this->httpRequestFactory = $httpRequestFactory;
73  }
74 
75  public function doesWrites() {
76  return true;
77  }
78 
85  public function execute( $subPage ) {
87 
88  // This is not set in constructor, because the context with the user is not safe to be set
89  $this->stash = $this->localRepo->getUploadStash( $this->getUser() );
90  $this->checkPermissions();
91 
92  if ( $subPage === null || $subPage === '' ) {
93  $this->showUploads();
94  } else {
95  $this->showUpload( $subPage );
96  }
97  }
98 
106  public function showUpload( $key ) {
107  // prevent callers from doing standard HTML output -- we'll take it from here
108  $this->getOutput()->disable();
109 
110  try {
111  $params = $this->parseKey( $key );
112  if ( $params['type'] === 'thumb' ) {
113  $this->outputThumbFromStash( $params['file'], $params['params'] );
114  } else {
115  $this->outputLocalFile( $params['file'] );
116  }
117  return;
118  } catch ( UploadStashFileNotFoundException $e ) {
119  $code = 404;
120  $message = $e->getMessage();
121  } catch ( Exception $e ) {
122  $code = 500;
123  $message = $e->getMessage();
124  }
125 
126  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable False positive
127  throw new HttpError( $code, $message );
128  }
129 
139  private function parseKey( $key ) {
140  $type = strtok( $key, '/' );
141 
142  if ( $type !== 'file' && $type !== 'thumb' ) {
143  throw new UploadStashBadPathException(
144  $this->msg( 'uploadstash-bad-path-unknown-type', $type )
145  );
146  }
147  $fileName = strtok( '/' );
148  $thumbPart = strtok( '/' );
149  $file = $this->stash->getFile( $fileName );
150  if ( $type === 'thumb' ) {
151  $srcNamePos = strrpos( $thumbPart, $fileName );
152  if ( $srcNamePos === false || $srcNamePos < 1 ) {
153  throw new UploadStashBadPathException(
154  $this->msg( 'uploadstash-bad-path-unrecognized-thumb-name' )
155  );
156  }
157  $paramString = substr( $thumbPart, 0, $srcNamePos - 1 );
158 
159  $handler = $file->getHandler();
160  if ( $handler ) {
161  $params = $handler->parseParamString( $paramString );
162 
163  return [ 'file' => $file, 'type' => $type, 'params' => $params ];
164  } else {
165  throw new UploadStashBadPathException(
166  $this->msg( 'uploadstash-bad-path-no-handler', $file->getMimeType(), $file->getPath() )
167  );
168  }
169  }
170 
171  return [ 'file' => $file, 'type' => $type ];
172  }
173 
180  private function outputThumbFromStash( $file, $params ) {
181  $flags = 0;
182  // this config option, if it exists, points to a "scaler", as you might find in
183  // the Wikimedia Foundation cluster. See outputRemoteScaledThumb(). This
184  // is part of our horrible NFS-based system, we create a file on a mount
185  // point here, but fetch the scaled file from somewhere else that
186  // happens to share it over NFS.
187  if ( $file->getRepo()->getThumbProxyUrl()
188  || $this->getConfig()->get( MainConfigNames::UploadStashScalerBaseUrl )
189  ) {
190  $this->outputRemoteScaledThumb( $file, $params, $flags );
191  } else {
192  $this->outputLocallyScaledThumb( $file, $params, $flags );
193  }
194  }
195 
204  private function outputLocallyScaledThumb( $file, $params, $flags ) {
205  // n.b. this is stupid, we insist on re-transforming the file every time we are invoked. We rely
206  // on HTTP caching to ensure this doesn't happen.
207 
208  $flags |= File::RENDER_NOW;
209 
210  $thumbnailImage = $file->transform( $params, $flags );
211  if ( !$thumbnailImage ) {
213  $this->msg( 'uploadstash-file-not-found-no-thumb' )
214  );
215  }
216 
217  // we should have just generated it locally
218  if ( !$thumbnailImage->getStoragePath() ) {
220  $this->msg( 'uploadstash-file-not-found-no-local-path' )
221  );
222  }
223 
224  // now we should construct a File, so we can get MIME and other such info in a standard way
225  // n.b. MIME type may be different from original (ogx original -> jpeg thumb)
226  $thumbFile = new UnregisteredLocalFile( false,
227  $this->stash->repo, $thumbnailImage->getStoragePath(), false );
228 
229  $this->outputLocalFile( $thumbFile );
230  }
231 
250  private function outputRemoteScaledThumb( $file, $params, $flags ) {
251  // We need to use generateThumbName() instead of thumbName(), because
252  // the suffix needs to match the file name for the remote thumbnailer
253  // to work
254  $scalerThumbName = $file->generateThumbName( $file->getName(), $params );
255 
256  // If a thumb proxy is set up for the repo, we favor that, as that will
257  // keep the request internal
258  $thumbProxyUrl = $file->getRepo()->getThumbProxyUrl();
259  if ( strlen( $thumbProxyUrl ) ) {
260  $scalerThumbUrl = $thumbProxyUrl . 'temp/' . $file->getUrlRel() .
261  '/' . rawurlencode( $scalerThumbName );
262  $secret = $file->getRepo()->getThumbProxySecret();
263  } else {
264  // This option probably looks something like
265  // '//upload.wikimedia.org/wikipedia/test/thumb/temp'. Do not use
266  // trailing slash.
267  $scalerBaseUrl = $this->getConfig()->get( MainConfigNames::UploadStashScalerBaseUrl );
268 
269  if ( preg_match( '/^\/\//', $scalerBaseUrl ) ) {
270  // this is apparently a protocol-relative URL, which makes no sense in this context,
271  // since this is used for communication that's internal to the application.
272  // default to http.
273  $scalerBaseUrl = wfExpandUrl( $scalerBaseUrl, PROTO_CANONICAL );
274  }
275 
276  $scalerThumbUrl = $scalerBaseUrl . '/' . $file->getUrlRel() .
277  '/' . rawurlencode( $scalerThumbName );
278  $secret = false;
279  }
280 
281  // make an http request based on wgUploadStashScalerBaseUrl to lazy-create
282  // a thumbnail
283  $httpOptions = [
284  'method' => 'GET',
285  'timeout' => 5 // T90599 attempt to time out cleanly
286  ];
287  $req = $this->httpRequestFactory->create( $scalerThumbUrl, $httpOptions, __METHOD__ );
288 
289  // Pass a secret key shared with the proxied service if any
290  if ( strlen( $secret ) ) {
291  $req->setHeader( 'X-Swift-Secret', $secret );
292  }
293 
294  $status = $req->execute();
295  if ( !$status->isOK() ) {
296  $errors = $status->getErrorsArray();
298  $this->msg(
299  'uploadstash-file-not-found-no-remote-thumb',
300  print_r( $errors, 1 ),
301  $scalerThumbUrl
302  )
303  );
304  }
305  $contentType = $req->getResponseHeader( "content-type" );
306  if ( !$contentType ) {
308  $this->msg( 'uploadstash-file-not-found-missing-content-type' )
309  );
310  }
311 
312  $this->outputContents( $req->getContent(), $contentType );
313  }
314 
323  private function outputLocalFile( File $file ) {
324  if ( $file->getSize() > self::MAX_SERVE_BYTES ) {
326  $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
327  );
328  }
329 
330  $file->getRepo()->streamFileWithStatus( $file->getPath(),
331  [ 'Content-Transfer-Encoding: binary',
332  'Expires: Sun, 17-Jan-2038 19:14:07 GMT' ]
333  );
334  }
335 
343  private function outputContents( $content, $contentType ) {
344  $size = strlen( $content );
345  if ( $size > self::MAX_SERVE_BYTES ) {
347  $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
348  );
349  }
350  // Cancel output buffering and gzipping if set
352  self::outputFileHeaders( $contentType, $size );
353  print $content;
354  }
355 
365  private static function outputFileHeaders( $contentType, $size ) {
366  header( "Content-Type: $contentType", true );
367  header( 'Content-Transfer-Encoding: binary', true );
368  header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true );
369  // T55032 - It shouldn't be a problem here, but let's be safe and not cache
370  header( 'Cache-Control: private' );
371  header( "Content-Length: $size", true );
372  }
373 
378  private function showUploads() {
379  // sets the title, etc.
380  $this->setHeaders();
381  $this->outputHeader();
382 
383  // create the form, which will also be used to execute a callback to process incoming form data
384  // this design is extremely dubious, but supposedly HTMLForm is our standard now?
385 
386  $form = HTMLForm::factory( 'ooui', [
387  'Clear' => [
388  'type' => 'hidden',
389  'default' => true,
390  'name' => 'clear',
391  ]
392  ], $this->getContext(), 'clearStashedUploads' );
393  $form->setTitle( $this->getPageTitle() ); // Remove subpage
394  $form->setSubmitDestructive();
395  $form->setSubmitCallback( function ( $formData, $form ) {
396  if ( isset( $formData['Clear'] ) ) {
397  wfDebug( 'stash has: ' . print_r( $this->stash->listFiles(), true ) );
398 
399  if ( !$this->stash->clear() ) {
400  return Status::newFatal( 'uploadstash-errclear' );
401  }
402  }
403 
404  return Status::newGood();
405  } );
406  $form->setSubmitTextMsg( 'uploadstash-clear' );
407 
408  $form->prepareForm();
409  $formResult = $form->tryAuthorizedSubmit();
410 
411  // show the files + form, if there are any, or just say there are none
412  $linkRenderer = $this->getLinkRenderer();
413  $refreshHtml = $linkRenderer->makeKnownLink(
414  $this->getPageTitle(),
415  $this->msg( 'uploadstash-refresh' )->text()
416  );
417  $files = $this->stash->listFiles();
418  if ( $files && count( $files ) ) {
419  sort( $files );
420  $fileListItemsHtml = '';
421  foreach ( $files as $file ) {
422  $itemHtml = $linkRenderer->makeKnownLink(
423  $this->getPageTitle( "file/$file" ),
424  $file
425  );
426  try {
427  $fileObj = $this->stash->getFile( $file );
428  $thumb = $fileObj->generateThumbName( $file, [ 'width' => 220 ] );
429  $itemHtml .=
430  $this->msg( 'word-separator' )->escaped() .
431  $this->msg( 'parentheses' )->rawParams(
432  $linkRenderer->makeKnownLink(
433  $this->getPageTitle( "thumb/$file/$thumb" ),
434  $this->msg( 'uploadstash-thumbnail' )->text()
435  )
436  )->escaped();
437  } catch ( TimeoutException $e ) {
438  throw $e;
439  } 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 }
const PROTO_CANONICAL
Definition: Defines.php:198
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
wfResetOutputBuffers( $resetGzipEncoding=true)
Clear away any user-level output buffers, discarding contents.
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition: File.php:68
const RENDER_NOW
Force rendering in the current process.
Definition: File.php:78
static factory( $displayFormat, $descriptor, IContextSource $context, $messagePrefix='')
Construct a HTMLForm object for given display type.
Definition: HTMLForm.php:338
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:236
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:214
Show an error that looks like an HTTP server error.
Definition: HttpError.php:32
static logException(Throwable $e, $catcher=self::CAUGHT_BY_OTHER, $extraData=[])
Log a throwable to the exception log (if enabled).
Factory creating MWHttpRequest objects.
A class containing constants representing the names of configuration variables.
Prioritized list of file repositories.
Definition: RepoGroup.php:29
getLocalRepo()
Get the local repository, i.e.
Definition: RepoGroup.php:339
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getOutput()
Get the OutputPage being used for this instance.
getUser()
Shortcut to get the User executing this instance.
checkPermissions()
Checks if userCanExecute, and if not throws a PermissionsError.
getContext()
Gets the context this SpecialPage is executed in.
LinkRenderer null $linkRenderer
Definition: SpecialPage.php:81
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getConfig()
Shortcut to get main config object.
getPageTitle( $subpage=false)
Get a self-referential title object.
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
Web access for files temporarily stored by UploadStash.
static outputFileHeaders( $contentType, $size)
Output headers for streaming.
outputLocalFile(File $file)
Output HTTP response for file Side effect: writes HTTP response to STDOUT.
outputThumbFromStash( $file, $params)
Get a thumbnail for file, either generated locally or remotely, and stream it out.
__construct(RepoGroup $repoGroup, HttpRequestFactory $httpRequestFactory)
showUploads()
Default action when we don't have a subpage – just show links to the uploads we have,...
parseKey( $key)
Parse the key passed to the SpecialPage.
showUpload( $key)
If file available in stash, cats it out to the client as a simple HTTP response.
outputRemoteScaledThumb( $file, $params, $flags)
Scale a file with a remote "scaler", as exists on the Wikimedia Foundation cluster,...
UploadStash null $stash
const MAX_SERVE_BYTES
Since we are directly writing the file to STDOUT, we should not be reading in really big files and se...
execute( $subPage)
Execute page – can output a file directly or show a listing of them.
doesWrites()
Indicates whether this special page may perform database writes.
outputLocallyScaledThumb( $file, $params, $flags)
Scale a file (probably with a locally installed imagemagick, or similar) and output it to STDOUT.
HttpRequestFactory $httpRequestFactory
outputContents( $content, $contentType)
Output HTTP response of raw content Side effect: writes HTTP response to STDOUT.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
Shortcut to construct a special page which is unlisted by default.
File without associated database record.
$content
Definition: router.php:76
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42