MediaWiki  master
SpecialUploadStash.php
Go to the documentation of this file.
1 <?php
26 use Wikimedia\RequestTimeout\TimeoutException;
27 
43  private $stash;
44 
46  private $localRepo;
47 
49  private $httpRequestFactory;
50 
52  private $urlUtils;
53 
64  private const MAX_SERVE_BYTES = 1048576; // 1 MiB
65 
71  public function __construct(
72  RepoGroup $repoGroup,
73  HttpRequestFactory $httpRequestFactory,
74  UrlUtils $urlUtils
75  ) {
76  parent::__construct( 'UploadStash', 'upload' );
77  $this->localRepo = $repoGroup->getLocalRepo();
78  $this->httpRequestFactory = $httpRequestFactory;
79  $this->urlUtils = $urlUtils;
80  }
81 
82  public function doesWrites() {
83  return true;
84  }
85 
92  public function execute( $subPage ) {
94 
95  // This is not set in constructor, because the context with the user is not safe to be set
96  $this->stash = $this->localRepo->getUploadStash( $this->getUser() );
97  $this->checkPermissions();
98 
99  if ( $subPage === null || $subPage === '' ) {
100  $this->showUploads();
101  } else {
102  $this->showUpload( $subPage );
103  }
104  }
105 
113  public function showUpload( $key ) {
114  // prevent callers from doing standard HTML output -- we'll take it from here
115  $this->getOutput()->disable();
116 
117  try {
118  $params = $this->parseKey( $key );
119  if ( $params['type'] === 'thumb' ) {
120  $this->outputThumbFromStash( $params['file'], $params['params'] );
121  } else {
122  $this->outputLocalFile( $params['file'] );
123  }
124  return;
125  } catch ( UploadStashFileNotFoundException $e ) {
126  $code = 404;
127  $message = $e->getMessage();
128  } catch ( Exception $e ) {
129  $code = 500;
130  $message = $e->getMessage();
131  }
132 
133  throw new HttpError( $code, $message );
134  }
135 
145  private function parseKey( $key ) {
146  $type = strtok( $key, '/' );
147 
148  if ( $type !== 'file' && $type !== 'thumb' ) {
149  throw new UploadStashBadPathException(
150  $this->msg( 'uploadstash-bad-path-unknown-type', $type )
151  );
152  }
153  $fileName = strtok( '/' );
154  $thumbPart = strtok( '/' );
155  $file = $this->stash->getFile( $fileName );
156  if ( $type === 'thumb' ) {
157  $srcNamePos = strrpos( $thumbPart, $fileName );
158  if ( $srcNamePos === false || $srcNamePos < 1 ) {
159  throw new UploadStashBadPathException(
160  $this->msg( 'uploadstash-bad-path-unrecognized-thumb-name' )
161  );
162  }
163  $paramString = substr( $thumbPart, 0, $srcNamePos - 1 );
164 
165  $handler = $file->getHandler();
166  if ( $handler ) {
167  $params = $handler->parseParamString( $paramString );
168 
169  return [ 'file' => $file, 'type' => $type, 'params' => $params ];
170  } else {
171  throw new UploadStashBadPathException(
172  $this->msg( 'uploadstash-bad-path-no-handler', $file->getMimeType(), $file->getPath() )
173  );
174  }
175  }
176 
177  return [ 'file' => $file, 'type' => $type ];
178  }
179 
186  private function outputThumbFromStash( $file, $params ) {
187  $flags = 0;
188  // this config option, if it exists, points to a "scaler", as you might find in
189  // the Wikimedia Foundation cluster. See outputRemoteScaledThumb(). This
190  // is part of our horrible NFS-based system, we create a file on a mount
191  // point here, but fetch the scaled file from somewhere else that
192  // happens to share it over NFS.
193  if ( $file->getRepo()->getThumbProxyUrl()
194  || $this->getConfig()->get( MainConfigNames::UploadStashScalerBaseUrl )
195  ) {
196  $this->outputRemoteScaledThumb( $file, $params, $flags );
197  } else {
198  $this->outputLocallyScaledThumb( $file, $params, $flags );
199  }
200  }
201 
210  private function outputLocallyScaledThumb( $file, $params, $flags ) {
211  // n.b. this is stupid, we insist on re-transforming the file every time we are invoked. We rely
212  // on HTTP caching to ensure this doesn't happen.
213 
214  $flags |= File::RENDER_NOW;
215 
216  $thumbnailImage = $file->transform( $params, $flags );
217  if ( !$thumbnailImage ) {
219  $this->msg( 'uploadstash-file-not-found-no-thumb' )
220  );
221  }
222 
223  // we should have just generated it locally
224  if ( !$thumbnailImage->getStoragePath() ) {
226  $this->msg( 'uploadstash-file-not-found-no-local-path' )
227  );
228  }
229 
230  // now we should construct a File, so we can get MIME and other such info in a standard way
231  // n.b. MIME type may be different from original (ogx original -> jpeg thumb)
232  $thumbFile = new UnregisteredLocalFile( false,
233  $this->stash->repo, $thumbnailImage->getStoragePath(), false );
234 
235  $this->outputLocalFile( $thumbFile );
236  }
237 
256  private function outputRemoteScaledThumb( $file, $params, $flags ) {
257  // We need to use generateThumbName() instead of thumbName(), because
258  // the suffix needs to match the file name for the remote thumbnailer
259  // to work
260  $scalerThumbName = $file->generateThumbName( $file->getName(), $params );
261 
262  // If a thumb proxy is set up for the repo, we favor that, as that will
263  // keep the request internal
264  $thumbProxyUrl = $file->getRepo()->getThumbProxyUrl();
265  if ( strlen( $thumbProxyUrl ) ) {
266  $scalerThumbUrl = $thumbProxyUrl . 'temp/' . $file->getUrlRel() .
267  '/' . rawurlencode( $scalerThumbName );
268  $secret = $file->getRepo()->getThumbProxySecret();
269  } else {
270  // This option probably looks something like
271  // '//upload.wikimedia.org/wikipedia/test/thumb/temp'. Do not use
272  // trailing slash.
273  $scalerBaseUrl = $this->getConfig()->get( MainConfigNames::UploadStashScalerBaseUrl );
274 
275  if ( preg_match( '/^\/\//', $scalerBaseUrl ) ) {
276  // this is apparently a protocol-relative URL, which makes no sense in this context,
277  // since this is used for communication that's internal to the application.
278  // default to http.
279  $scalerBaseUrl = $this->urlUtils->expand( $scalerBaseUrl, PROTO_CANONICAL );
280  }
281 
282  $scalerThumbUrl = $scalerBaseUrl . '/' . $file->getUrlRel() .
283  '/' . rawurlencode( $scalerThumbName );
284  $secret = false;
285  }
286 
287  // make an http request based on wgUploadStashScalerBaseUrl to lazy-create
288  // a thumbnail
289  $httpOptions = [
290  'method' => 'GET',
291  'timeout' => 5 // T90599 attempt to time out cleanly
292  ];
293  $req = $this->httpRequestFactory->create( $scalerThumbUrl, $httpOptions, __METHOD__ );
294 
295  // Pass a secret key shared with the proxied service if any
296  if ( strlen( $secret ) ) {
297  $req->setHeader( 'X-Swift-Secret', $secret );
298  }
299 
300  $status = $req->execute();
301  if ( !$status->isOK() ) {
302  $errors = $status->getErrorsArray();
304  $this->msg(
305  'uploadstash-file-not-found-no-remote-thumb',
306  print_r( $errors, 1 ),
307  $scalerThumbUrl
308  )
309  );
310  }
311  $contentType = $req->getResponseHeader( "content-type" );
312  if ( !$contentType ) {
314  $this->msg( 'uploadstash-file-not-found-missing-content-type' )
315  );
316  }
317 
318  $this->outputContents( $req->getContent(), $contentType );
319  }
320 
329  private function outputLocalFile( File $file ) {
330  if ( $file->getSize() > self::MAX_SERVE_BYTES ) {
332  $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
333  );
334  }
335 
336  $file->getRepo()->streamFileWithStatus( $file->getPath(),
337  [ 'Content-Transfer-Encoding: binary',
338  'Expires: Sun, 17-Jan-2038 19:14:07 GMT' ]
339  );
340  }
341 
349  private function outputContents( $content, $contentType ) {
350  $size = strlen( $content );
351  if ( $size > self::MAX_SERVE_BYTES ) {
353  $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
354  );
355  }
356  // Cancel output buffering and gzipping if set
358  self::outputFileHeaders( $contentType, $size );
359  print $content;
360  }
361 
371  private static function outputFileHeaders( $contentType, $size ) {
372  header( "Content-Type: $contentType", true );
373  header( 'Content-Transfer-Encoding: binary', true );
374  header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true );
375  // T55032 - It shouldn't be a problem here, but let's be safe and not cache
376  header( 'Cache-Control: private' );
377  header( "Content-Length: $size", true );
378  }
379 
384  private function showUploads() {
385  // sets the title, etc.
386  $this->setHeaders();
387  $this->outputHeader();
388 
389  // create the form, which will also be used to execute a callback to process incoming form data
390  // this design is extremely dubious, but supposedly HTMLForm is our standard now?
391 
392  $form = HTMLForm::factory( 'ooui', [
393  'Clear' => [
394  'type' => 'hidden',
395  'default' => true,
396  'name' => 'clear',
397  ]
398  ], $this->getContext(), 'clearStashedUploads' );
399  $form->setTitle( $this->getPageTitle() ); // Remove subpage
400  $form->setSubmitDestructive();
401  $form->setSubmitCallback( function ( $formData, $form ) {
402  if ( isset( $formData['Clear'] ) ) {
403  wfDebug( 'stash has: ' . print_r( $this->stash->listFiles(), true ) );
404 
405  if ( !$this->stash->clear() ) {
406  return Status::newFatal( 'uploadstash-errclear' );
407  }
408  }
409 
410  return Status::newGood();
411  } );
412  $form->setSubmitTextMsg( 'uploadstash-clear' );
413 
414  $form->prepareForm();
415  $formResult = $form->tryAuthorizedSubmit();
416 
417  // show the files + form, if there are any, or just say there are none
418  $linkRenderer = $this->getLinkRenderer();
419  $refreshHtml = $linkRenderer->makeKnownLink(
420  $this->getPageTitle(),
421  $this->msg( 'uploadstash-refresh' )->text()
422  );
423  $files = $this->stash->listFiles();
424  if ( $files && count( $files ) ) {
425  sort( $files );
426  $fileListItemsHtml = '';
427  foreach ( $files as $file ) {
428  $itemHtml = $linkRenderer->makeKnownLink(
429  $this->getPageTitle( "file/$file" ),
430  $file
431  );
432  try {
433  $fileObj = $this->stash->getFile( $file );
434  $thumb = $fileObj->generateThumbName( $file, [ 'width' => 220 ] );
435  $itemHtml .=
436  $this->msg( 'word-separator' )->escaped() .
437  $this->msg( 'parentheses' )->rawParams(
438  $linkRenderer->makeKnownLink(
439  $this->getPageTitle( "thumb/$file/$thumb" ),
440  $this->msg( 'uploadstash-thumbnail' )->text()
441  )
442  )->escaped();
443  } catch ( TimeoutException $e ) {
444  throw $e;
445  } catch ( Exception $e ) {
447  }
448  $fileListItemsHtml .= Html::rawElement( 'li', [], $itemHtml );
449  }
450  $this->getOutput()->addHTML( Html::rawElement( 'ul', [], $fileListItemsHtml ) );
451  $form->displayForm( $formResult );
452  $this->getOutput()->addHTML( Html::rawElement( 'p', [], $refreshHtml ) );
453  } else {
454  $this->getOutput()->addHTML( Html::rawElement( 'p', [],
455  Html::element( 'span', [], $this->msg( 'uploadstash-nofiles' )->text() )
456  . ' '
457  . $refreshHtml
458  ) );
459  }
460  }
461 }
const PROTO_CANONICAL
Definition: Defines.php:199
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
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:67
const RENDER_NOW
Force rendering in the current process.
Definition: File.php:77
static factory( $displayFormat, $descriptor, IContextSource $context, $messagePrefix='')
Construct a HTMLForm object for given display type.
Definition: HTMLForm.php:349
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.
A service to expand, parse, and otherwise manipulate URLs.
Definition: UrlUtils.php:17
Prioritized list of file repositories.
Definition: RepoGroup.php:29
getLocalRepo()
Get the local repository, i.e.
Definition: RepoGroup.php:342
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.
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.
__construct(RepoGroup $repoGroup, HttpRequestFactory $httpRequestFactory, UrlUtils $urlUtils)
showUpload( $key)
If file available in stash, cats it out to the client as a simple HTTP response.
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.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:73
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:85
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