MediaWiki REL1_38
SpecialUploadStash.php
Go to the documentation of this file.
1<?php
24use Wikimedia\RequestTimeout\TimeoutException;
25
41 private $stash;
42
44 private $localRepo;
45
48
59 private const MAX_SERVE_BYTES = 1048576; // 1 MiB
60
65 public function __construct(
66 RepoGroup $repoGroup,
68 ) {
69 parent::__construct( 'UploadStash', 'upload' );
70 $this->localRepo = $repoGroup->getLocalRepo();
71 $this->httpRequestFactory = $httpRequestFactory;
72 }
73
74 public function doesWrites() {
75 return true;
76 }
77
84 public function execute( $subPage ) {
86
87 // This is not set in constructor, because the context with the user is not safe to be set
88 $this->stash = $this->localRepo->getUploadStash( $this->getUser() );
89 $this->checkPermissions();
90
91 if ( $subPage === null || $subPage === '' ) {
92 $this->showUploads();
93 } else {
94 $this->showUpload( $subPage );
95 }
96 }
97
105 public function showUpload( $key ) {
106 // prevent callers from doing standard HTML output -- we'll take it from here
107 $this->getOutput()->disable();
108
109 try {
110 $params = $this->parseKey( $key );
111 if ( $params['type'] === 'thumb' ) {
112 $this->outputThumbFromStash( $params['file'], $params['params'] );
113 } else {
114 $this->outputLocalFile( $params['file'] );
115 }
116 return;
117 } catch ( UploadStashFileNotFoundException $e ) {
118 $code = 404;
119 $message = $e->getMessage();
120 } catch ( Exception $e ) {
121 $code = 500;
122 $message = $e->getMessage();
123 }
124
125 throw new HttpError( $code, $message );
126 }
127
137 private function parseKey( $key ) {
138 $type = strtok( $key, '/' );
139
140 if ( $type !== 'file' && $type !== 'thumb' ) {
142 $this->msg( 'uploadstash-bad-path-unknown-type', $type )
143 );
144 }
145 $fileName = strtok( '/' );
146 $thumbPart = strtok( '/' );
147 $file = $this->stash->getFile( $fileName );
148 if ( $type === 'thumb' ) {
149 $srcNamePos = strrpos( $thumbPart, $fileName );
150 if ( $srcNamePos === false || $srcNamePos < 1 ) {
152 $this->msg( 'uploadstash-bad-path-unrecognized-thumb-name' )
153 );
154 }
155 $paramString = substr( $thumbPart, 0, $srcNamePos - 1 );
156
157 $handler = $file->getHandler();
158 if ( $handler ) {
159 $params = $handler->parseParamString( $paramString );
160
161 return [ 'file' => $file, 'type' => $type, 'params' => $params ];
162 } else {
164 $this->msg( 'uploadstash-bad-path-no-handler', $file->getMimeType(), $file->getPath() )
165 );
166 }
167 }
168
169 return [ 'file' => $file, 'type' => $type ];
170 }
171
178 private function outputThumbFromStash( $file, $params ) {
179 $flags = 0;
180 // this config option, if it exists, points to a "scaler", as you might find in
181 // the Wikimedia Foundation cluster. See outputRemoteScaledThumb(). This
182 // is part of our horrible NFS-based system, we create a file on a mount
183 // point here, but fetch the scaled file from somewhere else that
184 // happens to share it over NFS.
185 if ( $file->getRepo()->getThumbProxyUrl()
186 || $this->getConfig()->get( 'UploadStashScalerBaseUrl' )
187 ) {
188 $this->outputRemoteScaledThumb( $file, $params, $flags );
189 } else {
190 $this->outputLocallyScaledThumb( $file, $params, $flags );
191 }
192 }
193
202 private function outputLocallyScaledThumb( $file, $params, $flags ) {
203 // n.b. this is stupid, we insist on re-transforming the file every time we are invoked. We rely
204 // on HTTP caching to ensure this doesn't happen.
205
206 $flags |= File::RENDER_NOW;
207
208 $thumbnailImage = $file->transform( $params, $flags );
209 if ( !$thumbnailImage ) {
211 $this->msg( 'uploadstash-file-not-found-no-thumb' )
212 );
213 }
214
215 // we should have just generated it locally
216 if ( !$thumbnailImage->getStoragePath() ) {
218 $this->msg( 'uploadstash-file-not-found-no-local-path' )
219 );
220 }
221
222 // now we should construct a File, so we can get MIME and other such info in a standard way
223 // n.b. MIME type may be different from original (ogx original -> jpeg thumb)
224 $thumbFile = new UnregisteredLocalFile( false,
225 $this->stash->repo, $thumbnailImage->getStoragePath(), false );
226
227 $this->outputLocalFile( $thumbFile );
228 }
229
248 private function outputRemoteScaledThumb( $file, $params, $flags ) {
249 // We need to use generateThumbName() instead of thumbName(), because
250 // the suffix needs to match the file name for the remote thumbnailer
251 // to work
252 $scalerThumbName = $file->generateThumbName( $file->getName(), $params );
253
254 // If a thumb proxy is set up for the repo, we favor that, as that will
255 // keep the request internal
256 $thumbProxyUrl = $file->getRepo()->getThumbProxyUrl();
257 if ( strlen( $thumbProxyUrl ) ) {
258 $scalerThumbUrl = $thumbProxyUrl . 'temp/' . $file->getUrlRel() .
259 '/' . rawurlencode( $scalerThumbName );
260 $secret = $file->getRepo()->getThumbProxySecret();
261 } else {
262 // This option probably looks something like
263 // '//upload.wikimedia.org/wikipedia/test/thumb/temp'. Do not use
264 // trailing slash.
265 $scalerBaseUrl = $this->getConfig()->get( 'UploadStashScalerBaseUrl' );
266
267 if ( preg_match( '/^\/\//', $scalerBaseUrl ) ) {
268 // this is apparently a protocol-relative URL, which makes no sense in this context,
269 // since this is used for communication that's internal to the application.
270 // default to http.
271 $scalerBaseUrl = wfExpandUrl( $scalerBaseUrl, PROTO_CANONICAL );
272 }
273
274 $scalerThumbUrl = $scalerBaseUrl . '/' . $file->getUrlRel() .
275 '/' . rawurlencode( $scalerThumbName );
276 $secret = false;
277 }
278
279 // make an http request based on wgUploadStashScalerBaseUrl to lazy-create
280 // a thumbnail
281 $httpOptions = [
282 'method' => 'GET',
283 'timeout' => 5 // T90599 attempt to time out cleanly
284 ];
285 $req = $this->httpRequestFactory->create( $scalerThumbUrl, $httpOptions, __METHOD__ );
286
287 // Pass a secret key shared with the proxied service if any
288 if ( strlen( $secret ) ) {
289 $req->setHeader( 'X-Swift-Secret', $secret );
290 }
291
292 $status = $req->execute();
293 if ( !$status->isOK() ) {
294 $errors = $status->getErrorsArray();
296 $this->msg(
297 'uploadstash-file-not-found-no-remote-thumb',
298 print_r( $errors, 1 ),
299 $scalerThumbUrl
300 )
301 );
302 }
303 $contentType = $req->getResponseHeader( "content-type" );
304 if ( !$contentType ) {
306 $this->msg( 'uploadstash-file-not-found-missing-content-type' )
307 );
308 }
309
310 $this->outputContents( $req->getContent(), $contentType );
311 }
312
321 private function outputLocalFile( File $file ) {
322 if ( $file->getSize() > self::MAX_SERVE_BYTES ) {
324 $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
325 );
326 }
327
328 $file->getRepo()->streamFileWithStatus( $file->getPath(),
329 [ 'Content-Transfer-Encoding: binary',
330 'Expires: Sun, 17-Jan-2038 19:14:07 GMT' ]
331 );
332 }
333
341 private function outputContents( $content, $contentType ) {
342 $size = strlen( $content );
343 if ( $size > self::MAX_SERVE_BYTES ) {
345 $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
346 );
347 }
348 // Cancel output buffering and gzipping if set
350 self::outputFileHeaders( $contentType, $size );
352 }
353
363 private static function outputFileHeaders( $contentType, $size ) {
364 header( "Content-Type: $contentType", true );
365 header( 'Content-Transfer-Encoding: binary', true );
366 header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true );
367 // T55032 - It shouldn't be a problem here, but let's be safe and not cache
368 header( 'Cache-Control: private' );
369 header( "Content-Length: $size", true );
370 }
371
376 private function showUploads() {
377 // sets the title, etc.
378 $this->setHeaders();
379 $this->outputHeader();
380
381 // create the form, which will also be used to execute a callback to process incoming form data
382 // this design is extremely dubious, but supposedly HTMLForm is our standard now?
383
384 $context = new DerivativeContext( $this->getContext() );
385 $context->setTitle( $this->getPageTitle() ); // Remove subpage
386 $form = HTMLForm::factory( 'ooui', [
387 'Clear' => [
388 'type' => 'hidden',
389 'default' => true,
390 'name' => 'clear',
391 ]
392 ], $context, 'clearStashedUploads' );
393 $form->setSubmitDestructive();
394 $form->setSubmitCallback( function ( $formData, $form ) {
395 if ( isset( $formData['Clear'] ) ) {
396 wfDebug( 'stash has: ' . print_r( $this->stash->listFiles(), true ) );
397
398 if ( !$this->stash->clear() ) {
399 return Status::newFatal( 'uploadstash-errclear' );
400 }
401 }
402
403 return Status::newGood();
404 } );
405 $form->setSubmitTextMsg( 'uploadstash-clear' );
406
407 $form->prepareForm();
408 $formResult = $form->tryAuthorizedSubmit();
409
410 // show the files + form, if there are any, or just say there are none
412 $refreshHtml = $linkRenderer->makeKnownLink(
413 $this->getPageTitle(),
414 $this->msg( 'uploadstash-refresh' )->text()
415 );
416 $files = $this->stash->listFiles();
417 if ( $files && count( $files ) ) {
418 sort( $files );
419 $fileListItemsHtml = '';
420 foreach ( $files as $file ) {
421 $itemHtml = $linkRenderer->makeKnownLink(
422 $this->getPageTitle( "file/$file" ),
423 $file
424 );
425 try {
426 $fileObj = $this->stash->getFile( $file );
427 $thumb = $fileObj->generateThumbName( $file, [ 'width' => 220 ] );
428 $itemHtml .=
429 $this->msg( 'word-separator' )->escaped() .
430 $this->msg( 'parentheses' )->rawParams(
432 $this->getPageTitle( "thumb/$file/$thumb" ),
433 $this->msg( 'uploadstash-thumbnail' )->text()
434 )
435 )->escaped();
436 } catch ( TimeoutException $e ) {
437 throw $e;
438 } catch ( Exception $e ) {
439 MWExceptionHandler::logException( $e );
440 }
441 $fileListItemsHtml .= Html::rawElement( 'li', [], $itemHtml );
442 }
443 $this->getOutput()->addHTML( Html::rawElement( 'ul', [], $fileListItemsHtml ) );
444 $form->displayForm( $formResult );
445 $this->getOutput()->addHTML( Html::rawElement( 'p', [], $refreshHtml ) );
446 } else {
447 $this->getOutput()->addHTML( Html::rawElement( 'p', [],
448 Html::element( 'span', [], $this->msg( 'uploadstash-nofiles' )->text() )
449 . ' '
450 . $refreshHtml
451 ) );
452 }
453 }
454}
const PROTO_CANONICAL
Definition Defines.php:195
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.
An IContextSource implementation which will inherit context from another source but allow individual ...
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:67
Show an error that looks like an HTTP server error.
Definition HttpError.php:32
A repository that stores files in the local filesystem and registers them in the wiki's own database.
Definition LocalRepo.php:41
Factory creating MWHttpRequest objects.
makeKnownLink( $target, $text=null, array $extraAttribs=[], array $query=[])
Prioritized list of file repositories.
Definition RepoGroup.php:32
getLocalRepo()
Get the local repository, i.e.
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
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.
Shortcut to construct a special page which is unlisted by default.
A file object referring to either a standalone local file, or a file in a local repository with no da...
UploadStash is intended to accomplish a few things:
while(( $__line=Maintenance::readconsole()) !==false) print
Definition eval.php:69
$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