Index: trunk/phase3/includes/upload/UploadStash.php |
— | — | @@ -30,17 +30,12 @@ |
31 | 31 | /** |
32 | 32 | * Represents the session which contains temporarily stored files. |
33 | 33 | * Designed to be compatible with the session stashing code in UploadBase (should replace it eventually) |
34 | | - * |
35 | | - * @param $repo FileRepo: optional -- repo in which to store files. Will choose LocalRepo if not supplied. |
36 | 34 | */ |
37 | | - public function __construct( $repo = null ) { |
| 35 | + public function __construct() { |
38 | 36 | |
39 | | - if ( is_null( $repo ) ) { |
40 | | - $repo = RepoGroup::singleton()->getLocalRepo(); |
41 | | - } |
| 37 | + // this might change based on wiki's configuration. |
| 38 | + $this->repo = RepoGroup::singleton()->getLocalRepo(); |
42 | 39 | |
43 | | - $this->repo = $repo; |
44 | | - |
45 | 40 | if ( ! isset( $_SESSION ) ) { |
46 | 41 | throw new UploadStashNotAvailableException( 'no session variable' ); |
47 | 42 | } |
— | — | @@ -51,6 +46,7 @@ |
52 | 47 | |
53 | 48 | } |
54 | 49 | |
| 50 | + |
55 | 51 | /** |
56 | 52 | * Get a file and its metadata from the stash. |
57 | 53 | * May throw exception if session data cannot be parsed due to schema change, or key not found. |
— | — | @@ -81,7 +77,7 @@ |
82 | 78 | unset( $data['mTempPath'] ); |
83 | 79 | |
84 | 80 | $file = new UploadStashFile( $this, $this->repo, $path, $key, $data ); |
85 | | - if ( $file->getSize === 0 ) { |
| 81 | + if ( $file->getSize() === 0 ) { |
86 | 82 | throw new UploadStashZeroLengthFileException( "File is zero length" ); |
87 | 83 | } |
88 | 84 | $this->files[$key] = $file; |
— | — | @@ -170,6 +166,36 @@ |
171 | 167 | } |
172 | 168 | |
173 | 169 | /** |
| 170 | + * Remove all files from the stash. |
| 171 | + * Does not clean up files in the repo, just the record of them. |
| 172 | + * @return boolean: success |
| 173 | + */ |
| 174 | + public function clear() { |
| 175 | + $_SESSION[UploadBase::SESSION_KEYNAME] = array(); |
| 176 | + return true; |
| 177 | + } |
| 178 | + |
| 179 | + |
| 180 | + /** |
| 181 | + * Remove a particular file from the stash. |
| 182 | + * Does not clean up file in the repo, just the record of it. |
| 183 | + * @return boolean: success |
| 184 | + */ |
| 185 | + public function removeFile( $key ) { |
| 186 | + unset ( $_SESSION[UploadBase::SESSION_KEYNAME][$key] ); |
| 187 | + return true; |
| 188 | + } |
| 189 | + |
| 190 | + |
| 191 | + /** |
| 192 | + * List all files in the stash. |
| 193 | + */ |
| 194 | + public function listFiles() { |
| 195 | + return array_keys( $_SESSION[UploadBase::SESSION_KEYNAME] ); |
| 196 | + } |
| 197 | + |
| 198 | + |
| 199 | + /** |
174 | 200 | * Find or guess extension -- ensuring that our extension matches our mime type. |
175 | 201 | * Since these files are constructed from php tempnames they may not start off |
176 | 202 | * with an extension. |
Index: trunk/phase3/includes/specials/SpecialUploadStash.php |
— | — | @@ -20,6 +20,12 @@ |
21 | 21 | // UploadStash |
22 | 22 | private $stash; |
23 | 23 | |
| 24 | + // is the edit request authorized? boolean |
| 25 | + private $isEditAuthorized; |
| 26 | + |
| 27 | + // did the user request us to clear the stash? boolean |
| 28 | + private $requestedClear; |
| 29 | + |
24 | 30 | // Since we are directly writing the file to STDOUT, |
25 | 31 | // we should not be reading in really big files and serving them out. |
26 | 32 | // |
— | — | @@ -30,18 +36,21 @@ |
31 | 37 | // uploading. |
32 | 38 | const MAX_SERVE_BYTES = 262144; // 256K |
33 | 39 | |
34 | | - public function __construct( ) { |
| 40 | + public function __construct( $request = null ) { |
| 41 | + global $wgRequest; |
| 42 | + |
35 | 43 | parent::__construct( 'UploadStash', 'upload' ); |
36 | 44 | try { |
37 | 45 | $this->stash = new UploadStash( ); |
38 | | - } catch (UploadStashNotAvailableException $e) { |
| 46 | + } catch ( UploadStashNotAvailableException $e ) { |
39 | 47 | return null; |
40 | 48 | } |
| 49 | + |
| 50 | + $this->loadRequest( is_null( $request ) ? $wgRequest : $request ); |
41 | 51 | } |
42 | 52 | |
43 | 53 | /** |
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. |
| 54 | + * Execute page -- can output a file directly or show a listing of them. |
46 | 55 | * |
47 | 56 | * @param $subPage String: subpage, e.g. in http://example.com/wiki/Special:UploadStash/foo.jpg, the "foo.jpg" part |
48 | 57 | * @return Boolean: success |
— | — | @@ -54,56 +63,62 @@ |
55 | 64 | return; |
56 | 65 | } |
57 | 66 | |
| 67 | + if ( !isset( $subPage ) || $subPage === '' ) { |
| 68 | + return $this->showUploads(); |
| 69 | + } |
| 70 | + |
| 71 | + return $this->showUpload( $subPage ); |
| 72 | + } |
| 73 | + |
| 74 | + |
| 75 | + /** |
| 76 | + * If file available in stash, cats it out to the client as a simple HTTP response. |
| 77 | + * n.b. Most sanity checking done in UploadStashLocalFile, so this is straightforward. |
| 78 | + * |
| 79 | + * @param $key String: the key of a particular requested file |
| 80 | + */ |
| 81 | + public function showUpload( $key ) { |
| 82 | + global $wgOut; |
| 83 | + |
58 | 84 | // prevent callers from doing standard HTML output -- we'll take it from here |
59 | 85 | $wgOut->disable(); |
60 | 86 | |
61 | | - if ( !isset( $subPage ) || $subPage === '' ) { |
62 | | - // the user probably visited the page just to see what would happen, so explain it a bit. |
63 | | - $code = '400'; |
64 | | - $message = "Missing key\n\n" |
65 | | - . 'This page provides access to temporarily stashed files for the user that ' |
66 | | - . 'uploaded those files. See the upload API documentation. To access a stashed file, ' |
67 | | - . 'use the URL of this page, with a slash and the key of the stashed file appended.'; |
68 | | - } else { |
69 | | - try { |
70 | | - if ( preg_match( '/^(\d+)px-(.*)$/', $subPage, $matches ) ) { |
71 | | - list( /* full match */, $width, $key ) = $matches; |
72 | | - return $this->outputThumbFromStash( $key, $width ); |
73 | | - } else { |
74 | | - return $this->outputFileFromStash( $subPage ); |
75 | | - } |
76 | | - } catch( UploadStashFileNotFoundException $e ) { |
77 | | - $code = 404; |
78 | | - $message = $e->getMessage(); |
79 | | - } catch( UploadStashZeroLengthFileException $e ) { |
80 | | - $code = 500; |
81 | | - $message = $e->getMessage(); |
82 | | - } catch( UploadStashBadPathException $e ) { |
83 | | - $code = 500; |
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 | | - } catch( Exception $e ) { |
89 | | - $code = 500; |
90 | | - $message = $e->getMessage(); |
| 87 | + try { |
| 88 | + if ( preg_match( '/^(\d+)px-(.*)$/', $key, $matches ) ) { |
| 89 | + list( /* full match */, $width, $key ) = $matches; |
| 90 | + return $this->outputThumbFromStash( $key, $width ); |
| 91 | + } else { |
| 92 | + return $this->outputFileFromStash( $key ); |
91 | 93 | } |
| 94 | + } catch( UploadStashFileNotFoundException $e ) { |
| 95 | + $code = 404; |
| 96 | + $message = $e->getMessage(); |
| 97 | + } catch( UploadStashZeroLengthFileException $e ) { |
| 98 | + $code = 500; |
| 99 | + $message = $e->getMessage(); |
| 100 | + } catch( UploadStashBadPathException $e ) { |
| 101 | + $code = 500; |
| 102 | + $message = $e->getMessage(); |
| 103 | + } catch( SpecialUploadStashTooLargeException $e ) { |
| 104 | + $code = 500; |
| 105 | + $message = 'Cannot serve a file larger than ' . self::MAX_SERVE_BYTES . ' bytes. ' . $e->getMessage(); |
| 106 | + } catch( Exception $e ) { |
| 107 | + $code = 500; |
| 108 | + $message = $e->getMessage(); |
92 | 109 | } |
93 | 110 | |
94 | 111 | wfHttpError( $code, OutputPage::getStatusMessage( $code ), $message ); |
95 | 112 | return false; |
96 | 113 | } |
97 | | - |
| 114 | + |
98 | 115 | /** |
99 | 116 | * Get a file from stash and stream it out. Rely on parent to catch exceptions and transform them into HTTP |
100 | 117 | * @param String: $key - key of this file in the stash, which probably looks like a filename with extension. |
101 | | - * @throws ....? |
102 | 118 | * @return boolean |
103 | 119 | */ |
104 | 120 | private function outputFileFromStash( $key ) { |
105 | 121 | $file = $this->stash->getFile( $key ); |
106 | | - $this->outputLocalFile( $file ); |
107 | | - return true; |
| 122 | + return $this->outputLocalFile( $file ); |
108 | 123 | } |
109 | 124 | |
110 | 125 | |
— | — | @@ -111,11 +126,13 @@ |
112 | 127 | * Get a thumbnail for file, either generated locally or remotely, and stream it out |
113 | 128 | * @param String $key: key for the file in the stash |
114 | 129 | * @param int $width: width of desired thumbnail |
115 | | - * @return ?? |
| 130 | + * @return boolean success |
116 | 131 | */ |
117 | 132 | private function outputThumbFromStash( $key, $width ) { |
118 | 133 | |
119 | 134 | // this global, if it exists, points to a "scaler", as you might find in the Wikimedia Foundation cluster. See outputRemoteScaledThumb() |
| 135 | + // this is part of our horrible NFS-based system, we create a file on a mount point here, but fetch the scaled file from somewhere else that |
| 136 | + // happens to share it over NFS |
120 | 137 | global $wgUploadStashScalerBaseUrl; |
121 | 138 | |
122 | 139 | // let exceptions propagate to caller. |
— | — | @@ -222,7 +239,7 @@ |
223 | 240 | if ( $file->getSize() > self::MAX_SERVE_BYTES ) { |
224 | 241 | throw new SpecialUploadStashTooLargeException(); |
225 | 242 | } |
226 | | - self::outputHeaders( $file->getMimeType(), $file->getSize() ); |
| 243 | + self::outputFileHeaders( $file->getMimeType(), $file->getSize() ); |
227 | 244 | readfile( $file->getPath() ); |
228 | 245 | return true; |
229 | 246 | } |
— | — | @@ -238,7 +255,7 @@ |
239 | 256 | if ( $size > self::MAX_SERVE_BYTES ) { |
240 | 257 | throw new SpecialUploadStashTooLargeException(); |
241 | 258 | } |
242 | | - self::outputHeaders( $contentType, $size ); |
| 259 | + self::outputFileHeaders( $contentType, $size ); |
243 | 260 | print $content; |
244 | 261 | return true; |
245 | 262 | } |
— | — | @@ -250,13 +267,102 @@ |
251 | 268 | * @param String $contentType : string suitable for content-type header |
252 | 269 | * @param String $size: length in bytes |
253 | 270 | */ |
254 | | - private static function outputHeaders( $contentType, $size ) { |
| 271 | + private static function outputFileHeaders( $contentType, $size ) { |
255 | 272 | header( "Content-Type: $contentType", true ); |
256 | 273 | header( 'Content-Transfer-Encoding: binary', true ); |
257 | 274 | header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true ); |
258 | 275 | header( "Content-Length: $size", true ); |
259 | 276 | } |
260 | 277 | |
| 278 | + |
| 279 | + /** |
| 280 | + * Initialize authorization & actions to take, from the request |
| 281 | + * @param $request: WebRequest |
| 282 | + */ |
| 283 | + private function loadRequest( $request ) { |
| 284 | + global $wgUser; |
| 285 | + if ( $request->wasPosted() ) { |
| 286 | + |
| 287 | + $token = $request->getVal( 'wpEditToken' ); |
| 288 | + $this->isEditAuthorized = $wgUser->matchEditToken( $token ); |
| 289 | + |
| 290 | + $this->requestedClear = $request->getBool( 'clear' ); |
| 291 | + |
| 292 | + } |
| 293 | + } |
| 294 | + |
| 295 | + /** |
| 296 | + * Static callback for the HTMLForm in showUploads, to process |
| 297 | + * Note the stash has to be recreated since this is being called in a static context. |
| 298 | + * This works, because there really is only one stash per logged-in user, despite appearances. |
| 299 | + * |
| 300 | + * @return Status |
| 301 | + */ |
| 302 | + public static function tryClearStashedUploads( $formData ) { |
| 303 | + wfDebug( __METHOD__ . " form data : " . print_r( $formData, 1 ) ); |
| 304 | + if ( isset( $formData['clear'] ) and $formData['clear'] ) { |
| 305 | + $stash = new UploadStash(); |
| 306 | + wfDebug( "stash has: " . print_r( $stash->listFiles(), 1 ) ); |
| 307 | + if ( ! $stash->clear() ) { |
| 308 | + return Status::newFatal( 'uploadstash-errclear' ); |
| 309 | + } |
| 310 | + } |
| 311 | + return Status::newGood(); |
| 312 | + } |
| 313 | + |
| 314 | + /** |
| 315 | + * Default action when we don't have a subpage -- just show links to the uploads we have, |
| 316 | + * Also show a button to clear stashed files |
| 317 | + * @param Status : $status - the result of processRequest |
| 318 | + */ |
| 319 | + private function showUploads( $status = null ) { |
| 320 | + global $wgOut; |
| 321 | + if ( $status === null ) { |
| 322 | + $status = Status::newGood(); |
| 323 | + } |
| 324 | + |
| 325 | + // sets the title, etc. |
| 326 | + $this->setHeaders(); |
| 327 | + $this->outputHeader(); |
| 328 | + |
| 329 | + |
| 330 | + // create the form, which will also be used to execute a callback to process incoming form data |
| 331 | + // this design is extremely dubious, but supposedly HTMLForm is our standard now? |
| 332 | + |
| 333 | + $form = new HTMLForm( array( 'clear' => array( 'class' => 'HTMLHiddenField', 'default' => true ) ), 'clearStashedUploads' ); |
| 334 | + $form->setSubmitCallback( array( __CLASS__, 'tryClearStashedUploads' ) ); |
| 335 | + $form->setTitle( $this->getTitle() ); |
| 336 | + $form->addHiddenField( 'clear', true, array( 'type' => 'boolean' ) ); |
| 337 | + $form->setSubmitText( wfMsg( 'uploadstash-clear' ) ); |
| 338 | + |
| 339 | + $form->prepareForm(); |
| 340 | + $formResult = $form->tryAuthorizedSubmit(); |
| 341 | + |
| 342 | + |
| 343 | + // show the files + form, if there are any, or just say there are none |
| 344 | + $refreshHtml = Html::element( 'a', array( 'href' => $this->getTitle()->getLocalURL() ), wfMsg( 'uploadstash-refresh' ) ); |
| 345 | + $files = $this->stash->listFiles(); |
| 346 | + if ( count( $files ) ) { |
| 347 | + sort( $files ); |
| 348 | + $fileListItemsHtml = ''; |
| 349 | + foreach ( $files as $file ) { |
| 350 | + $fileListItemsHtml .= Html::rawElement( 'li', array(), |
| 351 | + Html::element( 'a', array( 'href' => $this->getTitle( $file )->getLocalURL() ), $file ) |
| 352 | + ); |
| 353 | + } |
| 354 | + $wgOut->addHtml( Html::rawElement( 'ul', array(), $fileListItemsHtml ) ); |
| 355 | + $form->displayForm( $formResult ); |
| 356 | + $wgOut->addHtml( Html::rawElement( 'p', array(), $refreshHtml ) ); |
| 357 | + } else { |
| 358 | + $wgOut->addHtml( Html::rawElement( 'p', array(), |
| 359 | + Html::element( 'span', array(), wfMsg( 'uploadstash-nofiles' ) ) |
| 360 | + . ' ' |
| 361 | + . $refreshHtml |
| 362 | + ) ); |
| 363 | + } |
| 364 | + |
| 365 | + return true; |
| 366 | + } |
261 | 367 | } |
262 | 368 | |
263 | 369 | class SpecialUploadStashTooLargeException extends MWException {}; |
Index: trunk/phase3/languages/messages/MessagesEn.php |
— | — | @@ -2214,6 +2214,15 @@ |
2215 | 2215 | 'upload-unknown-size' => 'Unknown size', |
2216 | 2216 | 'upload-http-error' => 'An HTTP error occured: $1', |
2217 | 2217 | |
| 2218 | +# Special:UploadStash |
| 2219 | +'uploadstash' => 'Upload stash', |
| 2220 | +'uploadstash-summary' => 'This page provides access to files which are uploaded (or in the process of uploading) but are not yet published to the wiki. These files are not visible to anyone but the user who uploaded them. See the documentation for the upload API.', |
| 2221 | +'uploadstash-clear' => 'Clear stashed files', |
| 2222 | +'uploadstash-nofiles' => 'You have no stashed files.', |
| 2223 | +'uploadstash-badtoken' => 'We could not perform that action, perhaps because your editing credentials expired. Try again.', |
| 2224 | +'uploadstash-errclear' => 'We could not clear the files.', |
| 2225 | +'uploadstash-refresh' => 'Refresh the list of files', |
| 2226 | + |
2218 | 2227 | # img_auth script messages |
2219 | 2228 | 'img-auth-accessdenied' => 'Access denied', |
2220 | 2229 | 'img-auth-nopathinfo' => 'Missing PATH_INFO. |