MediaWiki master
SpecialUploadStash.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Specials;
24
25use Exception;
26use File;
27use HttpError;
28use LocalRepo;
37use RepoGroup;
40use UploadStash;
44
60 private $stash;
61
62 private LocalRepo $localRepo;
63 private HttpRequestFactory $httpRequestFactory;
64 private UrlUtils $urlUtils;
65 private IConnectionProvider $dbProvider;
66
77 private const MAX_SERVE_BYTES = 1_048_576; // 1 MiB
78
85 public function __construct(
86 RepoGroup $repoGroup,
87 HttpRequestFactory $httpRequestFactory,
88 UrlUtils $urlUtils,
89 IConnectionProvider $dbProvider
90 ) {
91 parent::__construct( 'UploadStash', 'upload' );
92 $this->localRepo = $repoGroup->getLocalRepo();
93 $this->httpRequestFactory = $httpRequestFactory;
94 $this->urlUtils = $urlUtils;
95 $this->dbProvider = $dbProvider;
96 }
97
98 public function doesWrites() {
99 return true;
100 }
101
108 public function execute( $subPage ) {
110
111 // This is not set in constructor, because the context with the user is not safe to be set
112 $this->stash = $this->localRepo->getUploadStash( $this->getUser() );
113 $this->checkPermissions();
114
115 if ( $subPage === null || $subPage === '' ) {
116 $this->showUploads();
117 } else {
118 $this->showUpload( $subPage );
119 }
120 }
121
129 public function showUpload( $key ) {
130 // prevent callers from doing standard HTML output -- we'll take it from here
131 $this->getOutput()->disable();
132
133 try {
134 $params = $this->parseKey( $key );
135 if ( $params['type'] === 'thumb' ) {
136 $this->outputThumbFromStash( $params['file'], $params['params'] );
137 } else {
138 $this->outputLocalFile( $params['file'] );
139 }
140 return;
141 } catch ( UploadStashFileNotFoundException $e ) {
142 $code = 404;
143 $message = $e->getMessage();
144 } catch ( Exception $e ) {
145 $code = 500;
146 $message = $e->getMessage();
147 }
148
149 throw new HttpError( $code, $message );
150 }
151
161 private function parseKey( $key ) {
162 $type = strtok( $key, '/' );
163
164 if ( $type !== 'file' && $type !== 'thumb' ) {
166 $this->msg( 'uploadstash-bad-path-unknown-type', $type )
167 );
168 }
169 $fileName = strtok( '/' );
170 $thumbPart = strtok( '/' );
171 $file = $this->stash->getFile( $fileName );
172 if ( $type === 'thumb' ) {
173 $srcNamePos = strrpos( $thumbPart, $fileName );
174 if ( $srcNamePos === false || $srcNamePos < 1 ) {
176 $this->msg( 'uploadstash-bad-path-unrecognized-thumb-name' )
177 );
178 }
179 $paramString = substr( $thumbPart, 0, $srcNamePos - 1 );
180
181 $handler = $file->getHandler();
182 if ( $handler ) {
183 $params = $handler->parseParamString( $paramString );
184 if ( $params === false ) {
185 // The params are invalid, but still try to show a thumb
186 $params = [];
187 }
188
189 return [ 'file' => $file, 'type' => $type, 'params' => $params ];
190 } else {
192 $this->msg( 'uploadstash-bad-path-no-handler', $file->getMimeType(), $file->getPath() )
193 );
194 }
195 }
196
197 return [ 'file' => $file, 'type' => $type ];
198 }
199
206 private function outputThumbFromStash( $file, $params ) {
207 // this config option, if it exists, points to a "scaler", as you might find in
208 // the Wikimedia Foundation cluster. See outputRemoteScaledThumb(). This
209 // is part of our horrible NFS-based system, we create a file on a mount
210 // point here, but fetch the scaled file from somewhere else that
211 // happens to share it over NFS.
212 if ( $file->getRepo()->getThumbProxyUrl()
213 || $this->getConfig()->get( MainConfigNames::UploadStashScalerBaseUrl )
214 ) {
215 $this->outputRemoteScaledThumb( $file, $params );
216 } else {
217 $this->outputLocallyScaledThumb( $file, $params );
218 }
219 }
220
227 private function outputLocallyScaledThumb( $file, $params ) {
228 // n.b. this is stupid, we insist on re-transforming the file every time we are invoked. We rely
229 // on HTTP caching to ensure this doesn't happen.
230
231 $thumbnailImage = $file->transform( $params, File::RENDER_NOW );
232 if ( !$thumbnailImage ) {
234 $this->msg( 'uploadstash-file-not-found-no-thumb' )
235 );
236 }
237
238 // we should have just generated it locally
239 if ( !$thumbnailImage->getStoragePath() ) {
241 $this->msg( 'uploadstash-file-not-found-no-local-path' )
242 );
243 }
244
245 // now we should construct a File, so we can get MIME and other such info in a standard way
246 // n.b. MIME type may be different from original (ogx original -> jpeg thumb)
247 $thumbFile = new UnregisteredLocalFile( false,
248 $this->stash->repo, $thumbnailImage->getStoragePath(), false );
249
250 $this->outputLocalFile( $thumbFile );
251 }
252
269 private function outputRemoteScaledThumb( $file, $params ) {
270 // We need to use generateThumbName() instead of thumbName(), because
271 // the suffix needs to match the file name for the remote thumbnailer
272 // to work
273 $scalerThumbName = $file->generateThumbName( $file->getName(), $params );
274
275 // If a thumb proxy is set up for the repo, we favor that, as that will
276 // keep the request internal
277 $thumbProxyUrl = $file->getRepo()->getThumbProxyUrl();
278 if ( strlen( $thumbProxyUrl ) ) {
279 $scalerThumbUrl = $thumbProxyUrl . 'temp/' . $file->getUrlRel() .
280 '/' . rawurlencode( $scalerThumbName );
281 $secret = $file->getRepo()->getThumbProxySecret();
282 } else {
283 // This option probably looks something like
284 // '//upload.wikimedia.org/wikipedia/test/thumb/temp'. Do not use
285 // trailing slash.
286 $scalerBaseUrl = $this->getConfig()->get( MainConfigNames::UploadStashScalerBaseUrl );
287
288 if ( preg_match( '/^\/\//', $scalerBaseUrl ) ) {
289 // this is apparently a protocol-relative URL, which makes no sense in this context,
290 // since this is used for communication that's internal to the application.
291 // default to http.
292 $scalerBaseUrl = $this->urlUtils->expand( $scalerBaseUrl, PROTO_CANONICAL );
293 }
294
295 $scalerThumbUrl = $scalerBaseUrl . '/' . $file->getUrlRel() .
296 '/' . rawurlencode( $scalerThumbName );
297 $secret = false;
298 }
299
300 // make an http request based on wgUploadStashScalerBaseUrl to lazy-create
301 // a thumbnail
302 $httpOptions = [
303 'method' => 'GET',
304 'timeout' => 5 // T90599 attempt to time out cleanly
305 ];
306 $req = $this->httpRequestFactory->create( $scalerThumbUrl, $httpOptions, __METHOD__ );
307
308 // Pass a secret key shared with the proxied service if any
309 if ( strlen( $secret ) ) {
310 $req->setHeader( 'X-Swift-Secret', $secret );
311 }
312
313 $status = $req->execute();
314 if ( !$status->isOK() ) {
315 $errors = $status->getErrorsArray();
317 $this->msg(
318 'uploadstash-file-not-found-no-remote-thumb',
319 print_r( $errors, 1 ),
320 $scalerThumbUrl
321 )
322 );
323 }
324 $contentType = $req->getResponseHeader( "content-type" );
325 if ( !$contentType ) {
327 $this->msg( 'uploadstash-file-not-found-missing-content-type' )
328 );
329 }
330
331 $this->outputContents( $req->getContent(), $contentType );
332 }
333
342 private function outputLocalFile( File $file ) {
343 if ( $file->getSize() > self::MAX_SERVE_BYTES ) {
345 $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
346 );
347 }
348
349 $file->getRepo()->streamFileWithStatus( $file->getPath(),
350 [ 'Content-Transfer-Encoding: binary',
351 'Expires: Sun, 17-Jan-2038 19:14:07 GMT' ]
352 );
353 }
354
362 private function outputContents( $content, $contentType ) {
363 $size = strlen( $content );
364 if ( $size > self::MAX_SERVE_BYTES ) {
366 $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
367 );
368 }
369 // Cancel output buffering and gzipping if set
371 self::outputFileHeaders( $contentType, $size );
372 print $content;
373 }
374
384 private static function outputFileHeaders( $contentType, $size ) {
385 header( "Content-Type: $contentType", true );
386 header( 'Content-Transfer-Encoding: binary', true );
387 header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true );
388 // T55032 - It shouldn't be a problem here, but let's be safe and not cache
389 header( 'Cache-Control: private' );
390 header( "Content-Length: $size", true );
391 }
392
397 private function showUploads() {
398 // sets the title, etc.
399 $this->setHeaders();
400 $this->outputHeader();
401
402 // create the form, which will also be used to execute a callback to process incoming form data
403 // this design is extremely dubious, but supposedly HTMLForm is our standard now?
404
405 $form = HTMLForm::factory( 'ooui', [
406 'Clear' => [
407 'type' => 'hidden',
408 'default' => true,
409 'name' => 'clear',
410 ]
411 ], $this->getContext(), 'clearStashedUploads' );
412 $form->setTitle( $this->getPageTitle() ); // Remove subpage
413 $form->setSubmitDestructive();
414 $form->setSubmitCallback( function ( $formData, $form ) {
415 if ( isset( $formData['Clear'] ) ) {
416 wfDebug( 'stash has: ' . print_r( $this->stash->listFiles(), true ) );
417
418 if ( !$this->stash->clear() ) {
419 return Status::newFatal( 'uploadstash-errclear' );
420 }
421 }
422
423 return Status::newGood();
424 } );
425 $form->setSubmitTextMsg( 'uploadstash-clear' );
426
427 $form->prepareForm();
428 $formResult = $form->tryAuthorizedSubmit();
429
430 // show the files + form, if there are any, or just say there are none
431 $linkRenderer = $this->getLinkRenderer();
432 $refreshHtml = $linkRenderer->makeKnownLink(
433 $this->getPageTitle(),
434 $this->msg( 'uploadstash-refresh' )->text()
435 );
436 $pager = new UploadStashPager(
437 $this->getContext(),
438 $linkRenderer,
439 $this->dbProvider,
440 $this->stash,
441 $this->localRepo
442 );
443 if ( $pager->getNumRows() ) {
444 $pager->getForm();
445 $this->getOutput()->addParserOutputContent( $pager->getFullOutput() );
446 $form->displayForm( $formResult );
447 $this->getOutput()->addHTML( Html::rawElement( 'p', [], $refreshHtml ) );
448 } else {
449 $this->getOutput()->addHTML( Html::rawElement( 'p', [],
450 Html::element( 'span', [], $this->msg( 'uploadstash-nofiles' )->text() )
451 . ' '
452 . $refreshHtml
453 ) );
454 }
455 }
456}
457
462class_alias( SpecialUploadStash::class, 'SpecialUploadStash' );
const PROTO_CANONICAL
Definition Defines.php:208
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:73
getPath()
Return the storage path to the file.
Definition File.php:469
getMimeType()
Returns the MIME type of the file.
Definition File.php:843
getSize()
Return the size of the image file, in bytes Overridden by LocalFile, UnregisteredLocalFile STUB.
Definition File.php:831
getName()
Return the name of this file.
Definition File.php:341
getRepo()
Returns the repository.
Definition File.php:2050
transform( $params, $flags=0)
Transform a media file.
Definition File.php:1177
getHandler()
Get a MediaHandler instance for this file.
Definition File.php:1548
getUrlRel()
Get urlencoded path of the file relative to the public zone root.
Definition File.php:1755
generateThumbName( $name, $params)
Generate a thumbnail file name from a name and specified parameters.
Definition File.php:1094
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:49
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:206
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 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, 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:30
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)
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...