3df193bb6f4cdc3a11bc461c6c3037d9ceccc722
[lhc/web/wiklou.git] / includes / specials / SpecialUploadStash.php
1 <?php
2 /**
3 * Implements Special:UploadStash
4 *
5 * Web access for files temporarily stored by UploadStash.
6 *
7 * For example -- files that were uploaded with the UploadWizard extension are stored temporarily
8 * before committing them to the db. But we want to see their thumbnails and get other information
9 * about them.
10 *
11 * Since this is based on the user's session, in effect this creates a private temporary file area.
12 * However, the URLs for the files cannot be shared.
13 *
14 * @file
15 * @ingroup SpecialPage
16 * @ingroup Upload
17 */
18
19 class SpecialUploadStash extends UnlistedSpecialPage {
20 // UploadStash
21 private $stash;
22
23 // Since we are directly writing the file to STDOUT,
24 // we should not be reading in really big files and serving them out.
25 //
26 // We also don't want people using this as a file drop, even if they
27 // share credentials.
28 //
29 // This service is really for thumbnails and other such previews while
30 // uploading.
31 const MAX_SERVE_BYTES = 262144; // 256K
32
33 public function __construct( ) {
34 parent::__construct( 'UploadStash', 'upload' );
35 try {
36 $this->stash = new UploadStash( );
37 } catch (UploadStashNotAvailableException $e) {
38 return null;
39 }
40 }
41
42 /**
43 * If file available in stash, cats it out to the client as a simple HTTP response.
44 * n.b. Most sanity checking done in UploadStashLocalFile, so this is straightforward.
45 *
46 * @param $subPage String: subpage, e.g. in http://example.com/wiki/Special:UploadStash/foo.jpg, the "foo.jpg" part
47 * @return Boolean: success
48 */
49 public function execute( $subPage ) {
50 global $wgOut, $wgUser;
51
52 if ( !$this->userCanExecute( $wgUser ) ) {
53 $this->displayRestrictionError();
54 return;
55 }
56
57 // prevent callers from doing standard HTML output -- we'll take it from here
58 $wgOut->disable();
59
60 if ( !isset( $subPage ) || $subPage === '' ) {
61 // the user probably visited the page just to see what would happen, so explain it a bit.
62 $code = '400';
63 $message = "Missing key\n\n"
64 . 'This page provides access to temporarily stashed files for the user that '
65 . 'uploaded those files. See the upload API documentation. To access a stashed file, '
66 . 'use the URL of this page, with a slash and the key of the stashed file appended.';
67 } else {
68 try {
69 if ( preg_match( '/^(\d+)px-(.*)$/', $subPage, $matches ) ) {
70 list( /* full match */, $width, $key ) = $matches;
71 return $this->outputThumbFromStash( $key, $width );
72 } else {
73 return $this->outputFileFromStash( $subPage );
74 }
75 } catch( UploadStashFileNotFoundException $e ) {
76 $code = 404;
77 $message = $e->getMessage();
78 } catch( UploadStashZeroLengthFileException $e ) {
79 $code = 500;
80 $message = $e->getMessage();
81 } catch( UploadStashBadPathException $e ) {
82 $code = 500;
83 $message = $e->getMessage();
84 } catch( SpecialUploadStashTooLargeException $e ) {
85 $code = 500;
86 $message = 'Cannot serve a file larger than ' . self::MAX_SERVE_BYTES . ' bytes. ' . $e->getMessage();
87 } catch( Exception $e ) {
88 $code = 500;
89 $message = $e->getMessage();
90 }
91 }
92
93 wfHttpError( $code, OutputPage::getStatusMessage( $code ), $message );
94 return false;
95 }
96
97 /**
98 * Get a file from stash and stream it out. Rely on parent to catch exceptions and transform them into HTTP
99 * @param String: $key - key of this file in the stash, which probably looks like a filename with extension.
100 * @throws ....?
101 * @return boolean
102 */
103 private function outputFileFromStash( $key ) {
104 $file = $this->stash->getFile( $key );
105 $this->outputLocalFile( $file );
106 return true;
107 }
108
109
110 /**
111 * Get a thumbnail for file, either generated locally or remotely, and stream it out
112 * @param String $key: key for the file in the stash
113 * @param int $width: width of desired thumbnail
114 * @return ??
115 */
116 private function outputThumbFromStash( $key, $width ) {
117
118 // this global, if it exists, points to a "scaler", as you might find in the Wikimedia Foundation cluster. See outputRemoteScaledThumb()
119 global $wgUploadStashScalerBaseUrl;
120
121 // let exceptions propagate to caller.
122 $file = $this->stash->getFile( $key );
123
124 // OK, we're here and no exception was thrown,
125 // so the original file must exist.
126
127 // let's get ready to transform the original -- these are standard
128 $params = array( 'width' => $width );
129 $flags = 0;
130
131 return $wgUploadStashScalerBaseUrl ? $this->outputRemoteScaledThumb( $file, $params, $flags )
132 : $this->outputLocallyScaledThumb( $file, $params, $flags );
133
134 }
135
136
137 /**
138 * Scale a file (probably with a locally installed imagemagick, or similar) and output it to STDOUT.
139 * @param $file: File object
140 * @param $params: scaling parameters ( e.g. array( width => '50' ) );
141 * @param $flags: scaling flags ( see File:: constants )
142 * @throws MWException
143 * @return boolean success
144 */
145 private function outputLocallyScaledThumb( $file, $params, $flags ) {
146
147 // n.b. this is stupid, we insist on re-transforming the file every time we are invoked. We rely
148 // on HTTP caching to ensure this doesn't happen.
149
150 $flags |= File::RENDER_NOW;
151
152 $thumbnailImage = $file->transform( $params, $flags );
153 if ( !$thumbnailImage ) {
154 throw new MWException( 'Could not obtain thumbnail' );
155 }
156
157 // we should have just generated it locally
158 if ( ! $thumbnailImage->getPath() ) {
159 throw new UploadStashFileNotFoundException( "no local path for scaled item" );
160 }
161
162 // now we should construct a File, so we can get mime and other such info in a standard way
163 // n.b. mimetype may be different from original (ogx original -> jpeg thumb)
164 $thumbFile = new UnregisteredLocalFile( false, $this->stash->repo, $thumbnailImage->getPath(), false );
165 if ( ! $thumbFile ) {
166 throw new UploadStashFileNotFoundException( "couldn't create local file object for thumbnail" );
167 }
168
169 return $this->outputLocalFile( $thumbFile );
170
171 }
172
173 /**
174 * Scale a file with a remote "scaler", as exists on the Wikimedia Foundation cluster, and output it to STDOUT.
175 * Note: unlike the usual thumbnail process, the web client never sees the cluster URL; we do the whole HTTP transaction to the scaler ourselves
176 * and cat the results out.
177 * 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
178 * propagated back to our filesystem. Instead we take the results of the HTTP request instead.
179 * Note: no caching is being done here, although we are instructing the client to cache it forever.
180 * @param $file: File object
181 * @param $params: scaling parameters ( e.g. array( width => '50' ) );
182 * @param $flags: scaling flags ( see File:: constants )
183 * @throws MWException
184 * @return boolean success
185 */
186 private function outputRemoteScaledThumb( $file, $params, $flags ) {
187
188 // this global probably looks something like 'http://upload.wikimedia.org/wikipedia/test/thumb/temp'
189 // do not use trailing slash
190 global $wgUploadStashScalerBaseUrl;
191
192 $scalerThumbName = $file->getParamThumbName( $file->name, $params );
193 $scalerThumbUrl = $wgUploadStashScalerBaseUrl . '/' . $file->getRel() . '/' . $scalerThumbName;
194
195 // make a curl call to the scaler to create a thumbnail
196 $httpOptions = array(
197 'method' => 'GET',
198 'timeout' => 'default'
199 );
200 $req = MWHttpRequest::factory( $scalerThumbUrl, $httpOptions );
201 $status = $req->execute();
202 if ( ! $status->isOK() ) {
203 $errors = $status->getErrorsArray();
204 throw new MWException( "Fetching thumbnail failed: " . join( ", ", $errors ) );
205 }
206 $contentType = $req->getResponseHeader( "content-type" );
207 if ( ! $contentType ) {
208 throw new MWException( "Missing content-type header" );
209 }
210 return $this->outputContents( $req->getContent(), $contentType );
211 }
212
213 /**
214 * Output HTTP response for file
215 * Side effect: writes HTTP response to STDOUT.
216 * XXX could use wfStreamfile (in includes/Streamfile.php), but for consistency with outputContents() doing it this way.
217 * XXX is mimeType really enough, or do we need encoding for full Content-Type header?
218 *
219 * @param $file File object with a local path (e.g. UnregisteredLocalFile, LocalFile. Oddly these don't share an ancestor!)
220 */
221 private function outputLocalFile( $file ) {
222 if ( $file->getSize() > self::MAX_SERVE_BYTES ) {
223 throw new SpecialUploadStashTooLargeException();
224 }
225 self::outputHeaders( $file->getMimeType(), $file->getSize() );
226 readfile( $file->getPath() );
227 return true;
228 }
229
230 /**
231 * Output HTTP response of raw content
232 * Side effect: writes HTTP response to STDOUT.
233 * @param String $content: content
234 * @param String $mimeType: mime type
235 */
236 private function outputContents( $content, $contentType ) {
237 $size = strlen( $content );
238 if ( $size > self::MAX_SERVE_BYTES ) {
239 throw new SpecialUploadStashTooLargeException();
240 }
241 self::outputHeaders( $contentType, $size );
242 print $content;
243 return true;
244 }
245
246 /**
247 * Output headers for streaming
248 * 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.
249 * Side effect: preps PHP to write headers to STDOUT.
250 * @param String $contentType : string suitable for content-type header
251 * @param String $size: length in bytes
252 */
253 private static function outputHeaders( $contentType, $size ) {
254 header( "Content-Type: $contentType", true );
255 header( 'Content-Transfer-Encoding: binary', true );
256 header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true );
257 header( "Content-Length: $size", true );
258 }
259
260 }
261
262 class SpecialUploadStashTooLargeException extends MWException {};