MediaWiki master
SpecialUploadStash.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Specials;
8
9use Exception;
19use MediaWiki\Pager\UploadStashPager;
29
45 private $stash;
46
47 private LocalRepo $localRepo;
48 private HttpRequestFactory $httpRequestFactory;
49 private UrlUtils $urlUtils;
50 private IConnectionProvider $dbProvider;
51
62 private const MAX_SERVE_BYTES = 1_048_576; // 1 MiB
63
64 public function __construct(
65 RepoGroup $repoGroup,
66 HttpRequestFactory $httpRequestFactory,
67 UrlUtils $urlUtils,
68 IConnectionProvider $dbProvider
69 ) {
70 parent::__construct( 'UploadStash', 'upload' );
71 $this->localRepo = $repoGroup->getLocalRepo();
72 $this->httpRequestFactory = $httpRequestFactory;
73 $this->urlUtils = $urlUtils;
74 $this->dbProvider = $dbProvider;
75 }
76
78 public function doesWrites() {
79 return true;
80 }
81
88 public function execute( $subPage ) {
90
91 // This is not set in constructor, because the context with the user is not safe to be set
92 $this->stash = $this->localRepo->getUploadStash( $this->getUser() );
93 $this->checkPermissions();
94
95 if ( $subPage === null || $subPage === '' ) {
96 $this->showUploads();
97 } else {
98 $this->showUpload( $subPage );
99 }
100 }
101
109 public function showUpload( $key ) {
110 // prevent callers from doing standard HTML output -- we'll take it from here
111 $this->getOutput()->disable();
112
113 try {
114 $params = $this->parseKey( $key );
115 if ( $params['type'] === 'thumb' ) {
116 $this->outputThumbFromStash( $params['file'], $params['params'] );
117 } else {
118 $this->outputLocalFile( $params['file'] );
119 }
120 return;
121 } catch ( UploadStashFileNotFoundException $e ) {
122 $code = 404;
123 $message = $e->getMessage();
124 } catch ( Exception $e ) {
125 $code = 500;
126 $message = $e->getMessage();
127 }
128
129 throw new HttpError( $code, $message );
130 }
131
141 private function parseKey( $key ) {
142 $type = strtok( $key, '/' );
143
144 if ( $type !== 'file' && $type !== 'thumb' ) {
146 $this->msg( 'uploadstash-bad-path-unknown-type', $type )
147 );
148 }
149 $fileName = strtok( '/' );
150 $thumbPart = strtok( '/' );
151 $file = $this->stash->getFile( $fileName );
152 if ( $type === 'thumb' ) {
153 $srcNamePos = strrpos( $thumbPart, $fileName );
154 if ( $srcNamePos === false || $srcNamePos < 1 ) {
155 throw new UploadStashBadPathException(
156 $this->msg( 'uploadstash-bad-path-unrecognized-thumb-name' )
157 );
158 }
159 $paramString = substr( $thumbPart, 0, $srcNamePos - 1 );
160
161 $handler = $file->getHandler();
162 if ( $handler ) {
163 $params = $handler->parseParamString( $paramString );
164 if ( $params === false ) {
165 // The params are invalid, but still try to show a thumb
166 $params = [];
167 }
168
169 return [ 'file' => $file, 'type' => $type, 'params' => $params ];
170 } else {
171 throw new UploadStashBadPathException(
172 $this->msg( 'uploadstash-bad-path-no-handler', $file->getMimeType(), $file->getPath() )
173 );
174 }
175 }
176
177 return [ 'file' => $file, 'type' => $type ];
178 }
179
186 private function outputThumbFromStash( $file, $params ) {
187 // this config option, if it exists, points to a "scaler", as you might find in
188 // the Wikimedia Foundation cluster. See outputRemoteScaledThumb(). This
189 // is part of our horrible NFS-based system, we create a file on a mount
190 // point here, but fetch the scaled file from somewhere else that
191 // happens to share it over NFS.
192 if ( $file->getRepo()->getThumbProxyUrl()
193 || $this->getConfig()->get( MainConfigNames::UploadStashScalerBaseUrl )
194 ) {
195 $this->outputRemoteScaledThumb( $file, $params );
196 } else {
197 $this->outputLocallyScaledThumb( $file, $params );
198 }
199 }
200
207 private function outputLocallyScaledThumb( $file, $params ) {
208 // n.b. this is stupid, we insist on re-transforming the file every time we are invoked. We rely
209 // on HTTP caching to ensure this doesn't happen.
210
211 $thumbnailImage = $file->transform( $params, File::RENDER_NOW );
212 if ( !$thumbnailImage ) {
213 throw new UploadStashFileNotFoundException(
214 $this->msg( 'uploadstash-file-not-found-no-thumb' )
215 );
216 }
217
218 // we should have just generated it locally
219 if ( !$thumbnailImage->getStoragePath() ) {
220 throw new UploadStashFileNotFoundException(
221 $this->msg( 'uploadstash-file-not-found-no-local-path' )
222 );
223 }
224
225 // now we should construct a File, so we can get MIME and other such info in a standard way
226 // n.b. MIME type may be different from original (ogx original -> jpeg thumb)
227 $thumbFile = new UnregisteredLocalFile( false,
228 $this->stash->repo, $thumbnailImage->getStoragePath(), false );
229
230 $this->outputLocalFile( $thumbFile );
231 }
232
249 private function outputRemoteScaledThumb( $file, $params ) {
250 // We need to use generateThumbName() instead of thumbName(), because
251 // the suffix needs to match the file name for the remote thumbnailer
252 // to work
253 $scalerThumbName = $file->generateThumbName( $file->getName(), $params );
254
255 // If a thumb proxy is set up for the repo, we favor that, as that will
256 // keep the request internal
257 $thumbProxyUrl = $file->getRepo()->getThumbProxyUrl();
258 if ( $thumbProxyUrl !== null ) {
259 $scalerThumbUrl = $thumbProxyUrl . 'temp/' . $file->getUrlRel() .
260 '/' . rawurlencode( $scalerThumbName );
261 $secret = $file->getRepo()->getThumbProxySecret();
262 } else {
263 // This option probably looks something like
264 // '//upload.wikimedia.org/wikipedia/test/thumb/temp'. Do not use
265 // trailing slash.
266 $scalerBaseUrl = $this->getConfig()->get( MainConfigNames::UploadStashScalerBaseUrl );
267
268 if ( preg_match( '/^\/\//', $scalerBaseUrl ) ) {
269 // this is apparently a protocol-relative URL, which makes no sense in this context,
270 // since this is used for communication that's internal to the application.
271 // default to http.
272 $scalerBaseUrl = $this->urlUtils->expand( $scalerBaseUrl, PROTO_CANONICAL );
273 }
274
275 $scalerThumbUrl = $scalerBaseUrl . '/' . $file->getUrlRel() .
276 '/' . rawurlencode( $scalerThumbName );
277 $secret = null;
278 }
279
280 // make an http request based on wgUploadStashScalerBaseUrl to lazy-create
281 // a thumbnail
282 $httpOptions = [
283 'method' => 'GET',
284 'timeout' => 5 // T90599 attempt to time out cleanly
285 ];
286 $req = $this->httpRequestFactory->create( $scalerThumbUrl, $httpOptions, __METHOD__ );
287
288 // Pass a secret key shared with the proxied service if any
289 if ( $secret !== null ) {
290 $req->setHeader( 'X-Swift-Secret', $secret );
291 }
292
293 $status = $req->execute();
294 if ( !$status->isOK() ) {
295 throw new UploadStashFileNotFoundException(
296 $this->msg(
297 'uploadstash-file-not-found-no-remote-thumb',
298 $status->getMessage(),
299 $scalerThumbUrl
300 )
301 );
302 }
303 $contentType = $req->getResponseHeader( "content-type" );
304 if ( !$contentType ) {
305 throw new UploadStashFileNotFoundException(
306 $this->msg( 'uploadstash-file-not-found-missing-content-type' )
307 );
308 }
309
310 $this->outputContents( $req->getContent(), $contentType );
311 }
312
321 private function outputLocalFile( File $file ) {
322 if ( $file->getSize() > self::MAX_SERVE_BYTES ) {
324 $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
325 );
326 }
327
328 $file->getRepo()->streamFileWithStatus( $file->getPath(),
329 [ 'Content-Transfer-Encoding: binary',
330 'Expires: Sun, 17-Jan-2038 19:14:07 GMT' ]
331 );
332 }
333
341 private function outputContents( $content, $contentType ) {
342 $size = strlen( $content );
343 if ( $size > self::MAX_SERVE_BYTES ) {
345 $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
346 );
347 }
348 // Cancel output buffering and gzipping if set
350 self::outputFileHeaders( $contentType, $size );
351 print $content;
352 }
353
363 private static function outputFileHeaders( $contentType, $size ) {
364 header( "Content-Type: $contentType", true );
365 header( 'Content-Transfer-Encoding: binary', true );
366 header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true );
367 // T55032 - It shouldn't be a problem here, but let's be safe and not cache
368 header( 'Cache-Control: private' );
369 header( "Content-Length: $size", true );
370 }
371
376 private function showUploads() {
377 // sets the title, etc.
378 $this->setHeaders();
379 $this->outputHeader();
380 $this->getOutput()->addModuleStyles( 'mediawiki.special' );
381
382 // create the form, which will also be used to execute a callback to process incoming form data
383 // this design is extremely dubious, but supposedly HTMLForm is our standard now?
384
385 $form = HTMLForm::factory( 'ooui', [
386 'Clear' => [
387 'type' => 'hidden',
388 'default' => true,
389 'name' => 'clear',
390 ]
391 ], $this->getContext(), 'clearStashedUploads' );
392 $form->setTitle( $this->getPageTitle() ); // Remove subpage
393 $form->setSubmitDestructive();
394 $form->setSubmitCallback( function ( $formData, $form ) {
395 if ( isset( $formData['Clear'] ) ) {
396 wfDebug( 'stash has: ' . print_r( $this->stash->listFiles(), true ) );
397
398 if ( !$this->stash->clear() ) {
399 return Status::newFatal( 'uploadstash-errclear' );
400 }
401 }
402
403 return Status::newGood();
404 } );
405 $form->setSubmitTextMsg( 'uploadstash-clear' );
406
407 $form->prepareForm();
408 $formResult = $form->tryAuthorizedSubmit();
409
410 // show the files + form, if there are any, or just say there are none
411 $linkRenderer = $this->getLinkRenderer();
412 $refreshHtml = $linkRenderer->makeKnownLink(
413 $this->getPageTitle(),
414 $this->msg( 'uploadstash-refresh' )->text()
415 );
416 $pager = new UploadStashPager(
417 $this->getContext(),
418 $linkRenderer,
419 $this->dbProvider,
420 $this->stash,
421 $this->localRepo
422 );
423 if ( $pager->getNumRows() ) {
424 $pager->getForm();
425 $this->getOutput()->addParserOutputContent(
426 $pager->getFullOutput(),
427 ParserOptions::newFromContext( $this->getContext() )
428 );
429 $form->displayForm( $formResult );
430 $this->getOutput()->addHTML( Html::rawElement( 'p', [], $refreshHtml ) );
431 } else {
432 $this->getOutput()->addHTML( Html::rawElement( 'p', [],
433 Html::element( 'span', [], $this->msg( 'uploadstash-nofiles' )->text() )
434 . ' '
435 . $refreshHtml
436 ) );
437 }
438 }
439}
440
445class_alias( SpecialUploadStash::class, 'SpecialUploadStash' );
const PROTO_CANONICAL
Definition Defines.php:223
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:23
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:79
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:45
Prioritized list of file repositories.
Definition RepoGroup.php:30
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:195
This class is a collection of static functions that serve two purposes:
Definition Html.php:43
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:44
UploadStash is intended to accomplish a few things:
A service to expand, parse, and otherwise manipulate URLs.
Definition UrlUtils.php:16
Provide primary and replica IDatabase connections.
element(SerializerNode $parent, SerializerNode $node, $contents)
array $params
The job parameters.