Index: branches/uploadwizard/phase3/maintenance/tests/phpunit/includes/api/ApiUploadTest.php |
— | — | @@ -521,7 +521,7 @@ |
522 | 522 | $this->assertTrue( isset( $result['upload']['sessionkey'] ) ); |
523 | 523 | $sessionkey = $result['upload']['sessionkey']; |
524 | 524 | |
525 | | - // it should be visible from Special:PrivateUploadStash |
| 525 | + // it should be visible from Special:UploadStash |
526 | 526 | // XXX ...but how to test this, with a fake WebRequest with the session? |
527 | 527 | |
528 | 528 | // now we should try to release the file from stash |
Index: branches/uploadwizard/phase3/includes/upload/PrivateUploadStash.php |
— | — | @@ -1,406 +0,0 @@ |
2 | | -<?php |
3 | | -/** |
4 | | - * PrivateUploadStash is intended to accomplish a few things: |
5 | | - * - enable applications to temporarily stash files without publishing them to the wiki. |
6 | | - * - Several parts of MediaWiki do this in similar ways: UploadBase, UploadWizard, and FirefoggChunkedExtension |
7 | | - * And there are several that reimplement stashing from scratch, in idiosyncratic ways. The idea is to unify them all here. |
8 | | - * Mostly all of them are the same except for storing some custom fields, which we subsume into the data array. |
9 | | - * - enable applications to find said files later, as long as the session or temp files haven't been purged. |
10 | | - * - enable the uploading user (and *ONLY* the uploading user) to access said files, and thumbnails of said files, via a URL. |
11 | | - * We accomplish this by making the session serve as a URL->file mapping, on the assumption that nobody else can access |
12 | | - * the session, even the uploading user. See SpecialPrivateUploadStash, which implements a web interface to some files stored this way. |
13 | | - * |
14 | | - */ |
15 | | -class PrivateUploadStash { |
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 | | - |
22 | | - // repository that this uses to store temp files |
23 | | - protected $repo; |
24 | | - |
25 | | - // array of initialized objects obtained from session (lazily initialized upon getFile()) |
26 | | - private $files = array(); |
27 | | - |
28 | | - // the base URL for files in the stash |
29 | | - private $baseUrl; |
30 | | - |
31 | | - // TODO: Once UploadBase starts using this, switch to use these constants rather than UploadBase::SESSION* |
32 | | - // const SESSION_VERSION = 2; |
33 | | - // const SESSION_KEYNAME = 'wsUploadData'; |
34 | | - |
35 | | - /** |
36 | | - * Represents the session which contains temporarily stored files. |
37 | | - * Designed to be compatible with the session stashing code in UploadBase (should replace it eventually) |
38 | | - * @param {FileRepo} $repo: optional -- repo in which to store files. Will choose LocalRepo if not supplied. |
39 | | - */ |
40 | | - public function __construct( $repo = null ) { |
41 | | - |
42 | | - if ( is_null( $repo ) ) { |
43 | | - $repo = RepoGroup::singleton()->getLocalRepo(); |
44 | | - } |
45 | | - |
46 | | - $this->repo = $repo; |
47 | | - |
48 | | - if ( ! isset( $_SESSION ) ) { |
49 | | - throw new PrivateUploadStashNotAvailableException( 'no session variable' ); |
50 | | - } |
51 | | - |
52 | | - if ( !isset( $_SESSION[UploadBase::SESSION_KEYNAME] ) ) { |
53 | | - $_SESSION[UploadBase::SESSION_KEYNAME] = array(); |
54 | | - } |
55 | | - |
56 | | - $this->baseUrl = SpecialPage::getTitleFor( 'PrivateUploadStash' )->getLocalURL(); |
57 | | - } |
58 | | - |
59 | | - /** |
60 | | - * Get the base of URLs by which one can access the files |
61 | | - * @return {String} url |
62 | | - */ |
63 | | - public function getBaseUrl() { |
64 | | - return $this->baseUrl; |
65 | | - } |
66 | | - |
67 | | - /** |
68 | | - * Get a file and its metadata from the stash. |
69 | | - * May throw exception if session data cannot be parsed due to schema change, or key not found. |
70 | | - * @param {Integer} $key: key |
71 | | - * @throws PrivateUploadStashFileNotFoundException |
72 | | - * @throws PrivateUploadStashBadVersionException |
73 | | - * @return {PrivateUploadStashItem} null if no such item or item out of date, or the item |
74 | | - */ |
75 | | - public function getFile( $key ) { |
76 | | - if ( ! preg_match( self::KEY_FORMAT_REGEX, $key ) ) { |
77 | | - throw new PrivateUploadStashBadPathException( "key '$key' is not in a proper format" ); |
78 | | - } |
79 | | - |
80 | | - if ( !isset( $this->files[$key] ) ) { |
81 | | - if ( !isset( $_SESSION[UploadBase::SESSION_KEYNAME][$key] ) ) { |
82 | | - throw new PrivateUploadStashFileNotFoundException( "key '$key' not found in session" ); |
83 | | - } |
84 | | - |
85 | | - $data = $_SESSION[UploadBase::SESSION_KEYNAME][$key]; |
86 | | - // guards against PHP class changing while session data doesn't |
87 | | - if ($data['version'] !== UploadBase::SESSION_VERSION ) { |
88 | | - throw new PrivateUploadStashBadVersionException( $data['version'] . " does not match current version " . UploadBase::SESSION_VERSION ); |
89 | | - } |
90 | | - |
91 | | - // separate the stashData into the path, and then the rest of the data |
92 | | - $path = $data['mTempPath']; |
93 | | - unset( $data['mTempPath'] ); |
94 | | - |
95 | | - $file = new PrivateUploadStashFile( $this, $this->repo, $path, $key, $data ); |
96 | | - |
97 | | - $this->files[$key] = $file; |
98 | | - |
99 | | - } |
100 | | - return $this->files[$key]; |
101 | | - } |
102 | | - |
103 | | - /** |
104 | | - * Stash a file in a temp directory and record that we did this in the session, along with other metadata. |
105 | | - * We store data in a flat key-val namespace because that's how UploadBase did it. This also means we have to |
106 | | - * ensure that the key-val pairs in $data do not overwrite other required fields. |
107 | | - * |
108 | | - * @param {String} $path: path to file you want stashed |
109 | | - * @param {Array} $data: optional, other data you want associated with the file. Do not use 'mTempPath', 'mFileProps', 'mFileSize', or 'version' as keys here |
110 | | - * @param {String} $key: optional, unique key for this file in this session. Used for directory hashing when storing, otherwise not important |
111 | | - * @throws PrivateUploadStashBadPathException |
112 | | - * @throws PrivateUploadStashFileException |
113 | | - * @return {null|PrivateUploadStashFile} file, or null on failure |
114 | | - */ |
115 | | - public function stashFile( $path, $data = array(), $key = null ) { |
116 | | - if ( ! file_exists( $path ) ) { |
117 | | - throw new PrivateUploadStashBadPathException( "path '$path' doesn't exist" ); |
118 | | - } |
119 | | - $fileProps = File::getPropsFromPath( $path ); |
120 | | - |
121 | | - // If no key was supplied, use content hash. Also has the nice property of collapsing multiple identical files |
122 | | - // uploaded this session, which could happen if uploads had failed. |
123 | | - if ( is_null( $key ) ) { |
124 | | - $key = $fileProps['sha1']; |
125 | | - } |
126 | | - |
127 | | - if ( ! preg_match( self::KEY_FORMAT_REGEX, $key ) ) { |
128 | | - throw new PrivateUploadStashBadPathException( "key '$key' is not in a proper format" ); |
129 | | - } |
130 | | - |
131 | | - // if not already in a temporary area, put it there |
132 | | - $status = $this->repo->storeTemp( basename( $path ), $path ); |
133 | | - if( ! $status->isOK() ) { |
134 | | - // It is a convention in MediaWiki to only return one error per API exception, even if multiple errors |
135 | | - // are available. We use reset() to pick the "first" thing that was wrong, preferring errors to warnings. |
136 | | - // This is a bit lame, as we may have more info in the $status and we're throwing it away, but to fix it means |
137 | | - // redesigning API errors significantly. |
138 | | - // $status->value just contains the virtual URL (if anything) which is probably useless to the caller |
139 | | - $error = reset( $status->getErrorsArray() ); |
140 | | - if ( ! count( $error ) ) { |
141 | | - $error = reset( $status->getWarningsArray() ); |
142 | | - if ( ! count( $error ) ) { |
143 | | - $error = array( 'unknown', 'no error recorded' ); |
144 | | - } |
145 | | - } |
146 | | - throw new PrivateUploadStashFileException( "error storing file in '$path': " . implode( '; ', $error ) ); |
147 | | - } |
148 | | - $stashPath = $status->value; |
149 | | - |
150 | | - // required info we always store. Must trump any other application info in $data |
151 | | - // 'mTempPath', 'mFileSize', and 'mFileProps' are arbitrary names |
152 | | - // chosen for compatibility with UploadBase's way of doing this. |
153 | | - $requiredData = array( |
154 | | - 'mTempPath' => $stashPath, |
155 | | - 'mFileSize' => $fileProps['size'], |
156 | | - 'mFileProps' => $fileProps, |
157 | | - 'version' => UploadBase::SESSION_VERSION |
158 | | - ); |
159 | | - |
160 | | - // now, merge required info and extra data into the session. (The extra data changes from application to application. |
161 | | - // UploadWizard wants different things than say FirefoggChunkedUpload.) |
162 | | - $_SESSION[UploadBase::SESSION_KEYNAME][$key] = array_merge( $data, $requiredData ); |
163 | | - |
164 | | - return $this->getFile( $key ); |
165 | | - } |
166 | | - |
167 | | -} |
168 | | - |
169 | | -class PrivateUploadStashFile extends UnregisteredLocalFile { |
170 | | - private $sessionStash; |
171 | | - private $sessionKey; |
172 | | - private $sessionData; |
173 | | - private $urlName; |
174 | | - |
175 | | - /** |
176 | | - * A LocalFile wrapper around a file that has been temporarily stashed, so we can do things like create thumbnails for it |
177 | | - * Arguably UnregisteredLocalFile should be handling its own file repo but that class is a bit retarded currently |
178 | | - * @param {PrivateUploadStash} $stash: PrivateUploadStash, useful for obtaining config, stashing transformed files |
179 | | - * @param {FileRepo} $repo: repository where we should find the path |
180 | | - * @param {String} $path: path to file |
181 | | - * @param {String} $key: key to store the path and any stashed data under |
182 | | - * @param {String} $data: any other data we want stored with this file |
183 | | - * @throws PrivateUploadStashBadPathException |
184 | | - * @throws PrivateUploadStashFileNotFoundException |
185 | | - */ |
186 | | - public function __construct( $stash, $repo, $path, $key, $data ) { |
187 | | - $this->sessionStash = $stash; |
188 | | - $this->sessionKey = $key; |
189 | | - $this->sessionData = $data; |
190 | | - |
191 | | - // resolve mwrepo:// urls |
192 | | - if ( $repo->isVirtualUrl( $path ) ) { |
193 | | - $path = $repo->resolveVirtualUrl( $path ); |
194 | | - } |
195 | | - |
196 | | - // check if path appears to be sane, no parent traversals, and is in this repo's temp zone. |
197 | | - $repoTempPath = $repo->getZonePath( 'temp' ); |
198 | | - if ( ( ! $repo->validateFilename( $path ) ) || |
199 | | - ( strpos( $path, $repoTempPath ) !== 0 ) ) { |
200 | | - throw new PrivateUploadStashBadPathException( "path '$path' is not valid or is not in repo temp area: '$repoTempPath'" ); |
201 | | - } |
202 | | - |
203 | | - // check if path exists! and is a plain file. |
204 | | - if ( ! $repo->fileExists( $path, FileRepo::FILES_ONLY ) ) { |
205 | | - throw new PrivateUploadStashFileNotFoundException( "cannot find path '$path'" ); |
206 | | - } |
207 | | - |
208 | | - parent::__construct( false, $repo, $path, false ); |
209 | | - |
210 | | - // we will be initializing from some tmpnam files that don't have extensions. |
211 | | - // most of MediaWiki assumes all uploaded files have good extensions. So, we fix this. |
212 | | - $this->name = basename( $this->path ); |
213 | | - $this->setExtension(); |
214 | | - |
215 | | - } |
216 | | - |
217 | | - /** |
218 | | - * A method needed by the file transforming and scaling routines in File.php |
219 | | - * We do not necessarily care about doing the description at this point |
220 | | - * However, we also can't return the empty string, as the rest of MediaWiki demands this (and calls to imagemagick |
221 | | - * convert require it to be there) |
222 | | - * @return {String} dummy value |
223 | | - */ |
224 | | - public function getDescriptionUrl() { |
225 | | - return $this->getUrl(); |
226 | | - } |
227 | | - |
228 | | - /** |
229 | | - * Find or guess extension -- ensuring that our extension matches our mime type. |
230 | | - * Since these files are constructed from php tempnames they may not start off |
231 | | - * with an extension. |
232 | | - * This does not override getExtension() because things like getMimeType() already call getExtension(), |
233 | | - * and that results in infinite recursion. So, we preemptively *set* the extension so getExtension() can find it. |
234 | | - * For obvious reasons this should be called as early as possible, as part of initialization |
235 | | - */ |
236 | | - public function setExtension() { |
237 | | - // Does this have an extension? |
238 | | - $n = strrpos( $this->path, '.' ); |
239 | | - $extension = null; |
240 | | - if ( $n !== false ) { |
241 | | - $extension = $n ? substr( $this->path, $n + 1 ) : ''; |
242 | | - } else { |
243 | | - // If not, assume that it should be related to the mime type of the original file. |
244 | | - // |
245 | | - // This entire thing is backwards -- we *should* just create an extension based on |
246 | | - // the mime type of the transformed file, *after* transformation. But File.php demands |
247 | | - // to know the name of the transformed file before creating it. |
248 | | - $mimeType = $this->getMimeType(); |
249 | | - $extensions = explode( ' ', MimeMagic::singleton()->getExtensionsForType( $mimeType ) ); |
250 | | - if ( count( $extensions ) ) { |
251 | | - $extension = $extensions[0]; |
252 | | - } |
253 | | - } |
254 | | - |
255 | | - if ( is_null( $extension ) ) { |
256 | | - throw new PrivateUploadStashFileException( "extension '$extension' is null" ); |
257 | | - } |
258 | | - |
259 | | - $this->extension = parent::normalizeExtension( $extension ); |
260 | | - } |
261 | | - |
262 | | - /** |
263 | | - * Get the path for the thumbnail (actually any transformation of this file) |
264 | | - * The actual argument is the result of thumbName although we seem to have |
265 | | - * buggy code elsewhere that expects a boolean 'suffix' |
266 | | - * |
267 | | - * @param {String|false} $thumbName: name of thumbnail (e.g. "120px-123456.jpg" ), or false to just get the path |
268 | | - * @return {String} path thumbnail should take on filesystem, or containing directory if thumbname is false |
269 | | - */ |
270 | | - public function getThumbPath( $thumbName = false ) { |
271 | | - $path = dirname( $this->path ); |
272 | | - if ( $thumbName !== false ) { |
273 | | - $path .= "/$thumbName"; |
274 | | - } |
275 | | - return $path; |
276 | | - } |
277 | | - |
278 | | - /** |
279 | | - * Return the file/url base name of a thumbnail with the specified parameters |
280 | | - * |
281 | | - * @param {Array} $params: handler-specific parameters |
282 | | - * @return {String|null} base name for URL, like '120px-12345.jpg', or null if there is no handler |
283 | | - */ |
284 | | - function thumbName( $params ) { |
285 | | - if ( !$this->getHandler() ) { |
286 | | - return null; |
287 | | - } |
288 | | - $extension = $this->getExtension(); |
289 | | - list( $thumbExt, $thumbMime ) = $this->handler->getThumbType( $extension, $this->getMimeType(), $params ); |
290 | | - $thumbName = $this->getHandler()->makeParamString( $params ) . '-' . $this->getUrlName(); |
291 | | - if ( $thumbExt != $extension ) { |
292 | | - $thumbName .= ".$thumbExt"; |
293 | | - } |
294 | | - return $thumbName; |
295 | | - } |
296 | | - |
297 | | - /** |
298 | | - * Get a URL to access the thumbnail |
299 | | - * This is required because the model of how files work requires that |
300 | | - * the thumbnail urls be predictable. However, in our model the URL is not based on the filename |
301 | | - * (that's hidden in the session) |
302 | | - * |
303 | | - * @param {String} $thumbName: basename of thumbnail file -- however, we don't want to use the file exactly |
304 | | - * @return {String} URL to access thumbnail, or URL with partial path |
305 | | - */ |
306 | | - public function getThumbUrl( $thumbName = false ) { |
307 | | - $path = $this->sessionStash->getBaseUrl(); |
308 | | - if ( $thumbName !== false ) { |
309 | | - $path .= '/' . rawurlencode( $thumbName ); |
310 | | - } |
311 | | - return $path; |
312 | | - } |
313 | | - |
314 | | - /** |
315 | | - * The basename for the URL, which we want to not be related to the filename. |
316 | | - * Will also be used as the lookup key for a thumbnail file. |
317 | | - * @return {String} base url name, like '120px-123456.jpg' |
318 | | - */ |
319 | | - public function getUrlName() { |
320 | | - if ( ! $this->urlName ) { |
321 | | - $this->urlName = $this->sessionKey . '.' . $this->getExtension(); |
322 | | - } |
323 | | - return $this->urlName; |
324 | | - } |
325 | | - |
326 | | - /** |
327 | | - * Return the URL of the file, if for some reason we wanted to download it |
328 | | - * We tend not to do this for the original file, but we do want thumb icons |
329 | | - * @return {String} url |
330 | | - */ |
331 | | - public function getUrl() { |
332 | | - if ( !isset( $this->url ) ) { |
333 | | - $this->url = $this->sessionStash->getBaseUrl() . '/' . $this->getUrlName(); |
334 | | - } |
335 | | - return $this->url; |
336 | | - } |
337 | | - |
338 | | - /** |
339 | | - * Parent classes use this method, for no obvious reason, to return the path (relative to wiki root, I assume). |
340 | | - * But with this class, the URL is unrelated to the path. |
341 | | - * |
342 | | - * @return {String} url |
343 | | - */ |
344 | | - public function getFullUrl() { |
345 | | - return $this->getUrl(); |
346 | | - } |
347 | | - |
348 | | - |
349 | | - /** |
350 | | - * Getter for session key (the session-unique id by which this file's location & metadata is stored in the session) |
351 | | - * @return {String} session key |
352 | | - */ |
353 | | - public function getSessionKey() { |
354 | | - return $this->sessionKey; |
355 | | - } |
356 | | - |
357 | | - /** |
358 | | - * Typically, transform() returns a ThumbnailImage, which you can think of as being the exact |
359 | | - * equivalent of an HTML thumbnail on Wikipedia. So its URL is the full-size file, not the thumbnail's URL. |
360 | | - * |
361 | | - * Here we override transform() to stash the thumbnail file, and then |
362 | | - * provide a way to get at the stashed thumbnail file to extract properties such as its URL |
363 | | - * |
364 | | - * @param {Array} $params: parameters suitable for File::transform() |
365 | | - * @param {Bitmask} $flags: flags suitable for File::transform() |
366 | | - * @return {ThumbnailImage} with additional File thumbnailFile property |
367 | | - */ |
368 | | - public function transform( $params, $flags = 0 ) { |
369 | | - |
370 | | - // force it to get a thumbnail right away |
371 | | - $flags |= self::RENDER_NOW; |
372 | | - |
373 | | - // returns a ThumbnailImage object containing the url and path. Note. NOT A FILE OBJECT. |
374 | | - $thumb = parent::transform( $params, $flags ); |
375 | | - $key = $this->thumbName($params); |
376 | | - |
377 | | - // remove extension, so it's stored in the session under '120px-123456' |
378 | | - // this makes it uniform with the other session key for the original, '123456' |
379 | | - $n = strrpos( $key, '.' ); |
380 | | - if ( $n !== false ) { |
381 | | - $key = substr( $key, 0, $n ); |
382 | | - } |
383 | | - |
384 | | - // stash the thumbnail File, and provide our caller with a way to get at its properties |
385 | | - $stashedThumbFile = $this->sessionStash->stashFile( $thumb->path, array(), $key ); |
386 | | - $thumb->thumbnailFile = $stashedThumbFile; |
387 | | - |
388 | | - return $thumb; |
389 | | - |
390 | | - } |
391 | | - |
392 | | - /** |
393 | | - * Remove the associated temporary file |
394 | | - * @return {Status} success |
395 | | - */ |
396 | | - public function remove() { |
397 | | - return $this->repo->freeTemp( $this->path ); |
398 | | - } |
399 | | - |
400 | | -} |
401 | | - |
402 | | -class PrivateUploadStashNotAvailableException extends MWException {}; |
403 | | -class PrivateUploadStashFileNotFoundException extends MWException {}; |
404 | | -class PrivateUploadStashBadPathException extends MWException {}; |
405 | | -class PrivateUploadStashBadVersionException extends MWException {}; |
406 | | -class PrivateUploadStashFileException extends MWException {}; |
407 | | - |
Index: branches/uploadwizard/phase3/includes/upload/UploadBase.php |
— | — | @@ -600,7 +600,7 @@ |
601 | 601 | } |
602 | 602 | |
603 | 603 | /** |
604 | | - * NOTE: Probably should be deprecated in favor of PrivateUploadStash, but this is sometimes |
| 604 | + * NOTE: Probably should be deprecated in favor of UploadStash, but this is sometimes |
605 | 605 | * called outside that context. |
606 | 606 | * |
607 | 607 | * Stash a file in a temporary directory for later processing |
— | — | @@ -632,7 +632,7 @@ |
633 | 633 | * @return {File}: stashed file |
634 | 634 | */ |
635 | 635 | public function stashSessionFile( $key = null ) { |
636 | | - $stash = new PrivateUploadStash(); |
| 636 | + $stash = new UploadStash(); |
637 | 637 | $data = array( |
638 | 638 | 'mFileProps' => $this->mFileProps |
639 | 639 | ); |
Index: branches/uploadwizard/phase3/includes/upload/UploadStash.php |
— | — | @@ -0,0 +1,406 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * UploadStash is intended to accomplish a few things: |
| 5 | + * - enable applications to temporarily stash files without publishing them to the wiki. |
| 6 | + * - Several parts of MediaWiki do this in similar ways: UploadBase, UploadWizard, and FirefoggChunkedExtension |
| 7 | + * And there are several that reimplement stashing from scratch, in idiosyncratic ways. The idea is to unify them all here. |
| 8 | + * Mostly all of them are the same except for storing some custom fields, which we subsume into the data array. |
| 9 | + * - enable applications to find said files later, as long as the session or temp files haven't been purged. |
| 10 | + * - enable the uploading user (and *ONLY* the uploading user) to access said files, and thumbnails of said files, via a URL. |
| 11 | + * We accomplish this by making the session serve as a URL->file mapping, on the assumption that nobody else can access |
| 12 | + * the session, even the uploading user. See SpecialUploadStash, which implements a web interface to some files stored this way. |
| 13 | + * |
| 14 | + */ |
| 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 | + |
| 22 | + // repository that this uses to store temp files |
| 23 | + protected $repo; |
| 24 | + |
| 25 | + // array of initialized objects obtained from session (lazily initialized upon getFile()) |
| 26 | + private $files = array(); |
| 27 | + |
| 28 | + // the base URL for files in the stash |
| 29 | + private $baseUrl; |
| 30 | + |
| 31 | + // TODO: Once UploadBase starts using this, switch to use these constants rather than UploadBase::SESSION* |
| 32 | + // const SESSION_VERSION = 2; |
| 33 | + // const SESSION_KEYNAME = 'wsUploadData'; |
| 34 | + |
| 35 | + /** |
| 36 | + * Represents the session which contains temporarily stored files. |
| 37 | + * Designed to be compatible with the session stashing code in UploadBase (should replace it eventually) |
| 38 | + * @param {FileRepo} $repo: optional -- repo in which to store files. Will choose LocalRepo if not supplied. |
| 39 | + */ |
| 40 | + public function __construct( $repo = null ) { |
| 41 | + |
| 42 | + if ( is_null( $repo ) ) { |
| 43 | + $repo = RepoGroup::singleton()->getLocalRepo(); |
| 44 | + } |
| 45 | + |
| 46 | + $this->repo = $repo; |
| 47 | + |
| 48 | + if ( ! isset( $_SESSION ) ) { |
| 49 | + throw new UploadStashNotAvailableException( 'no session variable' ); |
| 50 | + } |
| 51 | + |
| 52 | + if ( !isset( $_SESSION[UploadBase::SESSION_KEYNAME] ) ) { |
| 53 | + $_SESSION[UploadBase::SESSION_KEYNAME] = array(); |
| 54 | + } |
| 55 | + |
| 56 | + $this->baseUrl = SpecialPage::getTitleFor( 'UploadStash' )->getLocalURL(); |
| 57 | + } |
| 58 | + |
| 59 | + /** |
| 60 | + * Get the base of URLs by which one can access the files |
| 61 | + * @return {String} url |
| 62 | + */ |
| 63 | + public function getBaseUrl() { |
| 64 | + return $this->baseUrl; |
| 65 | + } |
| 66 | + |
| 67 | + /** |
| 68 | + * Get a file and its metadata from the stash. |
| 69 | + * May throw exception if session data cannot be parsed due to schema change, or key not found. |
| 70 | + * @param {Integer} $key: key |
| 71 | + * @throws UploadStashFileNotFoundException |
| 72 | + * @throws UploadStashBadVersionException |
| 73 | + * @return {UploadStashItem} null if no such item or item out of date, or the item |
| 74 | + */ |
| 75 | + public function getFile( $key ) { |
| 76 | + if ( ! preg_match( self::KEY_FORMAT_REGEX, $key ) ) { |
| 77 | + throw new UploadStashBadPathException( "key '$key' is not in a proper format" ); |
| 78 | + } |
| 79 | + |
| 80 | + if ( !isset( $this->files[$key] ) ) { |
| 81 | + if ( !isset( $_SESSION[UploadBase::SESSION_KEYNAME][$key] ) ) { |
| 82 | + throw new UploadStashFileNotFoundException( "key '$key' not found in session" ); |
| 83 | + } |
| 84 | + |
| 85 | + $data = $_SESSION[UploadBase::SESSION_KEYNAME][$key]; |
| 86 | + // guards against PHP class changing while session data doesn't |
| 87 | + if ($data['version'] !== UploadBase::SESSION_VERSION ) { |
| 88 | + throw new UploadStashBadVersionException( $data['version'] . " does not match current version " . UploadBase::SESSION_VERSION ); |
| 89 | + } |
| 90 | + |
| 91 | + // separate the stashData into the path, and then the rest of the data |
| 92 | + $path = $data['mTempPath']; |
| 93 | + unset( $data['mTempPath'] ); |
| 94 | + |
| 95 | + $file = new UploadStashFile( $this, $this->repo, $path, $key, $data ); |
| 96 | + |
| 97 | + $this->files[$key] = $file; |
| 98 | + |
| 99 | + } |
| 100 | + return $this->files[$key]; |
| 101 | + } |
| 102 | + |
| 103 | + /** |
| 104 | + * Stash a file in a temp directory and record that we did this in the session, along with other metadata. |
| 105 | + * We store data in a flat key-val namespace because that's how UploadBase did it. This also means we have to |
| 106 | + * ensure that the key-val pairs in $data do not overwrite other required fields. |
| 107 | + * |
| 108 | + * @param {String} $path: path to file you want stashed |
| 109 | + * @param {Array} $data: optional, other data you want associated with the file. Do not use 'mTempPath', 'mFileProps', 'mFileSize', or 'version' as keys here |
| 110 | + * @param {String} $key: optional, unique key for this file in this session. Used for directory hashing when storing, otherwise not important |
| 111 | + * @throws UploadStashBadPathException |
| 112 | + * @throws UploadStashFileException |
| 113 | + * @return {null|UploadStashFile} file, or null on failure |
| 114 | + */ |
| 115 | + public function stashFile( $path, $data = array(), $key = null ) { |
| 116 | + if ( ! file_exists( $path ) ) { |
| 117 | + throw new UploadStashBadPathException( "path '$path' doesn't exist" ); |
| 118 | + } |
| 119 | + $fileProps = File::getPropsFromPath( $path ); |
| 120 | + |
| 121 | + // If no key was supplied, use content hash. Also has the nice property of collapsing multiple identical files |
| 122 | + // uploaded this session, which could happen if uploads had failed. |
| 123 | + if ( is_null( $key ) ) { |
| 124 | + $key = $fileProps['sha1']; |
| 125 | + } |
| 126 | + |
| 127 | + if ( ! preg_match( self::KEY_FORMAT_REGEX, $key ) ) { |
| 128 | + throw new UploadStashBadPathException( "key '$key' is not in a proper format" ); |
| 129 | + } |
| 130 | + |
| 131 | + // if not already in a temporary area, put it there |
| 132 | + $status = $this->repo->storeTemp( basename( $path ), $path ); |
| 133 | + if( ! $status->isOK() ) { |
| 134 | + // It is a convention in MediaWiki to only return one error per API exception, even if multiple errors |
| 135 | + // are available. We use reset() to pick the "first" thing that was wrong, preferring errors to warnings. |
| 136 | + // This is a bit lame, as we may have more info in the $status and we're throwing it away, but to fix it means |
| 137 | + // redesigning API errors significantly. |
| 138 | + // $status->value just contains the virtual URL (if anything) which is probably useless to the caller |
| 139 | + $error = reset( $status->getErrorsArray() ); |
| 140 | + if ( ! count( $error ) ) { |
| 141 | + $error = reset( $status->getWarningsArray() ); |
| 142 | + if ( ! count( $error ) ) { |
| 143 | + $error = array( 'unknown', 'no error recorded' ); |
| 144 | + } |
| 145 | + } |
| 146 | + throw new UploadStashFileException( "error storing file in '$path': " . implode( '; ', $error ) ); |
| 147 | + } |
| 148 | + $stashPath = $status->value; |
| 149 | + |
| 150 | + // required info we always store. Must trump any other application info in $data |
| 151 | + // 'mTempPath', 'mFileSize', and 'mFileProps' are arbitrary names |
| 152 | + // chosen for compatibility with UploadBase's way of doing this. |
| 153 | + $requiredData = array( |
| 154 | + 'mTempPath' => $stashPath, |
| 155 | + 'mFileSize' => $fileProps['size'], |
| 156 | + 'mFileProps' => $fileProps, |
| 157 | + 'version' => UploadBase::SESSION_VERSION |
| 158 | + ); |
| 159 | + |
| 160 | + // now, merge required info and extra data into the session. (The extra data changes from application to application. |
| 161 | + // UploadWizard wants different things than say FirefoggChunkedUpload.) |
| 162 | + $_SESSION[UploadBase::SESSION_KEYNAME][$key] = array_merge( $data, $requiredData ); |
| 163 | + |
| 164 | + return $this->getFile( $key ); |
| 165 | + } |
| 166 | + |
| 167 | +} |
| 168 | + |
| 169 | +class UploadStashFile extends UnregisteredLocalFile { |
| 170 | + private $sessionStash; |
| 171 | + private $sessionKey; |
| 172 | + private $sessionData; |
| 173 | + private $urlName; |
| 174 | + |
| 175 | + /** |
| 176 | + * A LocalFile wrapper around a file that has been temporarily stashed, so we can do things like create thumbnails for it |
| 177 | + * Arguably UnregisteredLocalFile should be handling its own file repo but that class is a bit retarded currently |
| 178 | + * @param {UploadStash} $stash: UploadStash, useful for obtaining config, stashing transformed files |
| 179 | + * @param {FileRepo} $repo: repository where we should find the path |
| 180 | + * @param {String} $path: path to file |
| 181 | + * @param {String} $key: key to store the path and any stashed data under |
| 182 | + * @param {String} $data: any other data we want stored with this file |
| 183 | + * @throws UploadStashBadPathException |
| 184 | + * @throws UploadStashFileNotFoundException |
| 185 | + */ |
| 186 | + public function __construct( $stash, $repo, $path, $key, $data ) { |
| 187 | + $this->sessionStash = $stash; |
| 188 | + $this->sessionKey = $key; |
| 189 | + $this->sessionData = $data; |
| 190 | + |
| 191 | + // resolve mwrepo:// urls |
| 192 | + if ( $repo->isVirtualUrl( $path ) ) { |
| 193 | + $path = $repo->resolveVirtualUrl( $path ); |
| 194 | + } |
| 195 | + |
| 196 | + // check if path appears to be sane, no parent traversals, and is in this repo's temp zone. |
| 197 | + $repoTempPath = $repo->getZonePath( 'temp' ); |
| 198 | + if ( ( ! $repo->validateFilename( $path ) ) || |
| 199 | + ( strpos( $path, $repoTempPath ) !== 0 ) ) { |
| 200 | + throw new UploadStashBadPathException( "path '$path' is not valid or is not in repo temp area: '$repoTempPath'" ); |
| 201 | + } |
| 202 | + |
| 203 | + // check if path exists! and is a plain file. |
| 204 | + if ( ! $repo->fileExists( $path, FileRepo::FILES_ONLY ) ) { |
| 205 | + throw new UploadStashFileNotFoundException( "cannot find path '$path'" ); |
| 206 | + } |
| 207 | + |
| 208 | + parent::__construct( false, $repo, $path, false ); |
| 209 | + |
| 210 | + // we will be initializing from some tmpnam files that don't have extensions. |
| 211 | + // most of MediaWiki assumes all uploaded files have good extensions. So, we fix this. |
| 212 | + $this->name = basename( $this->path ); |
| 213 | + $this->setExtension(); |
| 214 | + |
| 215 | + } |
| 216 | + |
| 217 | + /** |
| 218 | + * A method needed by the file transforming and scaling routines in File.php |
| 219 | + * We do not necessarily care about doing the description at this point |
| 220 | + * However, we also can't return the empty string, as the rest of MediaWiki demands this (and calls to imagemagick |
| 221 | + * convert require it to be there) |
| 222 | + * @return {String} dummy value |
| 223 | + */ |
| 224 | + public function getDescriptionUrl() { |
| 225 | + return $this->getUrl(); |
| 226 | + } |
| 227 | + |
| 228 | + /** |
| 229 | + * Find or guess extension -- ensuring that our extension matches our mime type. |
| 230 | + * Since these files are constructed from php tempnames they may not start off |
| 231 | + * with an extension. |
| 232 | + * This does not override getExtension() because things like getMimeType() already call getExtension(), |
| 233 | + * and that results in infinite recursion. So, we preemptively *set* the extension so getExtension() can find it. |
| 234 | + * For obvious reasons this should be called as early as possible, as part of initialization |
| 235 | + */ |
| 236 | + public function setExtension() { |
| 237 | + // Does this have an extension? |
| 238 | + $n = strrpos( $this->path, '.' ); |
| 239 | + $extension = null; |
| 240 | + if ( $n !== false ) { |
| 241 | + $extension = $n ? substr( $this->path, $n + 1 ) : ''; |
| 242 | + } else { |
| 243 | + // If not, assume that it should be related to the mime type of the original file. |
| 244 | + // |
| 245 | + // This entire thing is backwards -- we *should* just create an extension based on |
| 246 | + // the mime type of the transformed file, *after* transformation. But File.php demands |
| 247 | + // to know the name of the transformed file before creating it. |
| 248 | + $mimeType = $this->getMimeType(); |
| 249 | + $extensions = explode( ' ', MimeMagic::singleton()->getExtensionsForType( $mimeType ) ); |
| 250 | + if ( count( $extensions ) ) { |
| 251 | + $extension = $extensions[0]; |
| 252 | + } |
| 253 | + } |
| 254 | + |
| 255 | + if ( is_null( $extension ) ) { |
| 256 | + throw new UploadStashFileException( "extension '$extension' is null" ); |
| 257 | + } |
| 258 | + |
| 259 | + $this->extension = parent::normalizeExtension( $extension ); |
| 260 | + } |
| 261 | + |
| 262 | + /** |
| 263 | + * Get the path for the thumbnail (actually any transformation of this file) |
| 264 | + * The actual argument is the result of thumbName although we seem to have |
| 265 | + * buggy code elsewhere that expects a boolean 'suffix' |
| 266 | + * |
| 267 | + * @param {String|false} $thumbName: name of thumbnail (e.g. "120px-123456.jpg" ), or false to just get the path |
| 268 | + * @return {String} path thumbnail should take on filesystem, or containing directory if thumbname is false |
| 269 | + */ |
| 270 | + public function getThumbPath( $thumbName = false ) { |
| 271 | + $path = dirname( $this->path ); |
| 272 | + if ( $thumbName !== false ) { |
| 273 | + $path .= "/$thumbName"; |
| 274 | + } |
| 275 | + return $path; |
| 276 | + } |
| 277 | + |
| 278 | + /** |
| 279 | + * Return the file/url base name of a thumbnail with the specified parameters |
| 280 | + * |
| 281 | + * @param {Array} $params: handler-specific parameters |
| 282 | + * @return {String|null} base name for URL, like '120px-12345.jpg', or null if there is no handler |
| 283 | + */ |
| 284 | + function thumbName( $params ) { |
| 285 | + if ( !$this->getHandler() ) { |
| 286 | + return null; |
| 287 | + } |
| 288 | + $extension = $this->getExtension(); |
| 289 | + list( $thumbExt, $thumbMime ) = $this->handler->getThumbType( $extension, $this->getMimeType(), $params ); |
| 290 | + $thumbName = $this->getHandler()->makeParamString( $params ) . '-' . $this->getUrlName(); |
| 291 | + if ( $thumbExt != $extension ) { |
| 292 | + $thumbName .= ".$thumbExt"; |
| 293 | + } |
| 294 | + return $thumbName; |
| 295 | + } |
| 296 | + |
| 297 | + /** |
| 298 | + * Get a URL to access the thumbnail |
| 299 | + * This is required because the model of how files work requires that |
| 300 | + * the thumbnail urls be predictable. However, in our model the URL is not based on the filename |
| 301 | + * (that's hidden in the session) |
| 302 | + * |
| 303 | + * @param {String} $thumbName: basename of thumbnail file -- however, we don't want to use the file exactly |
| 304 | + * @return {String} URL to access thumbnail, or URL with partial path |
| 305 | + */ |
| 306 | + public function getThumbUrl( $thumbName = false ) { |
| 307 | + $path = $this->sessionStash->getBaseUrl(); |
| 308 | + if ( $thumbName !== false ) { |
| 309 | + $path .= '/' . rawurlencode( $thumbName ); |
| 310 | + } |
| 311 | + return $path; |
| 312 | + } |
| 313 | + |
| 314 | + /** |
| 315 | + * The basename for the URL, which we want to not be related to the filename. |
| 316 | + * Will also be used as the lookup key for a thumbnail file. |
| 317 | + * @return {String} base url name, like '120px-123456.jpg' |
| 318 | + */ |
| 319 | + public function getUrlName() { |
| 320 | + if ( ! $this->urlName ) { |
| 321 | + $this->urlName = $this->sessionKey . '.' . $this->getExtension(); |
| 322 | + } |
| 323 | + return $this->urlName; |
| 324 | + } |
| 325 | + |
| 326 | + /** |
| 327 | + * Return the URL of the file, if for some reason we wanted to download it |
| 328 | + * We tend not to do this for the original file, but we do want thumb icons |
| 329 | + * @return {String} url |
| 330 | + */ |
| 331 | + public function getUrl() { |
| 332 | + if ( !isset( $this->url ) ) { |
| 333 | + $this->url = $this->sessionStash->getBaseUrl() . '/' . $this->getUrlName(); |
| 334 | + } |
| 335 | + return $this->url; |
| 336 | + } |
| 337 | + |
| 338 | + /** |
| 339 | + * Parent classes use this method, for no obvious reason, to return the path (relative to wiki root, I assume). |
| 340 | + * But with this class, the URL is unrelated to the path. |
| 341 | + * |
| 342 | + * @return {String} url |
| 343 | + */ |
| 344 | + public function getFullUrl() { |
| 345 | + return $this->getUrl(); |
| 346 | + } |
| 347 | + |
| 348 | + |
| 349 | + /** |
| 350 | + * Getter for session key (the session-unique id by which this file's location & metadata is stored in the session) |
| 351 | + * @return {String} session key |
| 352 | + */ |
| 353 | + public function getSessionKey() { |
| 354 | + return $this->sessionKey; |
| 355 | + } |
| 356 | + |
| 357 | + /** |
| 358 | + * Typically, transform() returns a ThumbnailImage, which you can think of as being the exact |
| 359 | + * equivalent of an HTML thumbnail on Wikipedia. So its URL is the full-size file, not the thumbnail's URL. |
| 360 | + * |
| 361 | + * Here we override transform() to stash the thumbnail file, and then |
| 362 | + * provide a way to get at the stashed thumbnail file to extract properties such as its URL |
| 363 | + * |
| 364 | + * @param {Array} $params: parameters suitable for File::transform() |
| 365 | + * @param {Bitmask} $flags: flags suitable for File::transform() |
| 366 | + * @return {ThumbnailImage} with additional File thumbnailFile property |
| 367 | + */ |
| 368 | + public function transform( $params, $flags = 0 ) { |
| 369 | + |
| 370 | + // force it to get a thumbnail right away |
| 371 | + $flags |= self::RENDER_NOW; |
| 372 | + |
| 373 | + // returns a ThumbnailImage object containing the url and path. Note. NOT A FILE OBJECT. |
| 374 | + $thumb = parent::transform( $params, $flags ); |
| 375 | + $key = $this->thumbName($params); |
| 376 | + |
| 377 | + // remove extension, so it's stored in the session under '120px-123456' |
| 378 | + // this makes it uniform with the other session key for the original, '123456' |
| 379 | + $n = strrpos( $key, '.' ); |
| 380 | + if ( $n !== false ) { |
| 381 | + $key = substr( $key, 0, $n ); |
| 382 | + } |
| 383 | + |
| 384 | + // stash the thumbnail File, and provide our caller with a way to get at its properties |
| 385 | + $stashedThumbFile = $this->sessionStash->stashFile( $thumb->path, array(), $key ); |
| 386 | + $thumb->thumbnailFile = $stashedThumbFile; |
| 387 | + |
| 388 | + return $thumb; |
| 389 | + |
| 390 | + } |
| 391 | + |
| 392 | + /** |
| 393 | + * Remove the associated temporary file |
| 394 | + * @return {Status} success |
| 395 | + */ |
| 396 | + public function remove() { |
| 397 | + return $this->repo->freeTemp( $this->path ); |
| 398 | + } |
| 399 | + |
| 400 | +} |
| 401 | + |
| 402 | +class UploadStashNotAvailableException extends MWException {}; |
| 403 | +class UploadStashFileNotFoundException extends MWException {}; |
| 404 | +class UploadStashBadPathException extends MWException {}; |
| 405 | +class UploadStashBadVersionException extends MWException {}; |
| 406 | +class UploadStashFileException extends MWException {}; |
| 407 | + |
Property changes on: branches/uploadwizard/phase3/includes/upload/UploadStash.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 408 | + native |
Index: branches/uploadwizard/phase3/includes/AutoLoader.php |
— | — | @@ -633,7 +633,7 @@ |
634 | 634 | 'SpecialRecentChanges' => 'includes/specials/SpecialRecentchanges.php', |
635 | 635 | 'SpecialRecentchangeslinked' => 'includes/specials/SpecialRecentchangeslinked.php', |
636 | 636 | 'SpecialSearch' => 'includes/specials/SpecialSearch.php', |
637 | | - 'SpecialPrivateUploadStash' => 'includes/specials/SpecialPrivateUploadStash.php', |
| 637 | + 'SpecialUploadStash' => 'includes/specials/SpecialUploadStash.php', |
638 | 638 | 'SpecialSpecialpages' => 'includes/specials/SpecialSpecialpages.php', |
639 | 639 | 'SpecialStatistics' => 'includes/specials/SpecialStatistics.php', |
640 | 640 | 'SpecialTags' => 'includes/specials/SpecialTags.php', |
— | — | @@ -669,7 +669,7 @@ |
670 | 670 | 'UserloginTemplate' => 'includes/templates/Userlogin.php', |
671 | 671 | |
672 | 672 | # includes/upload |
673 | | - 'PrivateUploadStash' => 'includes/upload/PrivateUploadStash.php', |
| 673 | + 'UploadStash' => 'includes/upload/UploadStash.php', |
674 | 674 | 'UploadBase' => 'includes/upload/UploadBase.php', |
675 | 675 | 'UploadFromStash' => 'includes/upload/UploadFromStash.php', |
676 | 676 | 'UploadFromFile' => 'includes/upload/UploadFromFile.php', |
Index: branches/uploadwizard/phase3/includes/specials/SpecialPrivateUploadStash.php |
— | — | @@ -1,140 +0,0 @@ |
2 | | -<?php |
3 | | -/** |
4 | | - * Special:PrivateUploadStash |
5 | | - * |
6 | | - * Web access for files temporarily stored by PrivateUploadStash. |
7 | | - * |
8 | | - * For example -- files that were uploaded with the UploadWizard extension are stored temporarily |
9 | | - * before committing them to the db. But we want to see their thumbnails and get other information |
10 | | - * about them. |
11 | | - * |
12 | | - * Since this is based on the user's session, in effect this creates a private temporary file area. |
13 | | - * However, the URLs for the files cannot be shared. |
14 | | - * |
15 | | - * @file |
16 | | - * @ingroup SpecialPage |
17 | | - * @ingroup Upload |
18 | | - */ |
19 | | - |
20 | | -class SpecialPrivateUploadStash extends SpecialPage { |
21 | | - |
22 | | - static $HttpErrors = array( // FIXME: Use OutputPage::getStatusMessage() --RK |
23 | | - 400 => 'Bad Request', |
24 | | - 403 => 'Access Denied', |
25 | | - 404 => 'File not found', |
26 | | - 500 => 'Internal Server Error', |
27 | | - ); |
28 | | - |
29 | | - // PrivateUploadStash |
30 | | - private $stash; |
31 | | - |
32 | | - // we should not be reading in really big files and serving them out |
33 | | - private $maxServeFileSize = 262144; // 256K |
34 | | - |
35 | | - // $request is the request (usually wgRequest) |
36 | | - // $subpage is everything in the URL after Special:PrivateUploadStash |
37 | | - // FIXME: These parameters don't match SpecialPage::__construct()'s params at all, and are unused --RK |
38 | | - public function __construct( $request = null, $subpage = null ) { |
39 | | - parent::__construct( 'PrivateUploadStash', 'upload' ); |
40 | | - $this->stash = new PrivateUploadStash(); |
41 | | - } |
42 | | - |
43 | | - /** |
44 | | - * If file available in stash, cats it out to the client as a simple HTTP response. |
45 | | - * n.b. Most sanity checking done in PrivateUploadStashLocalFile, so this is straightforward. |
46 | | - * |
47 | | - * @param {String} $subPage: subpage, e.g. in http://example.com/wiki/Special:PrivateUploadStash/foo.jpg, the "foo.jpg" part |
48 | | - * @return {Boolean} success |
49 | | - */ |
50 | | - public function execute( $subPage ) { |
51 | | - global $wgOut, $wgUser; |
52 | | - |
53 | | - if ( !$this->userCanExecute( $wgUser ) ) { |
54 | | - $this->displayRestrictionError(); |
55 | | - return; |
56 | | - } |
57 | | - |
58 | | - // prevent callers from doing standard HTML output -- we'll take it from here |
59 | | - $wgOut->disable(); |
60 | | - |
61 | | - try { |
62 | | - $file = $this->getStashFile( $subPage ); |
63 | | - if ( $file->getSize() > $this->maxServeFileSize ) { |
64 | | - throw new MWException( 'file size too large' ); |
65 | | - } |
66 | | - $this->outputFile( $file ); |
67 | | - return true; |
68 | | - |
69 | | - } catch( PrivateUploadStashFileNotFoundException $e ) { |
70 | | - $code = 404; |
71 | | - } catch( PrivateUploadStashBadPathException $e ) { |
72 | | - $code = 403; |
73 | | - } catch( Exception $e ) { |
74 | | - $code = 500; |
75 | | - } |
76 | | - |
77 | | - wfHttpError( $code, self::$HttpErrors[$code], $e->getCode(), $e->getMessage() ); |
78 | | - return false; |
79 | | - } |
80 | | - |
81 | | - |
82 | | - /** |
83 | | - * Convert the incoming url portion (subpage of Special page) into a stashed file, if available. |
84 | | - * @param {String} $subPage |
85 | | - * @return {File} file object |
86 | | - * @throws MWException, PrivateUploadStashFileNotFoundException, PrivateUploadStashBadPathException |
87 | | - */ |
88 | | - private function getStashFile( $subPage ) { |
89 | | - // due to an implementation quirk (and trying to be compatible with older method) |
90 | | - // the stash key doesn't have an extension |
91 | | - $key = $subPage; |
92 | | - $n = strrpos( $subPage, '.' ); |
93 | | - if ( $n !== false ) { |
94 | | - $key = $n ? substr( $subPage, 0, $n ) : $subPage; |
95 | | - } |
96 | | - |
97 | | - try { |
98 | | - $file = $this->stash->getFile( $key ); |
99 | | - } catch ( PrivateUploadStashFileNotFoundException $e ) { |
100 | | - // if we couldn't find it, and it looks like a thumbnail, |
101 | | - // and it looks like we have the original, go ahead and generate it |
102 | | - $matches = array(); |
103 | | - if ( ! preg_match( '/^(\d+)px-(.*)$/', $key, $matches ) ) { |
104 | | - // that doesn't look like a thumbnail. re-raise exception |
105 | | - throw $e; |
106 | | - } |
107 | | - |
108 | | - list( $dummy, $width, $origKey ) = $matches; |
109 | | - |
110 | | - // do not trap exceptions, if key is in bad format, or file not found, |
111 | | - // let exceptions propagate to caller. |
112 | | - $origFile = $this->stash->getFile( $origKey ); |
113 | | - |
114 | | - // ok we're here so the original must exist. Generate the thumbnail. |
115 | | - // because the file is a PrivateUploadStashFile, this thumbnail will also be stashed, |
116 | | - // and a thumbnailFile will be created in the thumbnailImage composite object |
117 | | - $thumbnailImage = null; |
118 | | - if ( !( $thumbnailImage = $origFile->getThumbnail( $width ) ) ) { |
119 | | - throw new MWException( 'Could not obtain thumbnail' ); |
120 | | - } |
121 | | - $file = $thumbnailImage->thumbnailFile; |
122 | | - } |
123 | | - |
124 | | - return $file; |
125 | | - } |
126 | | - |
127 | | - /** |
128 | | - * Output HTTP response for file |
129 | | - * Side effects, obviously, of echoing lots of stuff to stdout. |
130 | | - * @param {File} file |
131 | | - */ |
132 | | - private function outputFile( $file ) { |
133 | | - header( 'Content-Type: ' . $file->getMimeType(), true ); |
134 | | - header( 'Content-Transfer-Encoding: binary', true ); |
135 | | - header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true ); |
136 | | - header( 'Pragma: public', true ); |
137 | | - header( 'Content-Length: ' . $file->getSize(), true ); // FIXME: PHP can handle Content-Length for you just fine --RK |
138 | | - readfile( $file->getPath() ); |
139 | | - } |
140 | | -} |
141 | | - |
Index: branches/uploadwizard/phase3/includes/specials/SpecialUploadStash.php |
— | — | @@ -0,0 +1,140 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * Special:UploadStash |
| 5 | + * |
| 6 | + * Web access for files temporarily stored by UploadStash. |
| 7 | + * |
| 8 | + * For example -- files that were uploaded with the UploadWizard extension are stored temporarily |
| 9 | + * before committing them to the db. But we want to see their thumbnails and get other information |
| 10 | + * about them. |
| 11 | + * |
| 12 | + * Since this is based on the user's session, in effect this creates a private temporary file area. |
| 13 | + * However, the URLs for the files cannot be shared. |
| 14 | + * |
| 15 | + * @file |
| 16 | + * @ingroup SpecialPage |
| 17 | + * @ingroup Upload |
| 18 | + */ |
| 19 | + |
| 20 | +class SpecialUploadStash extends SpecialPage { |
| 21 | + |
| 22 | + static $HttpErrors = array( // FIXME: Use OutputPage::getStatusMessage() --RK |
| 23 | + 400 => 'Bad Request', |
| 24 | + 403 => 'Access Denied', |
| 25 | + 404 => 'File not found', |
| 26 | + 500 => 'Internal Server Error', |
| 27 | + ); |
| 28 | + |
| 29 | + // UploadStash |
| 30 | + private $stash; |
| 31 | + |
| 32 | + // we should not be reading in really big files and serving them out |
| 33 | + private $maxServeFileSize = 262144; // 256K |
| 34 | + |
| 35 | + // $request is the request (usually wgRequest) |
| 36 | + // $subpage is everything in the URL after Special:UploadStash |
| 37 | + // FIXME: These parameters don't match SpecialPage::__construct()'s params at all, and are unused --RK |
| 38 | + public function __construct( $request = null, $subpage = null ) { |
| 39 | + parent::__construct( 'UploadStash', 'upload' ); |
| 40 | + $this->stash = new UploadStash(); |
| 41 | + } |
| 42 | + |
| 43 | + /** |
| 44 | + * If file available in stash, cats it out to the client as a simple HTTP response. |
| 45 | + * n.b. Most sanity checking done in UploadStashLocalFile, so this is straightforward. |
| 46 | + * |
| 47 | + * @param {String} $subPage: subpage, e.g. in http://example.com/wiki/Special:UploadStash/foo.jpg, the "foo.jpg" part |
| 48 | + * @return {Boolean} success |
| 49 | + */ |
| 50 | + public function execute( $subPage ) { |
| 51 | + global $wgOut, $wgUser; |
| 52 | + |
| 53 | + if ( !$this->userCanExecute( $wgUser ) ) { |
| 54 | + $this->displayRestrictionError(); |
| 55 | + return; |
| 56 | + } |
| 57 | + |
| 58 | + // prevent callers from doing standard HTML output -- we'll take it from here |
| 59 | + $wgOut->disable(); |
| 60 | + |
| 61 | + try { |
| 62 | + $file = $this->getStashFile( $subPage ); |
| 63 | + if ( $file->getSize() > $this->maxServeFileSize ) { |
| 64 | + throw new MWException( 'file size too large' ); |
| 65 | + } |
| 66 | + $this->outputFile( $file ); |
| 67 | + return true; |
| 68 | + |
| 69 | + } catch( UploadStashFileNotFoundException $e ) { |
| 70 | + $code = 404; |
| 71 | + } catch( UploadStashBadPathException $e ) { |
| 72 | + $code = 403; |
| 73 | + } catch( Exception $e ) { |
| 74 | + $code = 500; |
| 75 | + } |
| 76 | + |
| 77 | + wfHttpError( $code, self::$HttpErrors[$code], $e->getCode(), $e->getMessage() ); |
| 78 | + return false; |
| 79 | + } |
| 80 | + |
| 81 | + |
| 82 | + /** |
| 83 | + * Convert the incoming url portion (subpage of Special page) into a stashed file, if available. |
| 84 | + * @param {String} $subPage |
| 85 | + * @return {File} file object |
| 86 | + * @throws MWException, UploadStashFileNotFoundException, UploadStashBadPathException |
| 87 | + */ |
| 88 | + private function getStashFile( $subPage ) { |
| 89 | + // due to an implementation quirk (and trying to be compatible with older method) |
| 90 | + // the stash key doesn't have an extension |
| 91 | + $key = $subPage; |
| 92 | + $n = strrpos( $subPage, '.' ); |
| 93 | + if ( $n !== false ) { |
| 94 | + $key = $n ? substr( $subPage, 0, $n ) : $subPage; |
| 95 | + } |
| 96 | + |
| 97 | + try { |
| 98 | + $file = $this->stash->getFile( $key ); |
| 99 | + } catch ( UploadStashFileNotFoundException $e ) { |
| 100 | + // if we couldn't find it, and it looks like a thumbnail, |
| 101 | + // and it looks like we have the original, go ahead and generate it |
| 102 | + $matches = array(); |
| 103 | + if ( ! preg_match( '/^(\d+)px-(.*)$/', $key, $matches ) ) { |
| 104 | + // that doesn't look like a thumbnail. re-raise exception |
| 105 | + throw $e; |
| 106 | + } |
| 107 | + |
| 108 | + list( $dummy, $width, $origKey ) = $matches; |
| 109 | + |
| 110 | + // do not trap exceptions, if key is in bad format, or file not found, |
| 111 | + // let exceptions propagate to caller. |
| 112 | + $origFile = $this->stash->getFile( $origKey ); |
| 113 | + |
| 114 | + // ok we're here so the original must exist. Generate the thumbnail. |
| 115 | + // because the file is a UploadStashFile, this thumbnail will also be stashed, |
| 116 | + // and a thumbnailFile will be created in the thumbnailImage composite object |
| 117 | + $thumbnailImage = null; |
| 118 | + if ( !( $thumbnailImage = $origFile->getThumbnail( $width ) ) ) { |
| 119 | + throw new MWException( 'Could not obtain thumbnail' ); |
| 120 | + } |
| 121 | + $file = $thumbnailImage->thumbnailFile; |
| 122 | + } |
| 123 | + |
| 124 | + return $file; |
| 125 | + } |
| 126 | + |
| 127 | + /** |
| 128 | + * Output HTTP response for file |
| 129 | + * Side effects, obviously, of echoing lots of stuff to stdout. |
| 130 | + * @param {File} file |
| 131 | + */ |
| 132 | + private function outputFile( $file ) { |
| 133 | + header( 'Content-Type: ' . $file->getMimeType(), true ); |
| 134 | + header( 'Content-Transfer-Encoding: binary', true ); |
| 135 | + header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true ); |
| 136 | + header( 'Pragma: public', true ); |
| 137 | + header( 'Content-Length: ' . $file->getSize(), true ); // FIXME: PHP can handle Content-Length for you just fine --RK |
| 138 | + readfile( $file->getPath() ); |
| 139 | + } |
| 140 | +} |
| 141 | + |
Property changes on: branches/uploadwizard/phase3/includes/specials/SpecialUploadStash.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 142 | + native |
Index: branches/uploadwizard/phase3/includes/SpecialPage.php |
— | — | @@ -149,7 +149,7 @@ |
150 | 150 | 'MIMEsearch' => array( 'SpecialPage', 'MIMEsearch' ), |
151 | 151 | 'FileDuplicateSearch' => array( 'SpecialPage', 'FileDuplicateSearch' ), |
152 | 152 | 'Upload' => 'SpecialUpload', |
153 | | - 'PrivateUploadStash' => 'SpecialPrivateUploadStash', |
| 153 | + 'UploadStash' => 'SpecialUploadStash', |
154 | 154 | |
155 | 155 | # Wiki data and tools |
156 | 156 | 'Statistics' => 'SpecialStatistics', |
Index: branches/uploadwizard/phase3/languages/messages/MessagesEn.php |
— | — | @@ -460,7 +460,7 @@ |
461 | 461 | 'RevisionMove' => array( 'RevisionMove' ), |
462 | 462 | 'ComparePages' => array( 'ComparePages' ), |
463 | 463 | 'Badtitle' => array( 'Badtitle' ), |
464 | | - 'PrivateUploadStash' => array( 'PrivateUploadStash' ), |
| 464 | + 'UploadStash' => array( 'UploadStash' ), |
465 | 465 | ); |
466 | 466 | |
467 | 467 | /** |
Index: branches/uploadwizard/extensions/UploadWizard/ApiQueryStashImageInfo.php |
— | — | @@ -42,7 +42,7 @@ |
43 | 43 | $result = $this->getResult(); |
44 | 44 | |
45 | 45 | try { |
46 | | - $stash = new PrivateUploadStash(); |
| 46 | + $stash = new UploadStash(); |
47 | 47 | |
48 | 48 | foreach ( $params['sessionkey'] as $sessionkey ) { |
49 | 49 | $file = $stash->getFile( $sessionkey ); |
— | — | @@ -51,11 +51,11 @@ |
52 | 52 | $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), $modulePrefix ); |
53 | 53 | } |
54 | 54 | |
55 | | - } catch ( PrivateUploadStashNotAvailableException $e ) { |
| 55 | + } catch ( UploadStashNotAvailableException $e ) { |
56 | 56 | $this->dieUsage( "Session not available: " . $e->getMessage(), "nosession" ); |
57 | | - } catch ( PrivateUploadStashFileNotFoundException $e ) { |
| 57 | + } catch ( UploadStashFileNotFoundException $e ) { |
58 | 58 | $this->dieUsage( "File not found: " . $e->getMessage(), "invalidsessiondata" ); |
59 | | - } catch ( PrivateUploadStashBadPathException $e ) { |
| 59 | + } catch ( UploadStashBadPathException $e ) { |
60 | 60 | $this->dieUsage( "Bad path: " . $e->getMessage(), "invalidsessiondata" ); |
61 | 61 | } |
62 | 62 | |