MediaWiki 1.41.2
SpecialUploadStash.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Specials;
24
25use Exception;
26use File;
27use HTMLForm;
28use HttpError;
29use LocalRepo;
36use MWException;
38use RepoGroup;
41use UploadStash;
44use 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 ) {
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' ) {
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 ) {
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 {
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
475class_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
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.
This class is a collection of static functions that serve two purposes:
Definition Html.php:57
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.
File without associated database record.
UploadStash is intended to accomplish a few things:
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...
$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