MediaWiki REL1_37
SpecialUploadStash.php
Go to the documentation of this file.
1<?php
24
40 private $stash;
41
43 private $localRepo;
44
47
58 private const MAX_SERVE_BYTES = 1048576; // 1 MiB
59
64 public function __construct(
65 RepoGroup $repoGroup,
67 ) {
68 parent::__construct( 'UploadStash', 'upload' );
69 $this->localRepo = $repoGroup->getLocalRepo();
70 $this->httpRequestFactory = $httpRequestFactory;
71 }
72
73 public function doesWrites() {
74 return true;
75 }
76
83 public function execute( $subPage ) {
85
86 // This is not set in constructor, because the context with the user is not safe to be set
87 $this->stash = $this->localRepo->getUploadStash( $this->getUser() );
88 $this->checkPermissions();
89
90 if ( $subPage === null || $subPage === '' ) {
91 $this->showUploads();
92 } else {
93 $this->showUpload( $subPage );
94 }
95 }
96
104 public function showUpload( $key ) {
105 // prevent callers from doing standard HTML output -- we'll take it from here
106 $this->getOutput()->disable();
107
108 try {
109 $params = $this->parseKey( $key );
110 if ( $params['type'] === 'thumb' ) {
111 $this->outputThumbFromStash( $params['file'], $params['params'] );
112 } else {
113 $this->outputLocalFile( $params['file'] );
114 }
115 return;
116 } catch ( UploadStashFileNotFoundException $e ) {
117 $code = 404;
118 $message = $e->getMessage();
119 } catch ( Exception $e ) {
120 $code = 500;
121 $message = $e->getMessage();
122 }
123
124 throw new HttpError( $code, $message );
125 }
126
136 private function parseKey( $key ) {
137 $type = strtok( $key, '/' );
138
139 if ( $type !== 'file' && $type !== 'thumb' ) {
141 $this->msg( 'uploadstash-bad-path-unknown-type', $type )
142 );
143 }
144 $fileName = strtok( '/' );
145 $thumbPart = strtok( '/' );
146 $file = $this->stash->getFile( $fileName );
147 if ( $type === 'thumb' ) {
148 $srcNamePos = strrpos( $thumbPart, $fileName );
149 if ( $srcNamePos === false || $srcNamePos < 1 ) {
151 $this->msg( 'uploadstash-bad-path-unrecognized-thumb-name' )
152 );
153 }
154 $paramString = substr( $thumbPart, 0, $srcNamePos - 1 );
155
156 $handler = $file->getHandler();
157 if ( $handler ) {
158 $params = $handler->parseParamString( $paramString );
159
160 return [ 'file' => $file, 'type' => $type, 'params' => $params ];
161 } else {
163 $this->msg( 'uploadstash-bad-path-no-handler', $file->getMimeType(), $file->getPath() )
164 );
165 }
166 }
167
168 return [ 'file' => $file, 'type' => $type ];
169 }
170
177 private function outputThumbFromStash( $file, $params ) {
178 $flags = 0;
179 // this config option, if it exists, points to a "scaler", as you might find in
180 // the Wikimedia Foundation cluster. See outputRemoteScaledThumb(). This
181 // is part of our horrible NFS-based system, we create a file on a mount
182 // point here, but fetch the scaled file from somewhere else that
183 // happens to share it over NFS.
184 if ( $file->getRepo()->getThumbProxyUrl()
185 || $this->getConfig()->get( 'UploadStashScalerBaseUrl' )
186 ) {
187 $this->outputRemoteScaledThumb( $file, $params, $flags );
188 } else {
189 $this->outputLocallyScaledThumb( $file, $params, $flags );
190 }
191 }
192
201 private function outputLocallyScaledThumb( $file, $params, $flags ) {
202 // n.b. this is stupid, we insist on re-transforming the file every time we are invoked. We rely
203 // on HTTP caching to ensure this doesn't happen.
204
205 $flags |= File::RENDER_NOW;
206
207 $thumbnailImage = $file->transform( $params, $flags );
208 if ( !$thumbnailImage ) {
210 $this->msg( 'uploadstash-file-not-found-no-thumb' )
211 );
212 }
213
214 // we should have just generated it locally
215 if ( !$thumbnailImage->getStoragePath() ) {
217 $this->msg( 'uploadstash-file-not-found-no-local-path' )
218 );
219 }
220
221 // now we should construct a File, so we can get MIME and other such info in a standard way
222 // n.b. MIME type may be different from original (ogx original -> jpeg thumb)
223 $thumbFile = new UnregisteredLocalFile( false,
224 $this->stash->repo, $thumbnailImage->getStoragePath(), false );
225
226 $this->outputLocalFile( $thumbFile );
227 }
228
247 private function outputRemoteScaledThumb( $file, $params, $flags ) {
248 // We need to use generateThumbName() instead of thumbName(), because
249 // the suffix needs to match the file name for the remote thumbnailer
250 // to work
251 $scalerThumbName = $file->generateThumbName( $file->getName(), $params );
252
253 // If a thumb proxy is set up for the repo, we favor that, as that will
254 // keep the request internal
255 $thumbProxyUrl = $file->getRepo()->getThumbProxyUrl();
256 if ( strlen( $thumbProxyUrl ) ) {
257 $scalerThumbUrl = $thumbProxyUrl . 'temp/' . $file->getUrlRel() .
258 '/' . rawurlencode( $scalerThumbName );
259 $secret = $file->getRepo()->getThumbProxySecret();
260 } else {
261 // This option probably looks something like
262 // '//upload.wikimedia.org/wikipedia/test/thumb/temp'. Do not use
263 // trailing slash.
264 $scalerBaseUrl = $this->getConfig()->get( 'UploadStashScalerBaseUrl' );
265
266 if ( preg_match( '/^\/\//', $scalerBaseUrl ) ) {
267 // this is apparently a protocol-relative URL, which makes no sense in this context,
268 // since this is used for communication that's internal to the application.
269 // default to http.
270 $scalerBaseUrl = wfExpandUrl( $scalerBaseUrl, PROTO_CANONICAL );
271 }
272
273 $scalerThumbUrl = $scalerBaseUrl . '/' . $file->getUrlRel() .
274 '/' . rawurlencode( $scalerThumbName );
275 $secret = false;
276 }
277
278 // make an http request based on wgUploadStashScalerBaseUrl to lazy-create
279 // a thumbnail
280 $httpOptions = [
281 'method' => 'GET',
282 'timeout' => 5 // T90599 attempt to time out cleanly
283 ];
284 $req = $this->httpRequestFactory->create( $scalerThumbUrl, $httpOptions, __METHOD__ );
285
286 // Pass a secret key shared with the proxied service if any
287 if ( strlen( $secret ) ) {
288 $req->setHeader( 'X-Swift-Secret', $secret );
289 }
290
291 $status = $req->execute();
292 if ( !$status->isOK() ) {
293 $errors = $status->getErrorsArray();
295 $this->msg(
296 'uploadstash-file-not-found-no-remote-thumb',
297 print_r( $errors, 1 ),
298 $scalerThumbUrl
299 )
300 );
301 }
302 $contentType = $req->getResponseHeader( "content-type" );
303 if ( !$contentType ) {
305 $this->msg( 'uploadstash-file-not-found-missing-content-type' )
306 );
307 }
308
309 $this->outputContents( $req->getContent(), $contentType );
310 }
311
320 private function outputLocalFile( File $file ) {
321 if ( $file->getSize() > self::MAX_SERVE_BYTES ) {
323 $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
324 );
325 }
326
327 $file->getRepo()->streamFileWithStatus( $file->getPath(),
328 [ 'Content-Transfer-Encoding: binary',
329 'Expires: Sun, 17-Jan-2038 19:14:07 GMT' ]
330 );
331 }
332
340 private function outputContents( $content, $contentType ) {
341 $size = strlen( $content );
342 if ( $size > self::MAX_SERVE_BYTES ) {
344 $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
345 );
346 }
347 // Cancel output buffering and gzipping if set
349 self::outputFileHeaders( $contentType, $size );
351 }
352
362 private static function outputFileHeaders( $contentType, $size ) {
363 header( "Content-Type: $contentType", true );
364 header( 'Content-Transfer-Encoding: binary', true );
365 header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true );
366 // T55032 - It shouldn't be a problem here, but let's be safe and not cache
367 header( 'Cache-Control: private' );
368 header( "Content-Length: $size", true );
369 }
370
375 private function showUploads() {
376 // sets the title, etc.
377 $this->setHeaders();
378 $this->outputHeader();
379
380 // create the form, which will also be used to execute a callback to process incoming form data
381 // this design is extremely dubious, but supposedly HTMLForm is our standard now?
382
383 $context = new DerivativeContext( $this->getContext() );
384 $context->setTitle( $this->getPageTitle() ); // Remove subpage
385 $form = HTMLForm::factory( 'ooui', [
386 'Clear' => [
387 'type' => 'hidden',
388 'default' => true,
389 'name' => 'clear',
390 ]
391 ], $context, 'clearStashedUploads' );
392 $form->setSubmitDestructive();
393 $form->setSubmitCallback( function ( $formData, $form ) {
394 if ( isset( $formData['Clear'] ) ) {
395 wfDebug( 'stash has: ' . print_r( $this->stash->listFiles(), true ) );
396
397 if ( !$this->stash->clear() ) {
398 return Status::newFatal( 'uploadstash-errclear' );
399 }
400 }
401
402 return Status::newGood();
403 } );
404 $form->setSubmitTextMsg( 'uploadstash-clear' );
405
406 $form->prepareForm();
407 $formResult = $form->tryAuthorizedSubmit();
408
409 // show the files + form, if there are any, or just say there are none
410 $refreshHtml = Html::element( 'a',
411 [ 'href' => $this->getPageTitle()->getLocalURL() ],
412 $this->msg( 'uploadstash-refresh' )->text() );
413 $files = $this->stash->listFiles();
414 if ( $files && count( $files ) ) {
415 sort( $files );
416 $fileListItemsHtml = '';
418 foreach ( $files as $file ) {
419 $itemHtml = $linkRenderer->makeKnownLink(
420 $this->getPageTitle( "file/$file" ),
421 $file
422 );
423 try {
424 $fileObj = $this->stash->getFile( $file );
425 $thumb = $fileObj->generateThumbName( $file, [ 'width' => 220 ] );
426 $itemHtml .=
427 $this->msg( 'word-separator' )->escaped() .
428 $this->msg( 'parentheses' )->rawParams(
430 $this->getPageTitle( "thumb/$file/$thumb" ),
431 $this->msg( 'uploadstash-thumbnail' )->text()
432 )
433 )->escaped();
434 } catch ( Exception $e ) {
435 }
436 $fileListItemsHtml .= Html::rawElement( 'li', [], $itemHtml );
437 }
438 $this->getOutput()->addHTML( Html::rawElement( 'ul', [], $fileListItemsHtml ) );
439 $form->displayForm( $formResult );
440 $this->getOutput()->addHTML( Html::rawElement( 'p', [], $refreshHtml ) );
441 } else {
442 $this->getOutput()->addHTML( Html::rawElement( 'p', [],
443 Html::element( 'span', [], $this->msg( 'uploadstash-nofiles' )->text() )
444 . ' '
445 . $refreshHtml
446 ) );
447 }
448 }
449}
const PROTO_CANONICAL
Definition Defines.php:196
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
wfResetOutputBuffers( $resetGzipEncoding=true)
Clear away any user-level output buffers, discarding contents.
An IContextSource implementation which will inherit context from another source but allow individual ...
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:66
Show an error that looks like an HTTP server error.
Definition HttpError.php:32
A repository that stores files in the local filesystem and registers them in the wiki's own database.
Definition LocalRepo.php:41
Factory creating MWHttpRequest objects.
makeKnownLink( $target, $text=null, array $extraAttribs=[], array $query=[])
Prioritized list of file repositories.
Definition RepoGroup.php:33
getLocalRepo()
Get the local repository, i.e.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getOutput()
Get the OutputPage being used for this instance.
getUser()
Shortcut to get the User executing this instance.
checkPermissions()
Checks if userCanExecute, and if not throws a PermissionsError.
getContext()
Gets the context this SpecialPage is executed in.
LinkRenderer null $linkRenderer
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getConfig()
Shortcut to get main config object.
getPageTitle( $subpage=false)
Get a self-referential title object.
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
Web access for files temporarily stored by UploadStash.
static outputFileHeaders( $contentType, $size)
Output headers for streaming.
outputLocalFile(File $file)
Output HTTP response for file Side effect: writes HTTP response to STDOUT.
outputThumbFromStash( $file, $params)
Get a thumbnail for file, either generated locally or remotely, and stream it out.
__construct(RepoGroup $repoGroup, HttpRequestFactory $httpRequestFactory)
showUploads()
Default action when we don't have a subpage – just show links to the uploads we have,...
parseKey( $key)
Parse the key passed to the SpecialPage.
showUpload( $key)
If file available in stash, cats it out to the client as a simple HTTP response.
outputRemoteScaledThumb( $file, $params, $flags)
Scale a file with a remote "scaler", as exists on the Wikimedia Foundation cluster,...
UploadStash null $stash
const MAX_SERVE_BYTES
Since we are directly writing the file to STDOUT, we should not be reading in really big files and se...
execute( $subPage)
Execute page – can output a file directly or show a listing of them.
doesWrites()
Indicates whether this special page may perform database writes.
outputLocallyScaledThumb( $file, $params, $flags)
Scale a file (probably with a locally installed imagemagick, or similar) and output it to STDOUT.
HttpRequestFactory $httpRequestFactory
outputContents( $content, $contentType)
Output HTTP response of raw content Side effect: writes HTTP response to STDOUT.
Shortcut to construct a special page which is unlisted by default.
A file object referring to either a standalone local file, or a file in a local repository with no da...
UploadStash is intended to accomplish a few things:
while(( $__line=Maintenance::readconsole()) !==false) print
Definition eval.php:69
$content
Definition router.php:76
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42