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