MediaWiki REL1_35
SpecialUploadStash.php
Go to the documentation of this file.
1<?php
24
39 // UploadStash
40 private $stash;
41
52 private const MAX_SERVE_BYTES = 1048576; // 1MB
53
54 public function __construct() {
55 parent::__construct( 'UploadStash', 'upload' );
56 }
57
58 public function doesWrites() {
59 return true;
60 }
61
68 public function execute( $subPage ) {
70
71 $this->stash = MediaWikiServices::getInstance()->getRepoGroup()
72 ->getLocalRepo()->getUploadStash( $this->getUser() );
73 $this->checkPermissions();
74
75 if ( $subPage === null || $subPage === '' ) {
76 $this->showUploads();
77 } else {
78 $this->showUpload( $subPage );
79 }
80 }
81
89 public function showUpload( $key ) {
90 // prevent callers from doing standard HTML output -- we'll take it from here
91 $this->getOutput()->disable();
92
93 try {
94 $params = $this->parseKey( $key );
95 if ( $params['type'] === 'thumb' ) {
96 $this->outputThumbFromStash( $params['file'], $params['params'] );
97 } else {
98 $this->outputLocalFile( $params['file'] );
99 }
100 return;
101 } catch ( UploadStashFileNotFoundException $e ) {
102 $code = 404;
103 $message = $e->getMessage();
104 } catch ( Exception $e ) {
105 $code = 500;
106 $message = $e->getMessage();
107 }
108
109 throw new HttpError( $code, $message );
110 }
111
121 private function parseKey( $key ) {
122 $type = strtok( $key, '/' );
123
124 if ( $type !== 'file' && $type !== 'thumb' ) {
126 $this->msg( 'uploadstash-bad-path-unknown-type', $type )
127 );
128 }
129 $fileName = strtok( '/' );
130 $thumbPart = strtok( '/' );
131 $file = $this->stash->getFile( $fileName );
132 if ( $type === 'thumb' ) {
133 $srcNamePos = strrpos( $thumbPart, $fileName );
134 if ( $srcNamePos === false || $srcNamePos < 1 ) {
136 $this->msg( 'uploadstash-bad-path-unrecognized-thumb-name' )
137 );
138 }
139 $paramString = substr( $thumbPart, 0, $srcNamePos - 1 );
140
141 $handler = $file->getHandler();
142 if ( $handler ) {
143 $params = $handler->parseParamString( $paramString );
144
145 return [ 'file' => $file, 'type' => $type, 'params' => $params ];
146 } else {
148 $this->msg( 'uploadstash-bad-path-no-handler', $file->getMimeType(), $file->getPath() )
149 );
150 }
151 }
152
153 return [ 'file' => $file, 'type' => $type ];
154 }
155
162 private function outputThumbFromStash( $file, $params ) {
163 $flags = 0;
164 // this config option, if it exists, points to a "scaler", as you might find in
165 // the Wikimedia Foundation cluster. See outputRemoteScaledThumb(). This
166 // is part of our horrible NFS-based system, we create a file on a mount
167 // point here, but fetch the scaled file from somewhere else that
168 // happens to share it over NFS.
169 if ( $this->getConfig()->get( 'UploadStashScalerBaseUrl' ) ) {
170 $this->outputRemoteScaledThumb( $file, $params, $flags );
171 } else {
172 $this->outputLocallyScaledThumb( $file, $params, $flags );
173 }
174 }
175
184 private function outputLocallyScaledThumb( $file, $params, $flags ) {
185 // n.b. this is stupid, we insist on re-transforming the file every time we are invoked. We rely
186 // on HTTP caching to ensure this doesn't happen.
187
188 $flags |= File::RENDER_NOW;
189
190 $thumbnailImage = $file->transform( $params, $flags );
191 if ( !$thumbnailImage ) {
193 $this->msg( 'uploadstash-file-not-found-no-thumb' )
194 );
195 }
196
197 // we should have just generated it locally
198 if ( !$thumbnailImage->getStoragePath() ) {
200 $this->msg( 'uploadstash-file-not-found-no-local-path' )
201 );
202 }
203
204 // now we should construct a File, so we can get MIME and other such info in a standard way
205 // n.b. MIME type may be different from original (ogx original -> jpeg thumb)
206 $thumbFile = new UnregisteredLocalFile( false,
207 $this->stash->repo, $thumbnailImage->getStoragePath(), false );
208
209 $this->outputLocalFile( $thumbFile );
210 }
211
230 private function outputRemoteScaledThumb( $file, $params, $flags ) {
231 // This option probably looks something like
232 // '//upload.wikimedia.org/wikipedia/test/thumb/temp'. Do not use
233 // trailing slash.
234 $scalerBaseUrl = $this->getConfig()->get( 'UploadStashScalerBaseUrl' );
235
236 if ( preg_match( '/^\/\//', $scalerBaseUrl ) ) {
237 // this is apparently a protocol-relative URL, which makes no sense in this context,
238 // since this is used for communication that's internal to the application.
239 // default to http.
240 $scalerBaseUrl = wfExpandUrl( $scalerBaseUrl, PROTO_CANONICAL );
241 }
242
243 // We need to use generateThumbName() instead of thumbName(), because
244 // the suffix needs to match the file name for the remote thumbnailer
245 // to work
246 $scalerThumbName = $file->generateThumbName( $file->getName(), $params );
247 $scalerThumbUrl = $scalerBaseUrl . '/' . $file->getUrlRel() .
248 '/' . rawurlencode( $scalerThumbName );
249
250 // If a thumb proxy is set up for the repo, we favor that, as that will
251 // keep the request internal
252 $thumbProxyUrl = $file->getRepo()->getThumbProxyUrl();
253
254 if ( strlen( $thumbProxyUrl ) ) {
255 $scalerThumbUrl = $thumbProxyUrl . 'temp/' . $file->getUrlRel() .
256 '/' . rawurlencode( $scalerThumbName );
257 }
258
259 // make an http request based on wgUploadStashScalerBaseUrl to lazy-create
260 // a thumbnail
261 $httpOptions = [
262 'method' => 'GET',
263 'timeout' => 5 // T90599 attempt to time out cleanly
264 ];
265 $req = MWHttpRequest::factory( $scalerThumbUrl, $httpOptions, __METHOD__ );
266
267 $secret = $file->getRepo()->getThumbProxySecret();
268
269 // Pass a secret key shared with the proxied service if any
270 if ( strlen( $secret ) ) {
271 $req->setHeader( 'X-Swift-Secret', $secret );
272 }
273
274 $status = $req->execute();
275 if ( !$status->isOK() ) {
276 $errors = $status->getErrorsArray();
278 $this->msg(
279 'uploadstash-file-not-found-no-remote-thumb',
280 print_r( $errors, 1 ),
281 $scalerThumbUrl
282 )
283 );
284 }
285 $contentType = $req->getResponseHeader( "content-type" );
286 if ( !$contentType ) {
288 $this->msg( 'uploadstash-file-not-found-missing-content-type' )
289 );
290 }
291
292 $this->outputContents( $req->getContent(), $contentType );
293 }
294
303 private function outputLocalFile( File $file ) {
304 if ( $file->getSize() > self::MAX_SERVE_BYTES ) {
306 $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
307 );
308 }
309
310 $file->getRepo()->streamFileWithStatus( $file->getPath(),
311 [ 'Content-Transfer-Encoding: binary',
312 'Expires: Sun, 17-Jan-2038 19:14:07 GMT' ]
313 );
314 }
315
323 private function outputContents( $content, $contentType ) {
324 $size = strlen( $content );
325 if ( $size > self::MAX_SERVE_BYTES ) {
327 $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
328 );
329 }
330 // Cancel output buffering and gzipping if set
332 self::outputFileHeaders( $contentType, $size );
334 }
335
345 private static function outputFileHeaders( $contentType, $size ) {
346 header( "Content-Type: $contentType", true );
347 header( 'Content-Transfer-Encoding: binary', true );
348 header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true );
349 // T55032 - It shouldn't be a problem here, but let's be safe and not cache
350 header( 'Cache-Control: private' );
351 header( "Content-Length: $size", true );
352 }
353
363 public static function tryClearStashedUploads( $formData, $form ) {
364 if ( isset( $formData['Clear'] ) ) {
365 $stash = MediaWikiServices::getInstance()->getRepoGroup()
366 ->getLocalRepo()->getUploadStash( $form->getUser() );
367 wfDebug( 'stash has: ' . print_r( $stash->listFiles(), true ) );
368
369 if ( !$stash->clear() ) {
370 return Status::newFatal( 'uploadstash-errclear' );
371 }
372 }
373
374 return Status::newGood();
375 }
376
381 private function showUploads() {
382 // sets the title, etc.
383 $this->setHeaders();
384 $this->outputHeader();
385
386 // create the form, which will also be used to execute a callback to process incoming form data
387 // this design is extremely dubious, but supposedly HTMLForm is our standard now?
388
389 $context = new DerivativeContext( $this->getContext() );
390 $context->setTitle( $this->getPageTitle() ); // Remove subpage
391 $form = HTMLForm::factory( 'ooui', [
392 'Clear' => [
393 'type' => 'hidden',
394 'default' => true,
395 'name' => 'clear',
396 ]
397 ], $context, 'clearStashedUploads' );
398 $form->setSubmitDestructive();
399 $form->setSubmitCallback( [ __CLASS__, 'tryClearStashedUploads' ] );
400 $form->setSubmitTextMsg( 'uploadstash-clear' );
401
402 $form->prepareForm();
403 $formResult = $form->tryAuthorizedSubmit();
404
405 // show the files + form, if there are any, or just say there are none
406 $refreshHtml = Html::element( 'a',
407 [ 'href' => $this->getPageTitle()->getLocalURL() ],
408 $this->msg( 'uploadstash-refresh' )->text() );
409 $files = $this->stash->listFiles();
410 if ( $files && count( $files ) ) {
411 sort( $files );
412 $fileListItemsHtml = '';
414 foreach ( $files as $file ) {
415 $itemHtml = $linkRenderer->makeKnownLink(
416 $this->getPageTitle( "file/$file" ),
417 $file
418 );
419 try {
420 $fileObj = $this->stash->getFile( $file );
421 $thumb = $fileObj->generateThumbName( $file, [ 'width' => 220 ] );
422 $itemHtml .=
423 $this->msg( 'word-separator' )->escaped() .
424 $this->msg( 'parentheses' )->rawParams(
425 $linkRenderer->makeKnownLink(
426 $this->getPageTitle( "thumb/$file/$thumb" ),
427 $this->msg( 'uploadstash-thumbnail' )->text()
428 )
429 )->escaped();
430 } catch ( Exception $e ) {
431 }
432 $fileListItemsHtml .= Html::rawElement( 'li', [], $itemHtml );
433 }
434 $this->getOutput()->addHTML( Html::rawElement( 'ul', [], $fileListItemsHtml ) );
435 $form->displayForm( $formResult );
436 $this->getOutput()->addHTML( Html::rawElement( 'p', [], $refreshHtml ) );
437 } else {
438 $this->getOutput()->addHTML( Html::rawElement( 'p', [],
439 Html::element( 'span', [], $this->msg( 'uploadstash-nofiles' )->text() )
440 . ' '
441 . $refreshHtml
442 ) );
443 }
444 }
445}
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:63
Show an error that looks like an HTTP server error.
Definition HttpError.php:32
MediaWikiServices is the service locator for the application scope of MediaWiki.
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:213
$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