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:74
getPath()
Return the storage path to the file.
Definition File.php:470
getMimeType()
Returns the MIME type of the file.
Definition File.php:846
getSize()
Return the size of the image file, in bytes Overridden by LocalFile, UnregisteredLocalFile STUB.
Definition File.php:834
getName()
Return the name of this file.
Definition File.php:342
getRepo()
Returns the repository.
Definition File.php:2051
transform( $params, $flags=0)
Transform a media file.
Definition File.php:1180
getHandler()
Get a MediaHandler instance for this file.
Definition File.php:1549
getUrlRel()
Get urlencoded path of the file relative to the public zone root.
Definition File.php:1756
generateThumbName( $name, $params)
Generate a thumbnail file name from a name and specified parameters.
Definition File.php:1097
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:48
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 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)