MediaWiki REL1_39
SpecialUploadStash.php
Go to the documentation of this file.
1<?php
25use Wikimedia\RequestTimeout\TimeoutException;
26
42 private $stash;
43
45 private $localRepo;
46
48 private $httpRequestFactory;
49
60 private const MAX_SERVE_BYTES = 1048576; // 1 MiB
61
66 public function __construct(
67 RepoGroup $repoGroup,
68 HttpRequestFactory $httpRequestFactory
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 throw new HttpError( $code, $message );
127 }
128
138 private function parseKey( $key ) {
139 $type = strtok( $key, '/' );
140
141 if ( $type !== 'file' && $type !== 'thumb' ) {
143 $this->msg( 'uploadstash-bad-path-unknown-type', $type )
144 );
145 }
146 $fileName = strtok( '/' );
147 $thumbPart = strtok( '/' );
148 $file = $this->stash->getFile( $fileName );
149 if ( $type === 'thumb' ) {
150 $srcNamePos = strrpos( $thumbPart, $fileName );
151 if ( $srcNamePos === false || $srcNamePos < 1 ) {
153 $this->msg( 'uploadstash-bad-path-unrecognized-thumb-name' )
154 );
155 }
156 $paramString = substr( $thumbPart, 0, $srcNamePos - 1 );
157
158 $handler = $file->getHandler();
159 if ( $handler ) {
160 $params = $handler->parseParamString( $paramString );
161
162 return [ 'file' => $file, 'type' => $type, 'params' => $params ];
163 } else {
165 $this->msg( 'uploadstash-bad-path-no-handler', $file->getMimeType(), $file->getPath() )
166 );
167 }
168 }
169
170 return [ 'file' => $file, 'type' => $type ];
171 }
172
179 private function outputThumbFromStash( $file, $params ) {
180 $flags = 0;
181 // this config option, if it exists, points to a "scaler", as you might find in
182 // the Wikimedia Foundation cluster. See outputRemoteScaledThumb(). This
183 // is part of our horrible NFS-based system, we create a file on a mount
184 // point here, but fetch the scaled file from somewhere else that
185 // happens to share it over NFS.
186 if ( $file->getRepo()->getThumbProxyUrl()
187 || $this->getConfig()->get( MainConfigNames::UploadStashScalerBaseUrl )
188 ) {
189 $this->outputRemoteScaledThumb( $file, $params, $flags );
190 } else {
191 $this->outputLocallyScaledThumb( $file, $params, $flags );
192 }
193 }
194
203 private function outputLocallyScaledThumb( $file, $params, $flags ) {
204 // n.b. this is stupid, we insist on re-transforming the file every time we are invoked. We rely
205 // on HTTP caching to ensure this doesn't happen.
206
207 $flags |= File::RENDER_NOW;
208
209 $thumbnailImage = $file->transform( $params, $flags );
210 if ( !$thumbnailImage ) {
212 $this->msg( 'uploadstash-file-not-found-no-thumb' )
213 );
214 }
215
216 // we should have just generated it locally
217 if ( !$thumbnailImage->getStoragePath() ) {
219 $this->msg( 'uploadstash-file-not-found-no-local-path' )
220 );
221 }
222
223 // now we should construct a File, so we can get MIME and other such info in a standard way
224 // n.b. MIME type may be different from original (ogx original -> jpeg thumb)
225 $thumbFile = new UnregisteredLocalFile( false,
226 $this->stash->repo, $thumbnailImage->getStoragePath(), false );
227
228 $this->outputLocalFile( $thumbFile );
229 }
230
249 private function outputRemoteScaledThumb( $file, $params, $flags ) {
250 // We need to use generateThumbName() instead of thumbName(), because
251 // the suffix needs to match the file name for the remote thumbnailer
252 // to work
253 $scalerThumbName = $file->generateThumbName( $file->getName(), $params );
254
255 // If a thumb proxy is set up for the repo, we favor that, as that will
256 // keep the request internal
257 $thumbProxyUrl = $file->getRepo()->getThumbProxyUrl();
258 if ( strlen( $thumbProxyUrl ) ) {
259 $scalerThumbUrl = $thumbProxyUrl . 'temp/' . $file->getUrlRel() .
260 '/' . rawurlencode( $scalerThumbName );
261 $secret = $file->getRepo()->getThumbProxySecret();
262 } else {
263 // This option probably looks something like
264 // '//upload.wikimedia.org/wikipedia/test/thumb/temp'. Do not use
265 // trailing slash.
266 $scalerBaseUrl = $this->getConfig()->get( MainConfigNames::UploadStashScalerBaseUrl );
267
268 if ( preg_match( '/^\/\//', $scalerBaseUrl ) ) {
269 // this is apparently a protocol-relative URL, which makes no sense in this context,
270 // since this is used for communication that's internal to the application.
271 // default to http.
272 $scalerBaseUrl = wfExpandUrl( $scalerBaseUrl, PROTO_CANONICAL );
273 }
274
275 $scalerThumbUrl = $scalerBaseUrl . '/' . $file->getUrlRel() .
276 '/' . rawurlencode( $scalerThumbName );
277 $secret = false;
278 }
279
280 // make an http request based on wgUploadStashScalerBaseUrl to lazy-create
281 // a thumbnail
282 $httpOptions = [
283 'method' => 'GET',
284 'timeout' => 5 // T90599 attempt to time out cleanly
285 ];
286 $req = $this->httpRequestFactory->create( $scalerThumbUrl, $httpOptions, __METHOD__ );
287
288 // Pass a secret key shared with the proxied service if any
289 if ( strlen( $secret ) ) {
290 $req->setHeader( 'X-Swift-Secret', $secret );
291 }
292
293 $status = $req->execute();
294 if ( !$status->isOK() ) {
295 $errors = $status->getErrorsArray();
297 $this->msg(
298 'uploadstash-file-not-found-no-remote-thumb',
299 print_r( $errors, 1 ),
300 $scalerThumbUrl
301 )
302 );
303 }
304 $contentType = $req->getResponseHeader( "content-type" );
305 if ( !$contentType ) {
307 $this->msg( 'uploadstash-file-not-found-missing-content-type' )
308 );
309 }
310
311 $this->outputContents( $req->getContent(), $contentType );
312 }
313
322 private function outputLocalFile( File $file ) {
323 if ( $file->getSize() > self::MAX_SERVE_BYTES ) {
325 $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
326 );
327 }
328
329 $file->getRepo()->streamFileWithStatus( $file->getPath(),
330 [ 'Content-Transfer-Encoding: binary',
331 'Expires: Sun, 17-Jan-2038 19:14:07 GMT' ]
332 );
333 }
334
342 private function outputContents( $content, $contentType ) {
343 $size = strlen( $content );
344 if ( $size > self::MAX_SERVE_BYTES ) {
346 $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
347 );
348 }
349 // Cancel output buffering and gzipping if set
351 self::outputFileHeaders( $contentType, $size );
353 }
354
364 private static function outputFileHeaders( $contentType, $size ) {
365 header( "Content-Type: $contentType", true );
366 header( 'Content-Transfer-Encoding: binary', true );
367 header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true );
368 // T55032 - It shouldn't be a problem here, but let's be safe and not cache
369 header( 'Cache-Control: private' );
370 header( "Content-Length: $size", true );
371 }
372
377 private function showUploads() {
378 // sets the title, etc.
379 $this->setHeaders();
380 $this->outputHeader();
381
382 // create the form, which will also be used to execute a callback to process incoming form data
383 // this design is extremely dubious, but supposedly HTMLForm is our standard now?
384
385 $form = HTMLForm::factory( 'ooui', [
386 'Clear' => [
387 'type' => 'hidden',
388 'default' => true,
389 'name' => 'clear',
390 ]
391 ], $this->getContext(), 'clearStashedUploads' );
392 $form->setTitle( $this->getPageTitle() ); // Remove subpage
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
411 $linkRenderer = $this->getLinkRenderer();
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(
431 $linkRenderer->makeKnownLink(
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 ) {
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:199
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: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:348
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
Local repository that stores files in the local filesystem and registers them in the wiki's own datab...
Definition LocalRepo.php:39
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.
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)
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.
static newGood( $value=null)
Factory function for good results.
Shortcut to construct a special page which is unlisted by default.
File without associated database record.
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