MediaWiki  master
SpecialUploadStash.php
Go to the documentation of this file.
1 <?php
23 namespace MediaWiki\Specials;
24 
25 use Exception;
26 use File;
27 use HTMLForm;
28 use HttpError;
29 use LocalRepo;
36 use MWException;
38 use RepoGroup;
41 use UploadStash;
44 use Wikimedia\RequestTimeout\TimeoutException;
45 
61  private $stash;
62 
63  private LocalRepo $localRepo;
64  private HttpRequestFactory $httpRequestFactory;
65  private UrlUtils $urlUtils;
66 
77  private const MAX_SERVE_BYTES = 1048576; // 1 MiB
78 
84  public function __construct(
85  RepoGroup $repoGroup,
86  HttpRequestFactory $httpRequestFactory,
87  UrlUtils $urlUtils
88  ) {
89  parent::__construct( 'UploadStash', 'upload' );
90  $this->localRepo = $repoGroup->getLocalRepo();
91  $this->httpRequestFactory = $httpRequestFactory;
92  $this->urlUtils = $urlUtils;
93  }
94 
95  public function doesWrites() {
96  return true;
97  }
98 
105  public function execute( $subPage ) {
106  $this->useTransactionalTimeLimit();
107 
108  // This is not set in constructor, because the context with the user is not safe to be set
109  $this->stash = $this->localRepo->getUploadStash( $this->getUser() );
110  $this->checkPermissions();
111 
112  if ( $subPage === null || $subPage === '' ) {
113  $this->showUploads();
114  } else {
115  $this->showUpload( $subPage );
116  }
117  }
118 
126  public function showUpload( $key ) {
127  // prevent callers from doing standard HTML output -- we'll take it from here
128  $this->getOutput()->disable();
129 
130  try {
131  $params = $this->parseKey( $key );
132  if ( $params['type'] === 'thumb' ) {
133  $this->outputThumbFromStash( $params['file'], $params['params'] );
134  } else {
135  $this->outputLocalFile( $params['file'] );
136  }
137  return;
138  } catch ( UploadStashFileNotFoundException $e ) {
139  $code = 404;
140  $message = $e->getMessage();
141  } catch ( Exception $e ) {
142  $code = 500;
143  $message = $e->getMessage();
144  }
145 
146  throw new HttpError( $code, $message );
147  }
148 
158  private function parseKey( $key ) {
159  $type = strtok( $key, '/' );
160 
161  if ( $type !== 'file' && $type !== 'thumb' ) {
162  throw new UploadStashBadPathException(
163  $this->msg( 'uploadstash-bad-path-unknown-type', $type )
164  );
165  }
166  $fileName = strtok( '/' );
167  $thumbPart = strtok( '/' );
168  $file = $this->stash->getFile( $fileName );
169  if ( $type === 'thumb' ) {
170  $srcNamePos = strrpos( $thumbPart, $fileName );
171  if ( $srcNamePos === false || $srcNamePos < 1 ) {
172  throw new UploadStashBadPathException(
173  $this->msg( 'uploadstash-bad-path-unrecognized-thumb-name' )
174  );
175  }
176  $paramString = substr( $thumbPart, 0, $srcNamePos - 1 );
177 
178  $handler = $file->getHandler();
179  if ( $handler ) {
180  $params = $handler->parseParamString( $paramString );
181 
182  return [ 'file' => $file, 'type' => $type, 'params' => $params ];
183  } else {
184  throw new UploadStashBadPathException(
185  $this->msg( 'uploadstash-bad-path-no-handler', $file->getMimeType(), $file->getPath() )
186  );
187  }
188  }
189 
190  return [ 'file' => $file, 'type' => $type ];
191  }
192 
199  private function outputThumbFromStash( $file, $params ) {
200  // this config option, if it exists, points to a "scaler", as you might find in
201  // the Wikimedia Foundation cluster. See outputRemoteScaledThumb(). This
202  // is part of our horrible NFS-based system, we create a file on a mount
203  // point here, but fetch the scaled file from somewhere else that
204  // happens to share it over NFS.
205  if ( $file->getRepo()->getThumbProxyUrl()
206  || $this->getConfig()->get( MainConfigNames::UploadStashScalerBaseUrl )
207  ) {
208  $this->outputRemoteScaledThumb( $file, $params );
209  } else {
210  $this->outputLocallyScaledThumb( $file, $params );
211  }
212  }
213 
221  private function outputLocallyScaledThumb( $file, $params ) {
222  // n.b. this is stupid, we insist on re-transforming the file every time we are invoked. We rely
223  // on HTTP caching to ensure this doesn't happen.
224 
225  $thumbnailImage = $file->transform( $params, File::RENDER_NOW );
226  if ( !$thumbnailImage ) {
228  $this->msg( 'uploadstash-file-not-found-no-thumb' )
229  );
230  }
231 
232  // we should have just generated it locally
233  if ( !$thumbnailImage->getStoragePath() ) {
235  $this->msg( 'uploadstash-file-not-found-no-local-path' )
236  );
237  }
238 
239  // now we should construct a File, so we can get MIME and other such info in a standard way
240  // n.b. MIME type may be different from original (ogx original -> jpeg thumb)
241  $thumbFile = new UnregisteredLocalFile( false,
242  $this->stash->repo, $thumbnailImage->getStoragePath(), false );
243 
244  $this->outputLocalFile( $thumbFile );
245  }
246 
264  private function outputRemoteScaledThumb( $file, $params ) {
265  // We need to use generateThumbName() instead of thumbName(), because
266  // the suffix needs to match the file name for the remote thumbnailer
267  // to work
268  $scalerThumbName = $file->generateThumbName( $file->getName(), $params );
269 
270  // If a thumb proxy is set up for the repo, we favor that, as that will
271  // keep the request internal
272  $thumbProxyUrl = $file->getRepo()->getThumbProxyUrl();
273  if ( strlen( $thumbProxyUrl ) ) {
274  $scalerThumbUrl = $thumbProxyUrl . 'temp/' . $file->getUrlRel() .
275  '/' . rawurlencode( $scalerThumbName );
276  $secret = $file->getRepo()->getThumbProxySecret();
277  } else {
278  // This option probably looks something like
279  // '//upload.wikimedia.org/wikipedia/test/thumb/temp'. Do not use
280  // trailing slash.
281  $scalerBaseUrl = $this->getConfig()->get( MainConfigNames::UploadStashScalerBaseUrl );
282 
283  if ( preg_match( '/^\/\//', $scalerBaseUrl ) ) {
284  // this is apparently a protocol-relative URL, which makes no sense in this context,
285  // since this is used for communication that's internal to the application.
286  // default to http.
287  $scalerBaseUrl = $this->urlUtils->expand( $scalerBaseUrl, PROTO_CANONICAL );
288  }
289 
290  $scalerThumbUrl = $scalerBaseUrl . '/' . $file->getUrlRel() .
291  '/' . rawurlencode( $scalerThumbName );
292  $secret = false;
293  }
294 
295  // make an http request based on wgUploadStashScalerBaseUrl to lazy-create
296  // a thumbnail
297  $httpOptions = [
298  'method' => 'GET',
299  'timeout' => 5 // T90599 attempt to time out cleanly
300  ];
301  $req = $this->httpRequestFactory->create( $scalerThumbUrl, $httpOptions, __METHOD__ );
302 
303  // Pass a secret key shared with the proxied service if any
304  if ( strlen( $secret ) ) {
305  $req->setHeader( 'X-Swift-Secret', $secret );
306  }
307 
308  $status = $req->execute();
309  if ( !$status->isOK() ) {
310  $errors = $status->getErrorsArray();
312  $this->msg(
313  'uploadstash-file-not-found-no-remote-thumb',
314  print_r( $errors, 1 ),
315  $scalerThumbUrl
316  )
317  );
318  }
319  $contentType = $req->getResponseHeader( "content-type" );
320  if ( !$contentType ) {
322  $this->msg( 'uploadstash-file-not-found-missing-content-type' )
323  );
324  }
325 
326  $this->outputContents( $req->getContent(), $contentType );
327  }
328 
337  private function outputLocalFile( File $file ) {
338  if ( $file->getSize() > self::MAX_SERVE_BYTES ) {
340  $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
341  );
342  }
343 
344  $file->getRepo()->streamFileWithStatus( $file->getPath(),
345  [ 'Content-Transfer-Encoding: binary',
346  'Expires: Sun, 17-Jan-2038 19:14:07 GMT' ]
347  );
348  }
349 
357  private function outputContents( $content, $contentType ) {
358  $size = strlen( $content );
359  if ( $size > self::MAX_SERVE_BYTES ) {
361  $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
362  );
363  }
364  // Cancel output buffering and gzipping if set
366  self::outputFileHeaders( $contentType, $size );
367  print $content;
368  }
369 
379  private static function outputFileHeaders( $contentType, $size ) {
380  header( "Content-Type: $contentType", true );
381  header( 'Content-Transfer-Encoding: binary', true );
382  header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true );
383  // T55032 - It shouldn't be a problem here, but let's be safe and not cache
384  header( 'Cache-Control: private' );
385  header( "Content-Length: $size", true );
386  }
387 
392  private function showUploads() {
393  // sets the title, etc.
394  $this->setHeaders();
395  $this->outputHeader();
396 
397  // create the form, which will also be used to execute a callback to process incoming form data
398  // this design is extremely dubious, but supposedly HTMLForm is our standard now?
399 
400  $form = HTMLForm::factory( 'ooui', [
401  'Clear' => [
402  'type' => 'hidden',
403  'default' => true,
404  'name' => 'clear',
405  ]
406  ], $this->getContext(), 'clearStashedUploads' );
407  $form->setTitle( $this->getPageTitle() ); // Remove subpage
408  $form->setSubmitDestructive();
409  $form->setSubmitCallback( function ( $formData, $form ) {
410  if ( isset( $formData['Clear'] ) ) {
411  wfDebug( 'stash has: ' . print_r( $this->stash->listFiles(), true ) );
412 
413  if ( !$this->stash->clear() ) {
414  return Status::newFatal( 'uploadstash-errclear' );
415  }
416  }
417 
418  return Status::newGood();
419  } );
420  $form->setSubmitTextMsg( 'uploadstash-clear' );
421 
422  $form->prepareForm();
423  $formResult = $form->tryAuthorizedSubmit();
424 
425  // show the files + form, if there are any, or just say there are none
426  $linkRenderer = $this->getLinkRenderer();
427  $refreshHtml = $linkRenderer->makeKnownLink(
428  $this->getPageTitle(),
429  $this->msg( 'uploadstash-refresh' )->text()
430  );
431  $files = $this->stash->listFiles();
432  if ( $files && count( $files ) ) {
433  sort( $files );
434  $fileListItemsHtml = '';
435  foreach ( $files as $file ) {
436  $itemHtml = $linkRenderer->makeKnownLink(
437  $this->getPageTitle( "file/$file" ),
438  $file
439  );
440  try {
441  $fileObj = $this->stash->getFile( $file );
442  $thumb = $fileObj->generateThumbName( $file, [ 'width' => 220 ] );
443  $itemHtml .=
444  $this->msg( 'word-separator' )->escaped() .
445  $this->msg( 'parentheses' )->rawParams(
446  $linkRenderer->makeKnownLink(
447  $this->getPageTitle( "thumb/$file/$thumb" ),
448  $this->msg( 'uploadstash-thumbnail' )->text()
449  )
450  )->escaped();
451  } catch ( TimeoutException $e ) {
452  throw $e;
453  } catch ( Exception $e ) {
455  }
456  $fileListItemsHtml .= Html::rawElement( 'li', [], $itemHtml );
457  }
458  $this->getOutput()->addHTML( Html::rawElement( 'ul', [], $fileListItemsHtml ) );
459  $form->displayForm( $formResult );
460  $this->getOutput()->addHTML( Html::rawElement( 'p', [], $refreshHtml ) );
461  } else {
462  $this->getOutput()->addHTML( Html::rawElement( 'p', [],
463  Html::element( 'span', [], $this->msg( 'uploadstash-nofiles' )->text() )
464  . ' '
465  . $refreshHtml
466  ) );
467  }
468  }
469 }
470 
475 class_alias( SpecialUploadStash::class, 'SpecialUploadStash' );
const PROTO_CANONICAL
Definition: Defines.php:197
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:70
const RENDER_NOW
Force rendering in the current process.
Definition: File.php:80
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition: HTMLForm.php:158
static factory( $displayFormat, $descriptor, IContextSource $context, $messagePrefix='')
Construct a HTMLForm object for given display type.
Definition: HTMLForm.php:360
Show an error that looks like an HTTP server error.
Definition: HttpError.php:32
Local repository that stores files in the local filesystem and registers them in the wiki's own datab...
Definition: LocalRepo.php:45
Handler class for MWExceptions.
static logException(Throwable $e, $catcher=self::CAUGHT_BY_OTHER, $extraData=[])
Log a throwable to the exception log (if enabled).
MediaWiki exception.
Definition: MWException.php:33
This class is a collection of static functions that serve two purposes:
Definition: Html.php:57
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:235
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:256
Factory creating MWHttpRequest objects.
A class containing constants representing the names of configuration variables.
const UploadStashScalerBaseUrl
Name constant for the UploadStashScalerBaseUrl setting, for use with Config::get()
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getUser()
Shortcut to get the User executing this instance.
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
getPageTitle( $subpage=false)
Get a self-referential title object.
checkPermissions()
Checks if userCanExecute, and if not throws a PermissionsError.
getConfig()
Shortcut to get main config object.
getContext()
Gets the context this SpecialPage is executed in.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getOutput()
Get the OutputPage being used for this instance.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
Shortcut to construct a special page which is unlisted by default.
Web access for files temporarily stored by UploadStash.
doesWrites()
Indicates whether this special page may perform database writes.
execute( $subPage)
Execute page – can output a file directly or show a listing of them.
showUpload( $key)
If file available in stash, cats it out to the client as a simple HTTP response.
__construct(RepoGroup $repoGroup, HttpRequestFactory $httpRequestFactory, UrlUtils $urlUtils)
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:58
A service to expand, parse, and otherwise manipulate URLs.
Definition: UrlUtils.php:17
Prioritized list of file repositories.
Definition: RepoGroup.php:30
getLocalRepo()
Get the local repository, i.e.
Definition: RepoGroup.php:343
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
File without associated database record.
UploadStash is intended to accomplish a few things:
Definition: UploadStash.php:57
$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