MediaWiki master
SpecialUploadStash.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Specials;
22
23use Exception;
39use UploadStash;
43
59 private $stash;
60
61 private LocalRepo $localRepo;
62 private HttpRequestFactory $httpRequestFactory;
63 private UrlUtils $urlUtils;
64 private IConnectionProvider $dbProvider;
65
76 private const MAX_SERVE_BYTES = 1_048_576; // 1 MiB
77
78 public function __construct(
79 RepoGroup $repoGroup,
80 HttpRequestFactory $httpRequestFactory,
81 UrlUtils $urlUtils,
82 IConnectionProvider $dbProvider
83 ) {
84 parent::__construct( 'UploadStash', 'upload' );
85 $this->localRepo = $repoGroup->getLocalRepo();
86 $this->httpRequestFactory = $httpRequestFactory;
87 $this->urlUtils = $urlUtils;
88 $this->dbProvider = $dbProvider;
89 }
90
91 public function doesWrites() {
92 return true;
93 }
94
101 public function execute( $subPage ) {
103
104 // This is not set in constructor, because the context with the user is not safe to be set
105 $this->stash = $this->localRepo->getUploadStash( $this->getUser() );
106 $this->checkPermissions();
107
108 if ( $subPage === null || $subPage === '' ) {
109 $this->showUploads();
110 } else {
111 $this->showUpload( $subPage );
112 }
113 }
114
122 public function showUpload( $key ) {
123 // prevent callers from doing standard HTML output -- we'll take it from here
124 $this->getOutput()->disable();
125
126 try {
127 $params = $this->parseKey( $key );
128 if ( $params['type'] === 'thumb' ) {
129 $this->outputThumbFromStash( $params['file'], $params['params'] );
130 } else {
131 $this->outputLocalFile( $params['file'] );
132 }
133 return;
134 } catch ( UploadStashFileNotFoundException $e ) {
135 $code = 404;
136 $message = $e->getMessage();
137 } catch ( Exception $e ) {
138 $code = 500;
139 $message = $e->getMessage();
140 }
141
142 throw new HttpError( $code, $message );
143 }
144
154 private function parseKey( $key ) {
155 $type = strtok( $key, '/' );
156
157 if ( $type !== 'file' && $type !== 'thumb' ) {
159 $this->msg( 'uploadstash-bad-path-unknown-type', $type )
160 );
161 }
162 $fileName = strtok( '/' );
163 $thumbPart = strtok( '/' );
164 $file = $this->stash->getFile( $fileName );
165 if ( $type === 'thumb' ) {
166 $srcNamePos = strrpos( $thumbPart, $fileName );
167 if ( $srcNamePos === false || $srcNamePos < 1 ) {
169 $this->msg( 'uploadstash-bad-path-unrecognized-thumb-name' )
170 );
171 }
172 $paramString = substr( $thumbPart, 0, $srcNamePos - 1 );
173
174 $handler = $file->getHandler();
175 if ( $handler ) {
176 $params = $handler->parseParamString( $paramString );
177 if ( $params === false ) {
178 // The params are invalid, but still try to show a thumb
179 $params = [];
180 }
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
220 private function outputLocallyScaledThumb( $file, $params ) {
221 // n.b. this is stupid, we insist on re-transforming the file every time we are invoked. We rely
222 // on HTTP caching to ensure this doesn't happen.
223
224 $thumbnailImage = $file->transform( $params, File::RENDER_NOW );
225 if ( !$thumbnailImage ) {
227 $this->msg( 'uploadstash-file-not-found-no-thumb' )
228 );
229 }
230
231 // we should have just generated it locally
232 if ( !$thumbnailImage->getStoragePath() ) {
234 $this->msg( 'uploadstash-file-not-found-no-local-path' )
235 );
236 }
237
238 // now we should construct a File, so we can get MIME and other such info in a standard way
239 // n.b. MIME type may be different from original (ogx original -> jpeg thumb)
240 $thumbFile = new UnregisteredLocalFile( false,
241 $this->stash->repo, $thumbnailImage->getStoragePath(), false );
242
243 $this->outputLocalFile( $thumbFile );
244 }
245
262 private function outputRemoteScaledThumb( $file, $params ) {
263 // We need to use generateThumbName() instead of thumbName(), because
264 // the suffix needs to match the file name for the remote thumbnailer
265 // to work
266 $scalerThumbName = $file->generateThumbName( $file->getName(), $params );
267
268 // If a thumb proxy is set up for the repo, we favor that, as that will
269 // keep the request internal
270 $thumbProxyUrl = $file->getRepo()->getThumbProxyUrl();
271 if ( $thumbProxyUrl !== null ) {
272 $scalerThumbUrl = $thumbProxyUrl . 'temp/' . $file->getUrlRel() .
273 '/' . rawurlencode( $scalerThumbName );
274 $secret = $file->getRepo()->getThumbProxySecret();
275 } else {
276 // This option probably looks something like
277 // '//upload.wikimedia.org/wikipedia/test/thumb/temp'. Do not use
278 // trailing slash.
279 $scalerBaseUrl = $this->getConfig()->get( MainConfigNames::UploadStashScalerBaseUrl );
280
281 if ( preg_match( '/^\/\//', $scalerBaseUrl ) ) {
282 // this is apparently a protocol-relative URL, which makes no sense in this context,
283 // since this is used for communication that's internal to the application.
284 // default to http.
285 $scalerBaseUrl = $this->urlUtils->expand( $scalerBaseUrl, PROTO_CANONICAL );
286 }
287
288 $scalerThumbUrl = $scalerBaseUrl . '/' . $file->getUrlRel() .
289 '/' . rawurlencode( $scalerThumbName );
290 $secret = null;
291 }
292
293 // make an http request based on wgUploadStashScalerBaseUrl to lazy-create
294 // a thumbnail
295 $httpOptions = [
296 'method' => 'GET',
297 'timeout' => 5 // T90599 attempt to time out cleanly
298 ];
299 $req = $this->httpRequestFactory->create( $scalerThumbUrl, $httpOptions, __METHOD__ );
300
301 // Pass a secret key shared with the proxied service if any
302 if ( $secret !== null ) {
303 $req->setHeader( 'X-Swift-Secret', $secret );
304 }
305
306 $status = $req->execute();
307 if ( !$status->isOK() ) {
309 $this->msg(
310 'uploadstash-file-not-found-no-remote-thumb',
311 $status->getMessage(),
312 $scalerThumbUrl
313 )
314 );
315 }
316 $contentType = $req->getResponseHeader( "content-type" );
317 if ( !$contentType ) {
319 $this->msg( 'uploadstash-file-not-found-missing-content-type' )
320 );
321 }
322
323 $this->outputContents( $req->getContent(), $contentType );
324 }
325
334 private function outputLocalFile( File $file ) {
335 if ( $file->getSize() > self::MAX_SERVE_BYTES ) {
337 $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
338 );
339 }
340
341 $file->getRepo()->streamFileWithStatus( $file->getPath(),
342 [ 'Content-Transfer-Encoding: binary',
343 'Expires: Sun, 17-Jan-2038 19:14:07 GMT' ]
344 );
345 }
346
354 private function outputContents( $content, $contentType ) {
355 $size = strlen( $content );
356 if ( $size > self::MAX_SERVE_BYTES ) {
358 $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
359 );
360 }
361 // Cancel output buffering and gzipping if set
363 self::outputFileHeaders( $contentType, $size );
364 print $content;
365 }
366
376 private static function outputFileHeaders( $contentType, $size ) {
377 header( "Content-Type: $contentType", true );
378 header( 'Content-Transfer-Encoding: binary', true );
379 header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true );
380 // T55032 - It shouldn't be a problem here, but let's be safe and not cache
381 header( 'Cache-Control: private' );
382 header( "Content-Length: $size", true );
383 }
384
389 private function showUploads() {
390 // sets the title, etc.
391 $this->setHeaders();
392 $this->outputHeader();
393
394 // create the form, which will also be used to execute a callback to process incoming form data
395 // this design is extremely dubious, but supposedly HTMLForm is our standard now?
396
397 $form = HTMLForm::factory( 'ooui', [
398 'Clear' => [
399 'type' => 'hidden',
400 'default' => true,
401 'name' => 'clear',
402 ]
403 ], $this->getContext(), 'clearStashedUploads' );
404 $form->setTitle( $this->getPageTitle() ); // Remove subpage
405 $form->setSubmitDestructive();
406 $form->setSubmitCallback( function ( $formData, $form ) {
407 if ( isset( $formData['Clear'] ) ) {
408 wfDebug( 'stash has: ' . print_r( $this->stash->listFiles(), true ) );
409
410 if ( !$this->stash->clear() ) {
411 return Status::newFatal( 'uploadstash-errclear' );
412 }
413 }
414
415 return Status::newGood();
416 } );
417 $form->setSubmitTextMsg( 'uploadstash-clear' );
418
419 $form->prepareForm();
420 $formResult = $form->tryAuthorizedSubmit();
421
422 // show the files + form, if there are any, or just say there are none
423 $linkRenderer = $this->getLinkRenderer();
424 $refreshHtml = $linkRenderer->makeKnownLink(
425 $this->getPageTitle(),
426 $this->msg( 'uploadstash-refresh' )->text()
427 );
428 $pager = new UploadStashPager(
429 $this->getContext(),
430 $linkRenderer,
431 $this->dbProvider,
432 $this->stash,
433 $this->localRepo
434 );
435 if ( $pager->getNumRows() ) {
436 $pager->getForm();
437 $this->getOutput()->addParserOutputContent(
438 $pager->getFullOutput(),
439 ParserOptions::newFromContext( $this->getContext() )
440 );
441 $form->displayForm( $formResult );
442 $this->getOutput()->addHTML( Html::rawElement( 'p', [], $refreshHtml ) );
443 } else {
444 $this->getOutput()->addHTML( Html::rawElement( 'p', [],
445 Html::element( 'span', [], $this->msg( 'uploadstash-nofiles' )->text() )
446 . ' '
447 . $refreshHtml
448 ) );
449 }
450 }
451}
452
457class_alias( SpecialUploadStash::class, 'SpecialUploadStash' );
const PROTO_CANONICAL
Definition Defines.php:237
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.
Show an error that looks like an HTTP server error.
Definition HttpError.php:36
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:93
File without associated database record.
Local repository that stores files in the local filesystem and registers them in the wiki's own datab...
Definition LocalRepo.php:57
Prioritized list of file repositories.
Definition RepoGroup.php:38
getLocalRepo()
Get the local repository, i.e.
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:209
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()
Set options of the Parser.
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
UploadStash is intended to accomplish a few things:
Provide primary and replica IDatabase connections.
element(SerializerNode $parent, SerializerNode $node, $contents)
array $params
The job parameters.