This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * Implements Special:UploadStash. |
||
4 | * |
||
5 | * This program is free software; you can redistribute it and/or modify |
||
6 | * it under the terms of the GNU General Public License as published by |
||
7 | * the Free Software Foundation; either version 2 of the License, or |
||
8 | * (at your option) any later version. |
||
9 | * |
||
10 | * This program is distributed in the hope that it will be useful, |
||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
13 | * GNU General Public License for more details. |
||
14 | * |
||
15 | * You should have received a copy of the GNU General Public License along |
||
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
||
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
18 | * http://www.gnu.org/copyleft/gpl.html |
||
19 | * |
||
20 | * @file |
||
21 | * @ingroup SpecialPage |
||
22 | * @ingroup Upload |
||
23 | */ |
||
24 | |||
25 | /** |
||
26 | * Web access for files temporarily stored by UploadStash. |
||
27 | * |
||
28 | * For example -- files that were uploaded with the UploadWizard extension are stored temporarily |
||
29 | * before committing them to the db. But we want to see their thumbnails and get other information |
||
30 | * about them. |
||
31 | * |
||
32 | * Since this is based on the user's session, in effect this creates a private temporary file area. |
||
33 | * However, the URLs for the files cannot be shared. |
||
34 | */ |
||
35 | class SpecialUploadStash extends UnlistedSpecialPage { |
||
36 | // UploadStash |
||
37 | private $stash; |
||
38 | |||
39 | /** |
||
40 | * Since we are directly writing the file to STDOUT, |
||
41 | * we should not be reading in really big files and serving them out. |
||
42 | * |
||
43 | * We also don't want people using this as a file drop, even if they |
||
44 | * share credentials. |
||
45 | * |
||
46 | * This service is really for thumbnails and other such previews while |
||
47 | * uploading. |
||
48 | */ |
||
49 | const MAX_SERVE_BYTES = 1048576; // 1MB |
||
50 | |||
51 | public function __construct() { |
||
52 | parent::__construct( 'UploadStash', 'upload' ); |
||
53 | } |
||
54 | |||
55 | public function doesWrites() { |
||
56 | return true; |
||
57 | } |
||
58 | |||
59 | /** |
||
60 | * Execute page -- can output a file directly or show a listing of them. |
||
61 | * |
||
62 | * @param string $subPage Subpage, e.g. in |
||
63 | * https://example.com/wiki/Special:UploadStash/foo.jpg, the "foo.jpg" part |
||
64 | * @return bool Success |
||
65 | */ |
||
66 | public function execute( $subPage ) { |
||
67 | $this->useTransactionalTimeLimit(); |
||
68 | |||
69 | $this->stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash( $this->getUser() ); |
||
70 | $this->checkPermissions(); |
||
71 | |||
72 | if ( $subPage === null || $subPage === '' ) { |
||
73 | return $this->showUploads(); |
||
74 | } |
||
75 | |||
76 | return $this->showUpload( $subPage ); |
||
77 | } |
||
78 | |||
79 | /** |
||
80 | * If file available in stash, cats it out to the client as a simple HTTP response. |
||
81 | * n.b. Most sanity checking done in UploadStashLocalFile, so this is straightforward. |
||
82 | * |
||
83 | * @param string $key The key of a particular requested file |
||
84 | * @throws HttpError |
||
85 | * @return bool |
||
86 | */ |
||
87 | public function showUpload( $key ) { |
||
88 | // prevent callers from doing standard HTML output -- we'll take it from here |
||
89 | $this->getOutput()->disable(); |
||
90 | |||
91 | try { |
||
92 | $params = $this->parseKey( $key ); |
||
93 | if ( $params['type'] === 'thumb' ) { |
||
94 | return $this->outputThumbFromStash( $params['file'], $params['params'] ); |
||
95 | } else { |
||
96 | return $this->outputLocalFile( $params['file'] ); |
||
97 | } |
||
98 | } catch ( UploadStashFileNotFoundException $e ) { |
||
99 | $code = 404; |
||
100 | $message = $e->getMessage(); |
||
101 | } catch ( UploadStashZeroLengthFileException $e ) { |
||
102 | $code = 500; |
||
103 | $message = $e->getMessage(); |
||
104 | } catch ( UploadStashBadPathException $e ) { |
||
105 | $code = 500; |
||
106 | $message = $e->getMessage(); |
||
107 | } catch ( SpecialUploadStashTooLargeException $e ) { |
||
108 | $code = 500; |
||
109 | $message = 'Cannot serve a file larger than ' . self::MAX_SERVE_BYTES . |
||
110 | ' bytes. ' . $e->getMessage(); |
||
111 | } catch ( Exception $e ) { |
||
112 | $code = 500; |
||
113 | $message = $e->getMessage(); |
||
114 | } |
||
115 | |||
116 | throw new HttpError( $code, $message ); |
||
117 | } |
||
118 | |||
119 | /** |
||
120 | * Parse the key passed to the SpecialPage. Returns an array containing |
||
121 | * the associated file object, the type ('file' or 'thumb') and if |
||
122 | * application the transform parameters |
||
123 | * |
||
124 | * @param string $key |
||
125 | * @throws UploadStashBadPathException |
||
126 | * @return array |
||
127 | */ |
||
128 | private function parseKey( $key ) { |
||
129 | $type = strtok( $key, '/' ); |
||
130 | |||
131 | if ( $type !== 'file' && $type !== 'thumb' ) { |
||
132 | throw new UploadStashBadPathException( "Unknown type '$type'" ); |
||
133 | } |
||
134 | $fileName = strtok( '/' ); |
||
135 | $thumbPart = strtok( '/' ); |
||
136 | $file = $this->stash->getFile( $fileName ); |
||
137 | if ( $type === 'thumb' ) { |
||
138 | $srcNamePos = strrpos( $thumbPart, $fileName ); |
||
139 | if ( $srcNamePos === false || $srcNamePos < 1 ) { |
||
140 | throw new UploadStashBadPathException( 'Unrecognized thumb name' ); |
||
141 | } |
||
142 | $paramString = substr( $thumbPart, 0, $srcNamePos - 1 ); |
||
143 | |||
144 | $handler = $file->getHandler(); |
||
145 | if ( $handler ) { |
||
146 | $params = $handler->parseParamString( $paramString ); |
||
147 | |||
148 | return [ 'file' => $file, 'type' => $type, 'params' => $params ]; |
||
149 | } else { |
||
150 | throw new UploadStashBadPathException( 'No handler found for ' . |
||
151 | "mime {$file->getMimeType()} of file {$file->getPath()}" ); |
||
152 | } |
||
153 | } |
||
154 | |||
155 | return [ 'file' => $file, 'type' => $type ]; |
||
156 | } |
||
157 | |||
158 | /** |
||
159 | * Get a thumbnail for file, either generated locally or remotely, and stream it out |
||
160 | * |
||
161 | * @param File $file |
||
162 | * @param array $params |
||
163 | * |
||
164 | * @return bool Success |
||
165 | */ |
||
166 | private function outputThumbFromStash( $file, $params ) { |
||
167 | $flags = 0; |
||
168 | // this config option, if it exists, points to a "scaler", as you might find in |
||
169 | // the Wikimedia Foundation cluster. See outputRemoteScaledThumb(). This |
||
170 | // is part of our horrible NFS-based system, we create a file on a mount |
||
171 | // point here, but fetch the scaled file from somewhere else that |
||
172 | // happens to share it over NFS. |
||
173 | if ( $this->getConfig()->get( 'UploadStashScalerBaseUrl' ) ) { |
||
174 | $this->outputRemoteScaledThumb( $file, $params, $flags ); |
||
175 | } else { |
||
176 | $this->outputLocallyScaledThumb( $file, $params, $flags ); |
||
177 | } |
||
178 | } |
||
179 | |||
180 | /** |
||
181 | * Scale a file (probably with a locally installed imagemagick, or similar) |
||
182 | * and output it to STDOUT. |
||
183 | * @param File $file |
||
184 | * @param array $params Scaling parameters ( e.g. [ width => '50' ] ); |
||
185 | * @param int $flags Scaling flags ( see File:: constants ) |
||
186 | * @throws MWException|UploadStashFileNotFoundException |
||
187 | * @return bool Success |
||
188 | */ |
||
189 | private function outputLocallyScaledThumb( $file, $params, $flags ) { |
||
190 | // n.b. this is stupid, we insist on re-transforming the file every time we are invoked. We rely |
||
191 | // on HTTP caching to ensure this doesn't happen. |
||
192 | |||
193 | $flags |= File::RENDER_NOW; |
||
194 | |||
195 | $thumbnailImage = $file->transform( $params, $flags ); |
||
196 | if ( !$thumbnailImage ) { |
||
197 | throw new MWException( 'Could not obtain thumbnail' ); |
||
198 | } |
||
199 | |||
200 | // we should have just generated it locally |
||
201 | if ( !$thumbnailImage->getStoragePath() ) { |
||
202 | throw new UploadStashFileNotFoundException( "no local path for scaled item" ); |
||
203 | } |
||
204 | |||
205 | // now we should construct a File, so we can get MIME and other such info in a standard way |
||
206 | // n.b. MIME type may be different from original (ogx original -> jpeg thumb) |
||
207 | $thumbFile = new UnregisteredLocalFile( false, |
||
208 | $this->stash->repo, $thumbnailImage->getStoragePath(), false ); |
||
209 | if ( !$thumbFile ) { |
||
210 | throw new UploadStashFileNotFoundException( "couldn't create local file object for thumbnail" ); |
||
211 | } |
||
212 | |||
213 | return $this->outputLocalFile( $thumbFile ); |
||
214 | } |
||
215 | |||
216 | /** |
||
217 | * Scale a file with a remote "scaler", as exists on the Wikimedia Foundation |
||
218 | * cluster, and output it to STDOUT. |
||
219 | * Note: Unlike the usual thumbnail process, the web client never sees the |
||
220 | * cluster URL; we do the whole HTTP transaction to the scaler ourselves |
||
221 | * and cat the results out. |
||
222 | * Note: We rely on NFS to have propagated the file contents to the scaler. |
||
223 | * However, we do not rely on the thumbnail being created in NFS and then |
||
224 | * propagated back to our filesystem. Instead we take the results of the |
||
225 | * HTTP request instead. |
||
226 | * Note: No caching is being done here, although we are instructing the |
||
227 | * client to cache it forever. |
||
228 | * |
||
229 | * @param File $file |
||
230 | * @param array $params Scaling parameters ( e.g. [ width => '50' ] ); |
||
231 | * @param int $flags Scaling flags ( see File:: constants ) |
||
232 | * @throws MWException |
||
233 | * @return bool Success |
||
234 | */ |
||
235 | private function outputRemoteScaledThumb( $file, $params, $flags ) { |
||
236 | // This option probably looks something like |
||
237 | // '//upload.wikimedia.org/wikipedia/test/thumb/temp'. Do not use |
||
238 | // trailing slash. |
||
239 | $scalerBaseUrl = $this->getConfig()->get( 'UploadStashScalerBaseUrl' ); |
||
240 | |||
241 | if ( preg_match( '/^\/\//', $scalerBaseUrl ) ) { |
||
242 | // this is apparently a protocol-relative URL, which makes no sense in this context, |
||
243 | // since this is used for communication that's internal to the application. |
||
244 | // default to http. |
||
245 | $scalerBaseUrl = wfExpandUrl( $scalerBaseUrl, PROTO_CANONICAL ); |
||
246 | } |
||
247 | |||
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 | $scalerThumbUrl = $scalerBaseUrl . '/' . $file->getUrlRel() . |
||
253 | '/' . rawurlencode( $scalerThumbName ); |
||
254 | |||
255 | // make a curl call to the scaler to create a thumbnail |
||
256 | $httpOptions = [ |
||
257 | 'method' => 'GET', |
||
258 | 'timeout' => 5 // T90599 attempt to time out cleanly |
||
259 | ]; |
||
260 | $req = MWHttpRequest::factory( $scalerThumbUrl, $httpOptions, __METHOD__ ); |
||
261 | $status = $req->execute(); |
||
262 | if ( !$status->isOK() ) { |
||
263 | $errors = $status->getErrorsArray(); |
||
264 | $errorStr = "Fetching thumbnail failed: " . print_r( $errors, 1 ); |
||
265 | $errorStr .= "\nurl = $scalerThumbUrl\n"; |
||
266 | throw new MWException( $errorStr ); |
||
267 | } |
||
268 | $contentType = $req->getResponseHeader( "content-type" ); |
||
269 | if ( !$contentType ) { |
||
270 | throw new MWException( "Missing content-type header" ); |
||
271 | } |
||
272 | |||
273 | return $this->outputContents( $req->getContent(), $contentType ); |
||
274 | } |
||
275 | |||
276 | /** |
||
277 | * Output HTTP response for file |
||
278 | * Side effect: writes HTTP response to STDOUT. |
||
279 | * |
||
280 | * @param File $file File object with a local path (e.g. UnregisteredLocalFile, |
||
281 | * LocalFile. Oddly these don't share an ancestor!) |
||
282 | * @throws SpecialUploadStashTooLargeException |
||
283 | * @return bool |
||
284 | */ |
||
285 | private function outputLocalFile( File $file ) { |
||
286 | if ( $file->getSize() > self::MAX_SERVE_BYTES ) { |
||
287 | throw new SpecialUploadStashTooLargeException(); |
||
288 | } |
||
289 | |||
290 | return $file->getRepo()->streamFile( $file->getPath(), |
||
0 ignored issues
–
show
|
|||
291 | [ 'Content-Transfer-Encoding: binary', |
||
292 | 'Expires: Sun, 17-Jan-2038 19:14:07 GMT' ] |
||
293 | ); |
||
294 | } |
||
295 | |||
296 | /** |
||
297 | * Output HTTP response of raw content |
||
298 | * Side effect: writes HTTP response to STDOUT. |
||
299 | * @param string $content Content |
||
300 | * @param string $contentType MIME type |
||
301 | * @throws SpecialUploadStashTooLargeException |
||
302 | * @return bool |
||
303 | */ |
||
304 | private function outputContents( $content, $contentType ) { |
||
305 | $size = strlen( $content ); |
||
306 | if ( $size > self::MAX_SERVE_BYTES ) { |
||
307 | throw new SpecialUploadStashTooLargeException(); |
||
308 | } |
||
309 | // Cancel output buffering and gzipping if set |
||
310 | wfResetOutputBuffers(); |
||
311 | self::outputFileHeaders( $contentType, $size ); |
||
312 | print $content; |
||
313 | |||
314 | return true; |
||
315 | } |
||
316 | |||
317 | /** |
||
318 | * Output headers for streaming |
||
319 | * @todo Unsure about encoding as binary; if we received from HTTP perhaps |
||
320 | * we should use that encoding, concatenated with semicolon to `$contentType` as it |
||
321 | * usually is. |
||
322 | * Side effect: preps PHP to write headers to STDOUT. |
||
323 | * @param string $contentType String suitable for content-type header |
||
324 | * @param string $size Length in bytes |
||
325 | */ |
||
326 | private static function outputFileHeaders( $contentType, $size ) { |
||
327 | header( "Content-Type: $contentType", true ); |
||
328 | header( 'Content-Transfer-Encoding: binary', true ); |
||
329 | header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true ); |
||
330 | // Bug 53032 - It shouldn't be a problem here, but let's be safe and not cache |
||
331 | header( 'Cache-Control: private' ); |
||
332 | header( "Content-Length: $size", true ); |
||
333 | } |
||
334 | |||
335 | /** |
||
336 | * Static callback for the HTMLForm in showUploads, to process |
||
337 | * Note the stash has to be recreated since this is being called in a static context. |
||
338 | * This works, because there really is only one stash per logged-in user, despite appearances. |
||
339 | * |
||
340 | * @param array $formData |
||
341 | * @param HTMLForm $form |
||
342 | * @return Status |
||
343 | */ |
||
344 | public static function tryClearStashedUploads( $formData, $form ) { |
||
345 | if ( isset( $formData['Clear'] ) ) { |
||
346 | $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash( $form->getUser() ); |
||
347 | wfDebug( 'stash has: ' . print_r( $stash->listFiles(), true ) . "\n" ); |
||
348 | |||
349 | if ( !$stash->clear() ) { |
||
350 | return Status::newFatal( 'uploadstash-errclear' ); |
||
351 | } |
||
352 | } |
||
353 | |||
354 | return Status::newGood(); |
||
355 | } |
||
356 | |||
357 | /** |
||
358 | * Default action when we don't have a subpage -- just show links to the uploads we have, |
||
359 | * Also show a button to clear stashed files |
||
360 | * @return bool |
||
361 | */ |
||
362 | private function showUploads() { |
||
363 | // sets the title, etc. |
||
364 | $this->setHeaders(); |
||
365 | $this->outputHeader(); |
||
366 | |||
367 | // create the form, which will also be used to execute a callback to process incoming form data |
||
368 | // this design is extremely dubious, but supposedly HTMLForm is our standard now? |
||
369 | |||
370 | $context = new DerivativeContext( $this->getContext() ); |
||
371 | $context->setTitle( $this->getPageTitle() ); // Remove subpage |
||
372 | $form = HTMLForm::factory( 'ooui', [ |
||
373 | 'Clear' => [ |
||
374 | 'type' => 'hidden', |
||
375 | 'default' => true, |
||
376 | 'name' => 'clear', |
||
377 | ] |
||
378 | ], $context, 'clearStashedUploads' ); |
||
379 | $form->setSubmitDestructive(); |
||
380 | $form->setSubmitCallback( [ __CLASS__, 'tryClearStashedUploads' ] ); |
||
381 | $form->setSubmitTextMsg( 'uploadstash-clear' ); |
||
382 | |||
383 | $form->prepareForm(); |
||
384 | $formResult = $form->tryAuthorizedSubmit(); |
||
385 | |||
386 | // show the files + form, if there are any, or just say there are none |
||
387 | $refreshHtml = Html::element( 'a', |
||
388 | [ 'href' => $this->getPageTitle()->getLocalURL() ], |
||
389 | $this->msg( 'uploadstash-refresh' )->text() ); |
||
390 | $files = $this->stash->listFiles(); |
||
391 | if ( $files && count( $files ) ) { |
||
392 | sort( $files ); |
||
393 | $fileListItemsHtml = ''; |
||
394 | $linkRenderer = $this->getLinkRenderer(); |
||
395 | foreach ( $files as $file ) { |
||
396 | $itemHtml = $linkRenderer->makeKnownLink( |
||
397 | $this->getPageTitle( "file/$file" ), |
||
398 | $file |
||
399 | ); |
||
400 | try { |
||
401 | $fileObj = $this->stash->getFile( $file ); |
||
402 | $thumb = $fileObj->generateThumbName( $file, [ 'width' => 220 ] ); |
||
403 | $itemHtml .= |
||
404 | $this->msg( 'word-separator' )->escaped() . |
||
405 | $this->msg( 'parentheses' )->rawParams( |
||
406 | $linkRenderer->makeKnownLink( |
||
407 | $this->getPageTitle( "thumb/$file/$thumb" ), |
||
408 | $this->msg( 'uploadstash-thumbnail' )->text() |
||
409 | ) |
||
410 | )->escaped(); |
||
411 | } catch ( Exception $e ) { |
||
0 ignored issues
–
show
Coding Style
Comprehensibility
introduced
by
|
|||
412 | } |
||
413 | $fileListItemsHtml .= Html::rawElement( 'li', [], $itemHtml ); |
||
414 | } |
||
415 | $this->getOutput()->addHTML( Html::rawElement( 'ul', [], $fileListItemsHtml ) ); |
||
416 | $form->displayForm( $formResult ); |
||
417 | $this->getOutput()->addHTML( Html::rawElement( 'p', [], $refreshHtml ) ); |
||
418 | } else { |
||
419 | $this->getOutput()->addHTML( Html::rawElement( 'p', [], |
||
420 | Html::element( 'span', [], $this->msg( 'uploadstash-nofiles' )->text() ) |
||
421 | . ' ' |
||
422 | . $refreshHtml |
||
423 | ) ); |
||
424 | } |
||
425 | |||
426 | return true; |
||
427 | } |
||
428 | } |
||
429 | |||
430 | class SpecialUploadStashTooLargeException extends MWException { |
||
431 | } |
||
432 |
This check looks at variables that are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.