MediaWiki master
SpecialUploadStash.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Specials;
22
23use Exception;
24use File;
25use HttpError;
26use LocalRepo;
35use RepoGroup;
38use UploadStash;
42
58 private $stash;
59
60 private LocalRepo $localRepo;
61 private HttpRequestFactory $httpRequestFactory;
62 private UrlUtils $urlUtils;
63 private IConnectionProvider $dbProvider;
64
75 private const MAX_SERVE_BYTES = 1_048_576; // 1 MiB
76
83 public function __construct(
84 RepoGroup $repoGroup,
85 HttpRequestFactory $httpRequestFactory,
86 UrlUtils $urlUtils,
87 IConnectionProvider $dbProvider
88 ) {
89 parent::__construct( 'UploadStash', 'upload' );
90 $this->localRepo = $repoGroup->getLocalRepo();
91 $this->httpRequestFactory = $httpRequestFactory;
92 $this->urlUtils = $urlUtils;
93 $this->dbProvider = $dbProvider;
94 }
95
96 public function doesWrites() {
97 return true;
98 }
99
106 public function execute( $subPage ) {
108
109 // This is not set in constructor, because the context with the user is not safe to be set
110 $this->stash = $this->localRepo->getUploadStash( $this->getUser() );
111 $this->checkPermissions();
112
113 if ( $subPage === null || $subPage === '' ) {
114 $this->showUploads();
115 } else {
116 $this->showUpload( $subPage );
117 }
118 }
119
127 public function showUpload( $key ) {
128 // prevent callers from doing standard HTML output -- we'll take it from here
129 $this->getOutput()->disable();
130
131 try {
132 $params = $this->parseKey( $key );
133 if ( $params['type'] === 'thumb' ) {
134 $this->outputThumbFromStash( $params['file'], $params['params'] );
135 } else {
136 $this->outputLocalFile( $params['file'] );
137 }
138 return;
139 } catch ( UploadStashFileNotFoundException $e ) {
140 $code = 404;
141 $message = $e->getMessage();
142 } catch ( Exception $e ) {
143 $code = 500;
144 $message = $e->getMessage();
145 }
146
147 throw new HttpError( $code, $message );
148 }
149
159 private function parseKey( $key ) {
160 $type = strtok( $key, '/' );
161
162 if ( $type !== 'file' && $type !== 'thumb' ) {
164 $this->msg( 'uploadstash-bad-path-unknown-type', $type )
165 );
166 }
167 $fileName = strtok( '/' );
168 $thumbPart = strtok( '/' );
169 $file = $this->stash->getFile( $fileName );
170 if ( $type === 'thumb' ) {
171 $srcNamePos = strrpos( $thumbPart, $fileName );
172 if ( $srcNamePos === false || $srcNamePos < 1 ) {
174 $this->msg( 'uploadstash-bad-path-unrecognized-thumb-name' )
175 );
176 }
177 $paramString = substr( $thumbPart, 0, $srcNamePos - 1 );
178
179 $handler = $file->getHandler();
180 if ( $handler ) {
181 $params = $handler->parseParamString( $paramString );
182 if ( $params === false ) {
183 // The params are invalid, but still try to show a thumb
184 $params = [];
185 }
186
187 return [ 'file' => $file, 'type' => $type, 'params' => $params ];
188 } else {
190 $this->msg( 'uploadstash-bad-path-no-handler', $file->getMimeType(), $file->getPath() )
191 );
192 }
193 }
194
195 return [ 'file' => $file, 'type' => $type ];
196 }
197
204 private function outputThumbFromStash( $file, $params ) {
205 // this config option, if it exists, points to a "scaler", as you might find in
206 // the Wikimedia Foundation cluster. See outputRemoteScaledThumb(). This
207 // is part of our horrible NFS-based system, we create a file on a mount
208 // point here, but fetch the scaled file from somewhere else that
209 // happens to share it over NFS.
210 if ( $file->getRepo()->getThumbProxyUrl()
211 || $this->getConfig()->get( MainConfigNames::UploadStashScalerBaseUrl )
212 ) {
213 $this->outputRemoteScaledThumb( $file, $params );
214 } else {
215 $this->outputLocallyScaledThumb( $file, $params );
216 }
217 }
218
225 private function outputLocallyScaledThumb( $file, $params ) {
226 // n.b. this is stupid, we insist on re-transforming the file every time we are invoked. We rely
227 // on HTTP caching to ensure this doesn't happen.
228
229 $thumbnailImage = $file->transform( $params, File::RENDER_NOW );
230 if ( !$thumbnailImage ) {
232 $this->msg( 'uploadstash-file-not-found-no-thumb' )
233 );
234 }
235
236 // we should have just generated it locally
237 if ( !$thumbnailImage->getStoragePath() ) {
239 $this->msg( 'uploadstash-file-not-found-no-local-path' )
240 );
241 }
242
243 // now we should construct a File, so we can get MIME and other such info in a standard way
244 // n.b. MIME type may be different from original (ogx original -> jpeg thumb)
245 $thumbFile = new UnregisteredLocalFile( false,
246 $this->stash->repo, $thumbnailImage->getStoragePath(), false );
247
248 $this->outputLocalFile( $thumbFile );
249 }
250
267 private function outputRemoteScaledThumb( $file, $params ) {
268 // We need to use generateThumbName() instead of thumbName(), because
269 // the suffix needs to match the file name for the remote thumbnailer
270 // to work
271 $scalerThumbName = $file->generateThumbName( $file->getName(), $params );
272
273 // If a thumb proxy is set up for the repo, we favor that, as that will
274 // keep the request internal
275 $thumbProxyUrl = $file->getRepo()->getThumbProxyUrl();
276 if ( strlen( $thumbProxyUrl ) ) {
277 $scalerThumbUrl = $thumbProxyUrl . 'temp/' . $file->getUrlRel() .
278 '/' . rawurlencode( $scalerThumbName );
279 $secret = $file->getRepo()->getThumbProxySecret();
280 } else {
281 // This option probably looks something like
282 // '//upload.wikimedia.org/wikipedia/test/thumb/temp'. Do not use
283 // trailing slash.
284 $scalerBaseUrl = $this->getConfig()->get( MainConfigNames::UploadStashScalerBaseUrl );
285
286 if ( preg_match( '/^\/\//', $scalerBaseUrl ) ) {
287 // this is apparently a protocol-relative URL, which makes no sense in this context,
288 // since this is used for communication that's internal to the application.
289 // default to http.
290 $scalerBaseUrl = $this->urlUtils->expand( $scalerBaseUrl, PROTO_CANONICAL );
291 }
292
293 $scalerThumbUrl = $scalerBaseUrl . '/' . $file->getUrlRel() .
294 '/' . rawurlencode( $scalerThumbName );
295 $secret = false;
296 }
297
298 // make an http request based on wgUploadStashScalerBaseUrl to lazy-create
299 // a thumbnail
300 $httpOptions = [
301 'method' => 'GET',
302 'timeout' => 5 // T90599 attempt to time out cleanly
303 ];
304 $req = $this->httpRequestFactory->create( $scalerThumbUrl, $httpOptions, __METHOD__ );
305
306 // Pass a secret key shared with the proxied service if any
307 if ( strlen( $secret ) ) {
308 $req->setHeader( 'X-Swift-Secret', $secret );
309 }
310
311 $status = $req->execute();
312 if ( !$status->isOK() ) {
314 $this->msg(
315 'uploadstash-file-not-found-no-remote-thumb',
316 $status->getMessage(),
317 $scalerThumbUrl
318 )
319 );
320 }
321 $contentType = $req->getResponseHeader( "content-type" );
322 if ( !$contentType ) {
324 $this->msg( 'uploadstash-file-not-found-missing-content-type' )
325 );
326 }
327
328 $this->outputContents( $req->getContent(), $contentType );
329 }
330
339 private function outputLocalFile( File $file ) {
340 if ( $file->getSize() > self::MAX_SERVE_BYTES ) {
342 $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
343 );
344 }
345
346 $file->getRepo()->streamFileWithStatus( $file->getPath(),
347 [ 'Content-Transfer-Encoding: binary',
348 'Expires: Sun, 17-Jan-2038 19:14:07 GMT' ]
349 );
350 }
351
359 private function outputContents( $content, $contentType ) {
360 $size = strlen( $content );
361 if ( $size > self::MAX_SERVE_BYTES ) {
363 $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
364 );
365 }
366 // Cancel output buffering and gzipping if set
368 self::outputFileHeaders( $contentType, $size );
369 print $content;
370 }
371
381 private static function outputFileHeaders( $contentType, $size ) {
382 header( "Content-Type: $contentType", true );
383 header( 'Content-Transfer-Encoding: binary', true );
384 header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true );
385 // T55032 - It shouldn't be a problem here, but let's be safe and not cache
386 header( 'Cache-Control: private' );
387 header( "Content-Length: $size", true );
388 }
389
394 private function showUploads() {
395 // sets the title, etc.
396 $this->setHeaders();
397 $this->outputHeader();
398
399 // create the form, which will also be used to execute a callback to process incoming form data
400 // this design is extremely dubious, but supposedly HTMLForm is our standard now?
401
402 $form = HTMLForm::factory( 'ooui', [
403 'Clear' => [
404 'type' => 'hidden',
405 'default' => true,
406 'name' => 'clear',
407 ]
408 ], $this->getContext(), 'clearStashedUploads' );
409 $form->setTitle( $this->getPageTitle() ); // Remove subpage
410 $form->setSubmitDestructive();
411 $form->setSubmitCallback( function ( $formData, $form ) {
412 if ( isset( $formData['Clear'] ) ) {
413 wfDebug( 'stash has: ' . print_r( $this->stash->listFiles(), true ) );
414
415 if ( !$this->stash->clear() ) {
416 return Status::newFatal( 'uploadstash-errclear' );
417 }
418 }
419
420 return Status::newGood();
421 } );
422 $form->setSubmitTextMsg( 'uploadstash-clear' );
423
424 $form->prepareForm();
425 $formResult = $form->tryAuthorizedSubmit();
426
427 // show the files + form, if there are any, or just say there are none
428 $linkRenderer = $this->getLinkRenderer();
429 $refreshHtml = $linkRenderer->makeKnownLink(
430 $this->getPageTitle(),
431 $this->msg( 'uploadstash-refresh' )->text()
432 );
433 $pager = new UploadStashPager(
434 $this->getContext(),
435 $linkRenderer,
436 $this->dbProvider,
437 $this->stash,
438 $this->localRepo
439 );
440 if ( $pager->getNumRows() ) {
441 $pager->getForm();
442 $this->getOutput()->addParserOutputContent( $pager->getFullOutput() );
443 $form->displayForm( $formResult );
444 $this->getOutput()->addHTML( Html::rawElement( 'p', [], $refreshHtml ) );
445 } else {
446 $this->getOutput()->addHTML( Html::rawElement( 'p', [],
447 Html::element( 'span', [], $this->msg( 'uploadstash-nofiles' )->text() )
448 . ' '
449 . $refreshHtml
450 ) );
451 }
452 }
453}
454
459class_alias( SpecialUploadStash::class, 'SpecialUploadStash' );
const PROTO_CANONICAL
Definition Defines.php:210
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.
array $params
The job parameters.
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:76
getPath()
Return the storage path to the file.
Definition File.php:472
getMimeType()
Returns the MIME type of the file.
Definition File.php:848
getSize()
Return the size of the image file, in bytes Overridden by LocalFile, UnregisteredLocalFile STUB.
Definition File.php:836
getName()
Return the name of this file.
Definition File.php:344
getRepo()
Returns the repository.
Definition File.php:2055
transform( $params, $flags=0)
Transform a media file.
Definition File.php:1182
getHandler()
Get a MediaHandler instance for this file.
Definition File.php:1553
getUrlRel()
Get urlencoded path of the file relative to the public zone root.
Definition File.php:1760
generateThumbName( $name, $params)
Generate a thumbnail file name from a name and specified parameters.
Definition File.php:1099
Show an error that looks like an HTTP server error.
Definition HttpError.php:33
Local repository that stores files in the local filesystem and registers them in the wiki's own datab...
Definition LocalRepo.php:49
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:208
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
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 By default the message key is the canonical name of...
Shortcut to construct a special page which is unlisted by default.
Web access for files temporarily stored by UploadStash.
doesWrites()
Indicates whether POST requests to this special page require write access to the wiki.
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, IConnectionProvider $dbProvider)
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
A service to expand, parse, and otherwise manipulate URLs.
Definition UrlUtils.php:16
Prioritized list of file repositories.
Definition RepoGroup.php:32
getLocalRepo()
Get the local repository, i.e.
File without associated database record.
UploadStash is intended to accomplish a few things:
Provide primary and replica IDatabase connections.
element(SerializerNode $parent, SerializerNode $node, $contents)