MediaWiki REL1_34
SpecialUploadStash.php
Go to the documentation of this file.
1<?php
37 // UploadStash
38 private $stash;
39
50 const MAX_SERVE_BYTES = 1048576; // 1MB
51
52 public function __construct() {
53 parent::__construct( 'UploadStash', 'upload' );
54 }
55
56 public function doesWrites() {
57 return true;
58 }
59
66 public function execute( $subPage ) {
68
69 $this->stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash( $this->getUser() );
70 $this->checkPermissions();
71
72 if ( $subPage === null || $subPage === '' ) {
73 $this->showUploads();
74 } else {
75 $this->showUpload( $subPage );
76 }
77 }
78
86 public function showUpload( $key ) {
87 // prevent callers from doing standard HTML output -- we'll take it from here
88 $this->getOutput()->disable();
89
90 try {
91 $params = $this->parseKey( $key );
92 if ( $params['type'] === 'thumb' ) {
93 $this->outputThumbFromStash( $params['file'], $params['params'] );
94 } else {
95 $this->outputLocalFile( $params['file'] );
96 }
97 return;
99 $code = 404;
100 $message = $e->getMessage();
102 $code = 500;
103 $message = $e->getMessage();
104 } catch ( UploadStashBadPathException $e ) {
105 $code = 500;
106 $message = $e->getMessage();
108 $code = 500;
109 $message = $e->getMessage();
110 } catch ( Exception $e ) {
111 $code = 500;
112 $message = $e->getMessage();
113 }
114
115 throw new HttpError( $code, $message );
116 }
117
127 private function parseKey( $key ) {
128 $type = strtok( $key, '/' );
129
130 if ( $type !== 'file' && $type !== 'thumb' ) {
132 $this->msg( 'uploadstash-bad-path-unknown-type', $type )
133 );
134 }
135 $fileName = strtok( '/' );
136 $thumbPart = strtok( '/' );
137 $file = $this->stash->getFile( $fileName );
138 if ( $type === 'thumb' ) {
139 $srcNamePos = strrpos( $thumbPart, $fileName );
140 if ( $srcNamePos === false || $srcNamePos < 1 ) {
142 $this->msg( 'uploadstash-bad-path-unrecognized-thumb-name' )
143 );
144 }
145 $paramString = substr( $thumbPart, 0, $srcNamePos - 1 );
146
147 $handler = $file->getHandler();
148 if ( $handler ) {
149 $params = $handler->parseParamString( $paramString );
150
151 return [ 'file' => $file, 'type' => $type, 'params' => $params ];
152 } else {
154 $this->msg( 'uploadstash-bad-path-no-handler', $file->getMimeType(), $file->getPath() )
155 );
156 }
157 }
158
159 return [ 'file' => $file, 'type' => $type ];
160 }
161
168 private function outputThumbFromStash( $file, $params ) {
169 $flags = 0;
170 // this config option, if it exists, points to a "scaler", as you might find in
171 // the Wikimedia Foundation cluster. See outputRemoteScaledThumb(). This
172 // is part of our horrible NFS-based system, we create a file on a mount
173 // point here, but fetch the scaled file from somewhere else that
174 // happens to share it over NFS.
175 if ( $this->getConfig()->get( 'UploadStashScalerBaseUrl' ) ) {
176 $this->outputRemoteScaledThumb( $file, $params, $flags );
177 } else {
178 $this->outputLocallyScaledThumb( $file, $params, $flags );
179 }
180 }
181
190 private function outputLocallyScaledThumb( $file, $params, $flags ) {
191 // n.b. this is stupid, we insist on re-transforming the file every time we are invoked. We rely
192 // on HTTP caching to ensure this doesn't happen.
193
194 $flags |= File::RENDER_NOW;
195
196 $thumbnailImage = $file->transform( $params, $flags );
197 if ( !$thumbnailImage ) {
199 $this->msg( 'uploadstash-file-not-found-no-thumb' )
200 );
201 }
202
203 // we should have just generated it locally
204 if ( !$thumbnailImage->getStoragePath() ) {
206 $this->msg( 'uploadstash-file-not-found-no-local-path' )
207 );
208 }
209
210 // now we should construct a File, so we can get MIME and other such info in a standard way
211 // n.b. MIME type may be different from original (ogx original -> jpeg thumb)
212 $thumbFile = new UnregisteredLocalFile( false,
213 $this->stash->repo, $thumbnailImage->getStoragePath(), false );
214 if ( !$thumbFile ) {
216 $this->msg( 'uploadstash-file-not-found-no-object' )
217 );
218 }
219
220 $this->outputLocalFile( $thumbFile );
221 }
222
241 private function outputRemoteScaledThumb( $file, $params, $flags ) {
242 // This option probably looks something like
243 // '//upload.wikimedia.org/wikipedia/test/thumb/temp'. Do not use
244 // trailing slash.
245 $scalerBaseUrl = $this->getConfig()->get( 'UploadStashScalerBaseUrl' );
246
247 if ( preg_match( '/^\/\//', $scalerBaseUrl ) ) {
248 // this is apparently a protocol-relative URL, which makes no sense in this context,
249 // since this is used for communication that's internal to the application.
250 // default to http.
251 $scalerBaseUrl = wfExpandUrl( $scalerBaseUrl, PROTO_CANONICAL );
252 }
253
254 // We need to use generateThumbName() instead of thumbName(), because
255 // the suffix needs to match the file name for the remote thumbnailer
256 // to work
257 $scalerThumbName = $file->generateThumbName( $file->getName(), $params );
258 $scalerThumbUrl = $scalerBaseUrl . '/' . $file->getUrlRel() .
259 '/' . rawurlencode( $scalerThumbName );
260
261 // If a thumb proxy is set up for the repo, we favor that, as that will
262 // keep the request internal
263 $thumbProxyUrl = $file->getRepo()->getThumbProxyUrl();
264
265 if ( strlen( $thumbProxyUrl ) ) {
266 $scalerThumbUrl = $thumbProxyUrl . 'temp/' . $file->getUrlRel() .
267 '/' . rawurlencode( $scalerThumbName );
268 }
269
270 // make an http request based on wgUploadStashScalerBaseUrl to lazy-create
271 // a thumbnail
272 $httpOptions = [
273 'method' => 'GET',
274 'timeout' => 5 // T90599 attempt to time out cleanly
275 ];
276 $req = MWHttpRequest::factory( $scalerThumbUrl, $httpOptions, __METHOD__ );
277
278 $secret = $file->getRepo()->getThumbProxySecret();
279
280 // Pass a secret key shared with the proxied service if any
281 if ( strlen( $secret ) ) {
282 $req->setHeader( 'X-Swift-Secret', $secret );
283 }
284
285 $status = $req->execute();
286 if ( !$status->isOK() ) {
287 $errors = $status->getErrorsArray();
289 $this->msg(
290 'uploadstash-file-not-found-no-remote-thumb',
291 print_r( $errors, 1 ),
292 $scalerThumbUrl
293 )
294 );
295 }
296 $contentType = $req->getResponseHeader( "content-type" );
297 if ( !$contentType ) {
299 $this->msg( 'uploadstash-file-not-found-missing-content-type' )
300 );
301 }
302
303 $this->outputContents( $req->getContent(), $contentType );
304 }
305
314 private function outputLocalFile( File $file ) {
315 if ( $file->getSize() > self::MAX_SERVE_BYTES ) {
317 $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
318 );
319 }
320
321 $file->getRepo()->streamFileWithStatus( $file->getPath(),
322 [ 'Content-Transfer-Encoding: binary',
323 'Expires: Sun, 17-Jan-2038 19:14:07 GMT' ]
324 );
325 }
326
334 private function outputContents( $content, $contentType ) {
335 $size = strlen( $content );
336 if ( $size > self::MAX_SERVE_BYTES ) {
338 $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
339 );
340 }
341 // Cancel output buffering and gzipping if set
343 self::outputFileHeaders( $contentType, $size );
345 }
346
356 private static function outputFileHeaders( $contentType, $size ) {
357 header( "Content-Type: $contentType", true );
358 header( 'Content-Transfer-Encoding: binary', true );
359 header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true );
360 // T55032 - It shouldn't be a problem here, but let's be safe and not cache
361 header( 'Cache-Control: private' );
362 header( "Content-Length: $size", true );
363 }
364
374 public static function tryClearStashedUploads( $formData, $form ) {
375 if ( isset( $formData['Clear'] ) ) {
376 $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash( $form->getUser() );
377 wfDebug( 'stash has: ' . print_r( $stash->listFiles(), true ) . "\n" );
378
379 if ( !$stash->clear() ) {
380 return Status::newFatal( 'uploadstash-errclear' );
381 }
382 }
383
384 return Status::newGood();
385 }
386
391 private function showUploads() {
392 // sets the title, etc.
393 $this->setHeaders();
394 $this->outputHeader();
395
396 // create the form, which will also be used to execute a callback to process incoming form data
397 // this design is extremely dubious, but supposedly HTMLForm is our standard now?
398
399 $context = new DerivativeContext( $this->getContext() );
400 $context->setTitle( $this->getPageTitle() ); // Remove subpage
401 $form = HTMLForm::factory( 'ooui', [
402 'Clear' => [
403 'type' => 'hidden',
404 'default' => true,
405 'name' => 'clear',
406 ]
407 ], $context, 'clearStashedUploads' );
408 $form->setSubmitDestructive();
409 $form->setSubmitCallback( [ __CLASS__, 'tryClearStashedUploads' ] );
410 $form->setSubmitTextMsg( 'uploadstash-clear' );
411
412 $form->prepareForm();
413 $formResult = $form->tryAuthorizedSubmit();
414
415 // show the files + form, if there are any, or just say there are none
416 $refreshHtml = Html::element( 'a',
417 [ 'href' => $this->getPageTitle()->getLocalURL() ],
418 $this->msg( 'uploadstash-refresh' )->text() );
419 $files = $this->stash->listFiles();
420 if ( $files && count( $files ) ) {
421 sort( $files );
422 $fileListItemsHtml = '';
424 foreach ( $files as $file ) {
425 $itemHtml = $linkRenderer->makeKnownLink(
426 $this->getPageTitle( "file/$file" ),
427 $file
428 );
429 try {
430 $fileObj = $this->stash->getFile( $file );
431 $thumb = $fileObj->generateThumbName( $file, [ 'width' => 220 ] );
432 $itemHtml .=
433 $this->msg( 'word-separator' )->escaped() .
434 $this->msg( 'parentheses' )->rawParams(
435 $linkRenderer->makeKnownLink(
436 $this->getPageTitle( "thumb/$file/$thumb" ),
437 $this->msg( 'uploadstash-thumbnail' )->text()
438 )
439 )->escaped();
440 } catch ( Exception $e ) {
441 }
442 $fileListItemsHtml .= Html::rawElement( 'li', [], $itemHtml );
443 }
444 $this->getOutput()->addHTML( Html::rawElement( 'ul', [], $fileListItemsHtml ) );
445 $form->displayForm( $formResult );
446 $this->getOutput()->addHTML( Html::rawElement( 'p', [], $refreshHtml ) );
447 } else {
448 $this->getOutput()->addHTML( Html::rawElement( 'p', [],
449 Html::element( 'span', [], $this->msg( 'uploadstash-nofiles' )->text() )
450 . ' '
451 . $refreshHtml
452 ) );
453 }
454 }
455}
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:61
const RENDER_NOW
Force rendering in the current process.
Definition File.php:69
Show an error that looks like an HTTP server error.
Definition HttpError.php:30
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.
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.
MediaWiki Linker LinkRenderer null $linkRenderer
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.
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,...
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.
static tryClearStashedUploads( $formData, $form)
Static callback for the HTMLForm in showUploads, to process Note the stash has to be recreated since ...
outputLocallyScaledThumb( $file, $params, $flags)
Scale a file (probably with a locally installed imagemagick, or similar) and output it to STDOUT.
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...
while(( $__line=Maintenance::readconsole()) !==false) print
Definition eval.php:64
const PROTO_CANONICAL
Definition Defines.php:212
$context
Definition load.php:45
$content
Definition router.php:78
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42