Index: trunk/phase3/includes/upload/UploadStash.php |
— | — | @@ -12,14 +12,13 @@ |
13 | 13 | * |
14 | 14 | */ |
15 | 15 | class UploadStash { |
16 | | - // Format of the key for files -- has to be suitable as a filename itself in some cases. |
17 | | - // This should encompass a sha1 content hash in hex (new style), or an integer (old style), |
18 | | - // and also thumbnails with prepended strings like "120px-". |
19 | | - // The file extension should not be part of the key. |
20 | | - const KEY_FORMAT_REGEX = '/^[\w-]+$/'; |
21 | 16 | |
| 17 | + // Format of the key for files -- has to be suitable as a filename itself (e.g. ab12cd34ef.jpg) |
| 18 | + const KEY_FORMAT_REGEX = '/^[\w-]+\.\w+$/'; |
| 19 | + |
22 | 20 | // repository that this uses to store temp files |
23 | | - protected $repo; |
| 21 | + // public because we sometimes need to get a LocalFile within the same repo. |
| 22 | + public $repo; |
24 | 23 | |
25 | 24 | // array of initialized objects obtained from session (lazily initialized upon getFile()) |
26 | 25 | private $files = array(); |
— | — | @@ -82,7 +81,9 @@ |
83 | 82 | unset( $data['mTempPath'] ); |
84 | 83 | |
85 | 84 | $file = new UploadStashFile( $this, $this->repo, $path, $key, $data ); |
86 | | - |
| 85 | + if ( $file->getSize === 0 ) { |
| 86 | + throw new UploadStashZeroLengthFileException( "File is zero length" ); |
| 87 | + } |
87 | 88 | $this->files[$key] = $file; |
88 | 89 | |
89 | 90 | } |
— | — | @@ -108,18 +109,31 @@ |
109 | 110 | } |
110 | 111 | $fileProps = File::getPropsFromPath( $path ); |
111 | 112 | |
| 113 | + // we will be initializing from some tmpnam files that don't have extensions. |
| 114 | + // most of MediaWiki assumes all uploaded files have good extensions. So, we fix this. |
| 115 | + $extension = self::getExtensionForPath( $path ); |
| 116 | + if ( ! preg_match( "/\\.\\Q$extension\\E$/", $path ) ) { |
| 117 | + $pathWithGoodExtension = "$path.$extension"; |
| 118 | + if ( ! rename( $path, $pathWithGoodExtension ) ) { |
| 119 | + throw new UploadStashFileException( "couldn't rename $path to have a better extension at $pathWithGoodExtension" ); |
| 120 | + } |
| 121 | + $path = $pathWithGoodExtension; |
| 122 | + } |
| 123 | + |
112 | 124 | // If no key was supplied, use content hash. Also has the nice property of collapsing multiple identical files |
113 | 125 | // uploaded this session, which could happen if uploads had failed. |
114 | 126 | if ( is_null( $key ) ) { |
115 | | - $key = $fileProps['sha1']; |
| 127 | + $key = $fileProps['sha1'] . "." . $extension; |
116 | 128 | } |
117 | 129 | |
118 | 130 | if ( ! preg_match( self::KEY_FORMAT_REGEX, $key ) ) { |
119 | 131 | throw new UploadStashBadPathException( "key '$key' is not in a proper format" ); |
120 | 132 | } |
121 | 133 | |
122 | | - // if not already in a temporary area, put it there |
| 134 | + |
| 135 | + // if not already in a temporary area, put it there |
123 | 136 | $status = $this->repo->storeTemp( basename( $path ), $path ); |
| 137 | + |
124 | 138 | if( ! $status->isOK() ) { |
125 | 139 | // It is a convention in MediaWiki to only return one error per API exception, even if multiple errors |
126 | 140 | // are available. We use reset() to pick the "first" thing that was wrong, preferring errors to warnings. |
— | — | @@ -136,7 +150,7 @@ |
137 | 151 | throw new UploadStashFileException( "error storing file in '$path': " . implode( '; ', $error ) ); |
138 | 152 | } |
139 | 153 | $stashPath = $status->value; |
140 | | - |
| 154 | + |
141 | 155 | // required info we always store. Must trump any other application info in $data |
142 | 156 | // 'mTempPath', 'mFileSize', and 'mFileProps' are arbitrary names |
143 | 157 | // chosen for compatibility with UploadBase's way of doing this. |
— | — | @@ -149,11 +163,42 @@ |
150 | 164 | |
151 | 165 | // now, merge required info and extra data into the session. (The extra data changes from application to application. |
152 | 166 | // UploadWizard wants different things than say FirefoggChunkedUpload.) |
| 167 | + wfDebug( __METHOD__ . " storing under $key\n" ); |
153 | 168 | $_SESSION[UploadBase::SESSION_KEYNAME][$key] = array_merge( $data, $requiredData ); |
154 | 169 | |
155 | 170 | return $this->getFile( $key ); |
156 | 171 | } |
157 | 172 | |
| 173 | + /** |
| 174 | + * Find or guess extension -- ensuring that our extension matches our mime type. |
| 175 | + * Since these files are constructed from php tempnames they may not start off |
| 176 | + * with an extension. |
| 177 | + * XXX this is somewhat redundant with the checks that ApiUpload.php does with incoming |
| 178 | + * uploads versus the desired filename. Maybe we can get that passed to us... |
| 179 | + */ |
| 180 | + public static function getExtensionForPath( $path ) { |
| 181 | + // Does this have an extension? |
| 182 | + $n = strrpos( $path, '.' ); |
| 183 | + $extension = null; |
| 184 | + if ( $n !== false ) { |
| 185 | + $extension = $n ? substr( $path, $n + 1 ) : ''; |
| 186 | + } else { |
| 187 | + // If not, assume that it should be related to the mime type of the original file. |
| 188 | + $magic = MimeMagic::singleton(); |
| 189 | + $mimeType = $magic->guessMimeType( $path ); |
| 190 | + $extensions = explode( ' ', MimeMagic::singleton()->getExtensionsForType( $mimeType ) ); |
| 191 | + if ( count( $extensions ) ) { |
| 192 | + $extension = $extensions[0]; |
| 193 | + } |
| 194 | + } |
| 195 | + |
| 196 | + if ( is_null( $extension ) ) { |
| 197 | + throw new UploadStashFileException( "extension is null" ); |
| 198 | + } |
| 199 | + |
| 200 | + return File::normalizeExtension( $extension ); |
| 201 | + } |
| 202 | + |
158 | 203 | } |
159 | 204 | |
160 | 205 | class UploadStashFile extends UnregisteredLocalFile { |
— | — | @@ -198,13 +243,11 @@ |
199 | 244 | throw new UploadStashFileNotFoundException( 'cannot find path, or not a plain file' ); |
200 | 245 | } |
201 | 246 | |
| 247 | + |
| 248 | + |
202 | 249 | parent::__construct( false, $repo, $path, false ); |
203 | 250 | |
204 | | - // we will be initializing from some tmpnam files that don't have extensions. |
205 | | - // most of MediaWiki assumes all uploaded files have good extensions. So, we fix this. |
206 | 251 | $this->name = basename( $this->path ); |
207 | | - $this->setExtension(); |
208 | | - |
209 | 252 | } |
210 | 253 | |
211 | 254 | /** |
— | — | @@ -220,40 +263,6 @@ |
221 | 264 | } |
222 | 265 | |
223 | 266 | /** |
224 | | - * Find or guess extension -- ensuring that our extension matches our mime type. |
225 | | - * Since these files are constructed from php tempnames they may not start off |
226 | | - * with an extension. |
227 | | - * This does not override getExtension() because things like getMimeType() already call getExtension(), |
228 | | - * and that results in infinite recursion. So, we preemptively *set* the extension so getExtension() can find it. |
229 | | - * For obvious reasons this should be called as early as possible, as part of initialization |
230 | | - */ |
231 | | - public function setExtension() { |
232 | | - // Does this have an extension? |
233 | | - $n = strrpos( $this->path, '.' ); |
234 | | - $extension = null; |
235 | | - if ( $n !== false ) { |
236 | | - $extension = $n ? substr( $this->path, $n + 1 ) : ''; |
237 | | - } else { |
238 | | - // If not, assume that it should be related to the mime type of the original file. |
239 | | - // |
240 | | - // This entire thing is backwards -- we *should* just create an extension based on |
241 | | - // the mime type of the transformed file, *after* transformation. But File.php demands |
242 | | - // to know the name of the transformed file before creating it. |
243 | | - $mimeType = $this->getMimeType(); |
244 | | - $extensions = explode( ' ', MimeMagic::singleton()->getExtensionsForType( $mimeType ) ); |
245 | | - if ( count( $extensions ) ) { |
246 | | - $extension = $extensions[0]; |
247 | | - } |
248 | | - } |
249 | | - |
250 | | - if ( is_null( $extension ) ) { |
251 | | - throw new UploadStashFileException( "extension is null" ); |
252 | | - } |
253 | | - |
254 | | - $this->extension = parent::normalizeExtension( $extension ); |
255 | | - } |
256 | | - |
257 | | - /** |
258 | 267 | * Get the path for the thumbnail (actually any transformation of this file) |
259 | 268 | * The actual argument is the result of thumbName although we seem to have |
260 | 269 | * buggy code elsewhere that expects a boolean 'suffix' |
— | — | @@ -276,12 +285,27 @@ |
277 | 286 | * @return String: base name for URL, like '120px-12345.jpg', or null if there is no handler |
278 | 287 | */ |
279 | 288 | function thumbName( $params ) { |
| 289 | + return $this->getParamThumbName( $this->getUrlName(), $params ); |
| 290 | + } |
| 291 | + |
| 292 | + |
| 293 | + /** |
| 294 | + * Given the name of the original, i.e. Foo.jpg, and scaling parameters, returns filename with appropriate extension |
| 295 | + * This is abstracted from getThumbName because we also use it to calculate the thumbname the file should have on |
| 296 | + * remote image scalers |
| 297 | + * |
| 298 | + * @param String $urlName: A filename, like MyMovie.ogx |
| 299 | + * @param Array $parameters: scaling parameters, like array( 'width' => '120' ); |
| 300 | + * @return String|null parameterized thumb name, like 120px-MyMovie.ogx.jpg, or null if no handler found |
| 301 | + */ |
| 302 | + function getParamThumbName( $urlName, $params ) { |
| 303 | + wfDebug( __METHOD__ . " getting for $urlName, " . print_r( $params, 1 ) . " \n" ); |
280 | 304 | if ( !$this->getHandler() ) { |
281 | 305 | return null; |
282 | 306 | } |
283 | 307 | $extension = $this->getExtension(); |
284 | 308 | list( $thumbExt, $thumbMime ) = $this->handler->getThumbType( $extension, $this->getMimeType(), $params ); |
285 | | - $thumbName = $this->getHandler()->makeParamString( $params ) . '-' . $this->getUrlName(); |
| 309 | + $thumbName = $this->getHandler()->makeParamString( $params ) . '-' . $urlName; |
286 | 310 | if ( $thumbExt != $extension ) { |
287 | 311 | $thumbName .= ".$thumbExt"; |
288 | 312 | } |
— | — | @@ -308,6 +332,7 @@ |
309 | 333 | * @return String: URL to access thumbnail, or URL with partial path |
310 | 334 | */ |
311 | 335 | public function getThumbUrl( $thumbName = false ) { |
| 336 | + wfDebug( __METHOD__ . " getting for $thumbName \n" ); |
312 | 337 | return $this->getSpecialUrl( $thumbName ); |
313 | 338 | } |
314 | 339 | |
— | — | @@ -319,7 +344,7 @@ |
320 | 345 | */ |
321 | 346 | public function getUrlName() { |
322 | 347 | if ( ! $this->urlName ) { |
323 | | - $this->urlName = $this->sessionKey . '.' . $this->getExtension(); |
| 348 | + $this->urlName = $this->sessionKey; |
324 | 349 | } |
325 | 350 | return $this->urlName; |
326 | 351 | } |
— | — | @@ -358,43 +383,6 @@ |
359 | 384 | } |
360 | 385 | |
361 | 386 | /** |
362 | | - * Typically, transform() returns a ThumbnailImage, which you can think of as being the exact |
363 | | - * equivalent of an HTML thumbnail on Wikipedia. So its URL is the full-size file, not the thumbnail's URL. |
364 | | - * |
365 | | - * Here we override transform() to stash the thumbnail file, and then |
366 | | - * provide a way to get at the stashed thumbnail file to extract properties such as its URL |
367 | | - * |
368 | | - * @param $params Array: parameters suitable for File::transform() |
369 | | - * @param $flags Integer: bitmask, flags suitable for File::transform() |
370 | | - * @return ThumbnailImage: with additional File thumbnailFile property |
371 | | - */ |
372 | | - public function transform( $params, $flags = 0 ) { |
373 | | - |
374 | | - // force it to get a thumbnail right away |
375 | | - $flags |= self::RENDER_NOW; |
376 | | - |
377 | | - // returns a ThumbnailImage object containing the url and path. Note. NOT A FILE OBJECT. |
378 | | - $thumb = parent::transform( $params, $flags ); |
379 | | - wfDebug( "UploadStash: generating thumbnail\n" ); |
380 | | - wfDebug( print_r( $thumb, 1 ) ); |
381 | | - $key = $this->thumbName($params); |
382 | | - |
383 | | - // remove extension, so it's stored in the session under '120px-123456' |
384 | | - // this makes it uniform with the other session key for the original, '123456' |
385 | | - $n = strrpos( $key, '.' ); |
386 | | - if ( $n !== false ) { |
387 | | - $key = substr( $key, 0, $n ); |
388 | | - } |
389 | | - |
390 | | - // stash the thumbnail File, and provide our caller with a way to get at its properties |
391 | | - $stashedThumbFile = $this->sessionStash->stashFile( $thumb->getPath(), array(), $key ); |
392 | | - $thumb->thumbnailFile = $stashedThumbFile; |
393 | | - |
394 | | - return $thumb; |
395 | | - |
396 | | - } |
397 | | - |
398 | | - /** |
399 | 387 | * Remove the associated temporary file |
400 | 388 | * @return Status: success |
401 | 389 | */ |
— | — | @@ -409,4 +397,5 @@ |
410 | 398 | class UploadStashBadPathException extends MWException {}; |
411 | 399 | class UploadStashBadVersionException extends MWException {}; |
412 | 400 | class UploadStashFileException extends MWException {}; |
| 401 | +class UploadStashZeroLengthFileException extends MWException {}; |
413 | 402 | |
Index: trunk/phase3/includes/specials/SpecialUploadStash.php |
— | — | @@ -66,24 +66,24 @@ |
67 | 67 | . 'use the URL of this page, with a slash and the key of the stashed file appended.'; |
68 | 68 | } else { |
69 | 69 | try { |
70 | | - $file = $this->getStashFile( $subPage ); |
71 | | - $size = $file->getSize(); |
72 | | - if ( $size === 0 ) { |
73 | | - $code = 500; |
74 | | - $message = 'File is zero length'; |
75 | | - } else if ( $size > self::MAX_SERVE_BYTES ) { |
76 | | - $code = 500; |
77 | | - $message = 'Cannot serve a file larger than ' . self::MAX_SERVE_BYTES . ' bytes'; |
| 70 | + if ( preg_match( '/^(\d+)px-(.*)$/', $subPage, $matches ) ) { |
| 71 | + list( /* full match */, $width, $key ) = $matches; |
| 72 | + return $this->outputThumbFromStash( $key, $width ); |
78 | 73 | } else { |
79 | | - $this->outputFile( $file ); |
80 | | - return true; |
| 74 | + return $this->outputFileFromStash( $subPage ); |
81 | 75 | } |
82 | 76 | } catch( UploadStashFileNotFoundException $e ) { |
83 | 77 | $code = 404; |
84 | 78 | $message = $e->getMessage(); |
| 79 | + } catch( UploadStashZeroLengthFileException $e ) { |
| 80 | + $code = 500; |
| 81 | + $message = $e->getMessage(); |
85 | 82 | } catch( UploadStashBadPathException $e ) { |
86 | 83 | $code = 500; |
87 | 84 | $message = $e->getMessage(); |
| 85 | + } catch( SpecialUploadStashTooLargeException $e ) { |
| 86 | + $code = 500; |
| 87 | + $message = 'Cannot serve a file larger than ' . self::MAX_SERVE_BYTES . ' bytes. ' . $e->getMessage(); |
88 | 88 | } catch( Exception $e ) { |
89 | 89 | $code = 500; |
90 | 90 | $message = $e->getMessage(); |
— | — | @@ -93,66 +93,164 @@ |
94 | 94 | wfHttpError( $code, OutputPage::getStatusMessage( $code ), $message ); |
95 | 95 | return false; |
96 | 96 | } |
| 97 | + |
| 98 | + /** |
| 99 | + * Get a file from stash and stream it out. Rely on parent to catch exceptions and transform them into HTTP |
| 100 | + * @param String: $key - key of this file in the stash, which probably looks like a filename with extension. |
| 101 | + * @throws ....? |
| 102 | + * @return boolean |
| 103 | + */ |
| 104 | + private function outputFileFromStash( $key ) { |
| 105 | + $file = $this->stash->getFile( $key ); |
| 106 | + $this->outputLocalFile( $file ); |
| 107 | + return true; |
| 108 | + } |
97 | 109 | |
98 | 110 | |
99 | 111 | /** |
100 | | - * Convert the incoming url portion (subpage of Special page) into a stashed file, |
101 | | - * if available. |
102 | | - * |
103 | | - * @param $subPage String |
104 | | - * @return File object |
105 | | - * @throws MWException, UploadStashFileNotFoundException, UploadStashBadPathException |
| 112 | + * Get a thumbnail for file, either generated locally or remotely, and stream it out |
| 113 | + * @param String $key: key for the file in the stash |
| 114 | + * @param int $width: width of desired thumbnail |
| 115 | + * @return ?? |
| 116 | + */ |
| 117 | + private function outputThumbFromStash( $key, $width ) { |
| 118 | + |
| 119 | + // this global, if it exists, points to a "scaler", as you might find in the Wikimedia Foundation cluster. See outputRemoteScaledThumb() |
| 120 | + global $wgUploadStashScalerBaseUrl; |
| 121 | + |
| 122 | + // let exceptions propagate to caller. |
| 123 | + $file = $this->stash->getFile( $key ); |
| 124 | + |
| 125 | + // OK, we're here and no exception was thrown, |
| 126 | + // so the original file must exist. |
| 127 | + |
| 128 | + // let's get ready to transform the original -- these are standard |
| 129 | + $params = array( 'width' => $width ); |
| 130 | + $flags = 0; |
| 131 | + |
| 132 | + return $wgUploadStashScalerBaseUrl ? $this->outputRemoteScaledThumb( $file, $params, $flags ) |
| 133 | + : $this->outputLocallyScaledThumb( $file, $params, $flags ); |
| 134 | + |
| 135 | + } |
| 136 | + |
| 137 | + |
| 138 | + /** |
| 139 | + * Scale a file (probably with a locally installed imagemagick, or similar) and output it to STDOUT. |
| 140 | + * @param $file: File object |
| 141 | + * @param $params: scaling parameters ( e.g. array( width => '50' ) ); |
| 142 | + * @param $flags: scaling flags ( see File:: constants ) |
| 143 | + * @throws MWException |
| 144 | + * @return boolean success |
106 | 145 | */ |
107 | | - private function getStashFile( $subPage ) { |
108 | | - // due to an implementation quirk (and trying to be compatible with older method) |
109 | | - // the stash key doesn't have an extension |
110 | | - $key = $subPage; |
111 | | - $n = strrpos( $subPage, '.' ); |
112 | | - if ( $n !== false ) { |
113 | | - $key = $n ? substr( $subPage, 0, $n ) : $subPage; |
114 | | - } |
| 146 | + private function outputLocallyScaledThumb( $params, $flags ) { |
| 147 | + wfDebug( "UploadStash: SCALING locally!\n" ); |
115 | 148 | |
116 | | - try { |
117 | | - $file = $this->stash->getFile( $key ); |
118 | | - } catch ( UploadStashFileNotFoundException $e ) { |
119 | | - // if we couldn't find it, and it looks like a thumbnail, |
120 | | - // and it looks like we have the original, go ahead and generate it |
121 | | - $matches = array(); |
122 | | - if ( ! preg_match( '/^(\d+)px-(.*)$/', $key, $matches ) ) { |
123 | | - // that doesn't look like a thumbnail. re-raise exception |
124 | | - throw $e; |
125 | | - } |
| 149 | + // n.b. this is stupid, we insist on re-transforming the file every time we are invoked. We rely |
| 150 | + // on HTTP caching to ensure this doesn't happen. |
| 151 | + |
| 152 | + $flags |= File::RENDER_NOW; |
126 | 153 | |
127 | | - list( , $width, $origKey ) = $matches; |
| 154 | + $thumbnailImage = $file->transform( $params, $flags ); |
| 155 | + if ( !$thumbnailImage ) { |
| 156 | + throw new MWException( 'Could not obtain thumbnail' ); |
| 157 | + } |
128 | 158 | |
129 | | - // do not trap exceptions, if key is in bad format, or file not found, |
130 | | - // let exceptions propagate to caller. |
131 | | - $origFile = $this->stash->getFile( $origKey ); |
| 159 | + // we should have just generated it locally |
| 160 | + if ( ! $thumbnailImage->getPath() ) { |
| 161 | + throw new UploadStashFileNotFoundException( "no local path for scaled item" ); |
| 162 | + } |
132 | 163 | |
133 | | - // ok we're here so the original must exist. Generate the thumbnail. |
134 | | - // because the file is a UploadStashFile, this thumbnail will also be stashed, |
135 | | - // and a thumbnailFile will be created in the thumbnailImage composite object |
136 | | - $thumbnailImage = $origFile->transform( array( 'width' => $width ) ); |
137 | | - if ( !$thumbnailImage ) { |
138 | | - throw new MWException( 'Could not obtain thumbnail' ); |
139 | | - } |
140 | | - $file = $thumbnailImage->thumbnailFile; |
| 164 | + // now we should construct a File, so we can get mime and other such info in a standard way |
| 165 | + // n.b. mimetype may be different from original (ogx original -> jpeg thumb) |
| 166 | + $thumbFile = new UnregisteredLocalFile( false, $this->stash->repo, $thumbnailImage->getPath(), false ); |
| 167 | + if ( ! $thumbFile ) { |
| 168 | + throw new UploadStashFileNotFoundException( "couldn't create local file object for thumbnail" ); |
141 | 169 | } |
142 | 170 | |
143 | | - return $file; |
| 171 | + return $this->outputLocalFile( $thumbFile ); |
| 172 | + |
144 | 173 | } |
| 174 | + |
| 175 | + /** |
| 176 | + * Scale a file with a remote "scaler", as exists on the Wikimedia Foundation cluster, and output it to STDOUT. |
| 177 | + * Note: unlike the usual thumbnail process, the web client never sees the cluster URL; we do the whole HTTP transaction to the scaler ourselves |
| 178 | + * and cat the results out. |
| 179 | + * Note: We rely on NFS to have propagated the file contents to the scaler. However, we do not rely on the thumbnail being created in NFS and then |
| 180 | + * propagated back to our filesystem. Instead we take the results of the HTTP request instead. |
| 181 | + * Note: no caching is being done here, although we are instructing the client to cache it forever. |
| 182 | + * @param $file: File object |
| 183 | + * @param $params: scaling parameters ( e.g. array( width => '50' ) ); |
| 184 | + * @param $flags: scaling flags ( see File:: constants ) |
| 185 | + * @throws MWException |
| 186 | + * @return boolean success |
| 187 | + */ |
| 188 | + private function outputRemoteScaledThumb( $file, $params, $flags ) { |
| 189 | + |
| 190 | + // this global probably looks something like 'http://upload.wikimedia.org/wikipedia/test/thumb/temp' |
| 191 | + // do not use trailing slash |
| 192 | + global $wgUploadStashScalerBaseUrl; |
145 | 193 | |
| 194 | + $scalerThumbName = $file->getParamThumbName( $file->name, $params ); |
| 195 | + $scalerThumbUrl = $wgUploadStashScalerBaseUrl . '/' . $file->getRel() . '/' . $scalerThumbName; |
| 196 | + // make a CURL call to the scaler to create a thumbnail |
| 197 | + wfDebug( "UploadStash: calling " . $scalerThumbUrl . " with curl \n" ); |
| 198 | + $req = MWHttpRequest::factory( $thumbScalerUrl ); |
| 199 | + $status = $req->execute(); |
| 200 | + if ( ! $status->isOK() ) { |
| 201 | + throw new MWException( "Fetching thumbnail failed" ); |
| 202 | + } |
| 203 | + $contentType = $req->getResponseHeader( "content-type" ); |
| 204 | + if ( ! $contentType ) { |
| 205 | + throw new MWException( "Missing content-type header" ); |
| 206 | + } |
| 207 | + return $this->outputFromContent( $req->getContent(), $contentType ); |
| 208 | + } |
| 209 | + |
146 | 210 | /** |
147 | 211 | * Output HTTP response for file |
148 | | - * Side effects, obviously, of echoing lots of stuff to stdout. |
| 212 | + * Side effect: writes HTTP response to STDOUT. |
| 213 | + * XXX could use wfStreamfile (in includes/Streamfile.php), but for consistency with outputContents() doing it this way. |
| 214 | + * XXX is mimeType really enough, or do we need encoding for full Content-Type header? |
149 | 215 | * |
150 | | - * @param $file File object |
| 216 | + * @param $file File object with a local path (e.g. UnregisteredLocalFile, LocalFile. Oddly these don't share an ancestor!) |
151 | 217 | */ |
152 | | - private function outputFile( $file ) { |
153 | | - header( 'Content-Type: ' . $file->getMimeType(), true ); |
| 218 | + private function outputLocalFile( $file ) { |
| 219 | + if ( $file->getSize() > self::MAX_SERVE_BYTES ) { |
| 220 | + throw new SpecialUploadStashTooLargeException(); |
| 221 | + } |
| 222 | + self::outputHeaders( $file->getMimeType(), $file->getSize() ); |
| 223 | + readfile( $file->getPath() ); |
| 224 | + } |
| 225 | + |
| 226 | + /** |
| 227 | + * Output HTTP response of raw content |
| 228 | + * Side effect: writes HTTP response to STDOUT. |
| 229 | + * @param String $content: content |
| 230 | + * @param String $mimeType: mime type |
| 231 | + */ |
| 232 | + private function outputContents( $content, $contentType ) { |
| 233 | + $size = strlen( $content ); |
| 234 | + if ( $size > self::MAX_SERVE_BYTES ) { |
| 235 | + throw new SpecialUploadStashTooLargeException(); |
| 236 | + } |
| 237 | + self::outputHeaders( $contentType, $size ); |
| 238 | + print $content; |
| 239 | + } |
| 240 | + |
| 241 | + /** |
| 242 | + * Output headers for streaming |
| 243 | + * XXX unsure about encoding as binary; if we received from HTTP perhaps we should use that encoding, concatted with semicolon to mimeType as it usually is. |
| 244 | + * Side effect: preps PHP to write headers to STDOUT. |
| 245 | + * @param String $contentType : string suitable for content-type header |
| 246 | + * @param String $size: length in bytes |
| 247 | + */ |
| 248 | + private static function outputHeaders( $contentType, $size ) { |
| 249 | + header( "Content-Type: $mimeType", true ); |
154 | 250 | header( 'Content-Transfer-Encoding: binary', true ); |
155 | 251 | header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true ); |
156 | | - header( 'Content-Length: ' . $file->getSize(), true ); |
157 | | - readfile( $file->getPath() ); |
| 252 | + header( "Content-Length: $size", true ); |
158 | 253 | } |
| 254 | + |
159 | 255 | } |
| 256 | + |
| 257 | +class SpecialUploadStashTooLargeException extends MWException {}; |