Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 205 |
|
0.00% |
0 / 12 |
CRAP | |
0.00% |
0 / 1 |
UploadStash | |
0.00% |
0 / 205 |
|
0.00% |
0 / 12 |
2070 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getFile | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
156 | |||
getMetadata | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getFileProps | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
stashFile | |
0.00% |
0 / 68 |
|
0.00% |
0 / 1 |
110 | |||
clear | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
removeFile | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
20 | |||
removeFileNoAuth | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
listFiles | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
getExtensionForPath | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
fetchFileMetadata | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
12 | |||
initFile | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | /** |
3 | * Temporary storage for uploaded files. |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | */ |
22 | |
23 | use MediaWiki\Context\RequestContext; |
24 | use MediaWiki\MainConfigNames; |
25 | use MediaWiki\MediaWikiServices; |
26 | use MediaWiki\User\UserIdentity; |
27 | |
28 | /** |
29 | * UploadStash is intended to accomplish a few things: |
30 | * - Enable applications to temporarily stash files without publishing them to |
31 | * the wiki. |
32 | * - Several parts of MediaWiki do this in similar ways: UploadBase, |
33 | * UploadWizard, and FirefoggChunkedExtension. |
34 | * And there are several that reimplement stashing from scratch, in |
35 | * idiosyncratic ways. The idea is to unify them all here. |
36 | * Mostly all of them are the same except for storing some custom fields, |
37 | * which we subsume into the data array. |
38 | * - Enable applications to find said files later, as long as the db table or |
39 | * temp files haven't been purged. |
40 | * - Enable the uploading user (and *ONLY* the uploading user) to access said |
41 | * files, and thumbnails of said files, via a URL. We accomplish this using |
42 | * a database table, with ownership checking as you might expect. See |
43 | * SpecialUploadStash, which implements a web interface to some files stored |
44 | * this way. |
45 | * |
46 | * UploadStash right now is *mostly* intended to show you one user's slice of |
47 | * the entire stash. The user parameter is only optional because there are few |
48 | * cases where we clean out the stash from an automated script. In the future we |
49 | * might refactor this. |
50 | * |
51 | * UploadStash represents the entire stash of temporary files. |
52 | * UploadStashFile is a filestore for the actual physical disk files. |
53 | * UploadFromStash extends UploadBase, and represents a single stashed file as |
54 | * it is moved from the stash to the regular file repository |
55 | * |
56 | * @ingroup Upload |
57 | */ |
58 | class UploadStash { |
59 | // Format of the key for files -- has to be suitable as a filename itself (e.g. ab12cd34ef.jpg) |
60 | public const KEY_FORMAT_REGEX = '/^[\w\-\.]+\.\w*$/'; |
61 | private const MAX_US_PROPS_SIZE = 65535; |
62 | |
63 | /** |
64 | * repository that this uses to store temp files |
65 | * public because we sometimes need to get a LocalFile within the same repo. |
66 | * |
67 | * @var LocalRepo |
68 | */ |
69 | public $repo; |
70 | |
71 | /** @var array array of initialized repo objects */ |
72 | protected $files = []; |
73 | |
74 | /** @var array cache of the file metadata that's stored in the database */ |
75 | protected $fileMetadata = []; |
76 | |
77 | /** @var array fileprops cache */ |
78 | protected $fileProps = []; |
79 | |
80 | /** @var UserIdentity */ |
81 | private $user; |
82 | |
83 | /** |
84 | * Represents a temporary filestore, with metadata in the database. |
85 | * Designed to be compatible with the session stashing code in UploadBase |
86 | * (should replace it eventually). |
87 | * |
88 | * @param FileRepo $repo |
89 | * @param UserIdentity|null $user |
90 | */ |
91 | public function __construct( FileRepo $repo, ?UserIdentity $user = null ) { |
92 | // this might change based on wiki's configuration. |
93 | $this->repo = $repo; |
94 | |
95 | // if a user was passed, use it. otherwise, attempt to use the global request context. |
96 | // this keeps FileRepo from breaking when it creates an UploadStash object |
97 | $this->user = $user ?? RequestContext::getMain()->getUser(); |
98 | } |
99 | |
100 | /** |
101 | * Get a file and its metadata from the stash. |
102 | * The noAuth param is a bit janky but is required for automated scripts |
103 | * which clean out the stash. |
104 | * |
105 | * @param string $key Key under which file information is stored |
106 | * @param bool $noAuth (optional) Don't check authentication. Used by maintenance scripts. |
107 | * @throws UploadStashFileNotFoundException |
108 | * @throws UploadStashNotLoggedInException |
109 | * @throws UploadStashWrongOwnerException |
110 | * @throws UploadStashBadPathException |
111 | * @return UploadStashFile |
112 | */ |
113 | public function getFile( $key, $noAuth = false ) { |
114 | if ( !preg_match( self::KEY_FORMAT_REGEX, $key ) ) { |
115 | throw new UploadStashBadPathException( |
116 | wfMessage( 'uploadstash-bad-path-bad-format', $key ) |
117 | ); |
118 | } |
119 | |
120 | if ( !$noAuth && !$this->user->isRegistered() ) { |
121 | throw new UploadStashNotLoggedInException( |
122 | wfMessage( 'uploadstash-not-logged-in' ) |
123 | ); |
124 | } |
125 | |
126 | if ( !isset( $this->fileMetadata[$key] ) ) { |
127 | if ( !$this->fetchFileMetadata( $key ) ) { |
128 | // If nothing was received, it's likely due to replication lag. |
129 | // Check the primary DB to see if the record is there. |
130 | $this->fetchFileMetadata( $key, DB_PRIMARY ); |
131 | } |
132 | |
133 | if ( !isset( $this->fileMetadata[$key] ) ) { |
134 | throw new UploadStashFileNotFoundException( |
135 | wfMessage( 'uploadstash-file-not-found', $key ) |
136 | ); |
137 | } |
138 | |
139 | // create $this->files[$key] |
140 | $this->initFile( $key ); |
141 | |
142 | // fetch fileprops |
143 | if ( |
144 | isset( $this->fileMetadata[$key]['us_props'] ) && strlen( $this->fileMetadata[$key]['us_props'] ) |
145 | ) { |
146 | $this->fileProps[$key] = unserialize( $this->fileMetadata[$key]['us_props'] ); |
147 | } else { // b/c for rows with no us_props |
148 | wfDebug( __METHOD__ . " fetched props for $key from file" ); |
149 | $path = $this->fileMetadata[$key]['us_path']; |
150 | $this->fileProps[$key] = $this->repo->getFileProps( $path ); |
151 | } |
152 | } |
153 | |
154 | if ( !$this->files[$key]->exists() ) { |
155 | wfDebug( __METHOD__ . " tried to get file at $key, but it doesn't exist" ); |
156 | // @todo Is this not an UploadStashFileNotFoundException case? |
157 | throw new UploadStashBadPathException( |
158 | wfMessage( 'uploadstash-bad-path' ) |
159 | ); |
160 | } |
161 | |
162 | if ( !$noAuth && $this->fileMetadata[$key]['us_user'] != $this->user->getId() ) { |
163 | throw new UploadStashWrongOwnerException( |
164 | wfMessage( 'uploadstash-wrong-owner', $key ) |
165 | ); |
166 | } |
167 | |
168 | return $this->files[$key]; |
169 | } |
170 | |
171 | /** |
172 | * Getter for file metadata. |
173 | * |
174 | * @param string $key Key under which file information is stored |
175 | * @return array |
176 | */ |
177 | public function getMetadata( $key ) { |
178 | $this->getFile( $key ); |
179 | |
180 | return $this->fileMetadata[$key]; |
181 | } |
182 | |
183 | /** |
184 | * Getter for fileProps |
185 | * |
186 | * @param string $key Key under which file information is stored |
187 | * @return array |
188 | */ |
189 | public function getFileProps( $key ) { |
190 | $this->getFile( $key ); |
191 | |
192 | return $this->fileProps[$key]; |
193 | } |
194 | |
195 | /** |
196 | * Stash a file in a temp directory and record that we did this in the |
197 | * database, along with other metadata. |
198 | * |
199 | * @param string $path Path to file you want stashed |
200 | * @param string|null $sourceType The type of upload that generated this file |
201 | * (currently, I believe, 'file' or null) |
202 | * @param array|null $fileProps File props or null to regenerate |
203 | * @throws UploadStashBadPathException |
204 | * @throws UploadStashFileException |
205 | * @throws UploadStashNotLoggedInException |
206 | * @return UploadStashFile|null File, or null on failure |
207 | */ |
208 | public function stashFile( $path, $sourceType = null, $fileProps = null ) { |
209 | if ( !is_file( $path ) ) { |
210 | wfDebug( __METHOD__ . " tried to stash file at '$path', but it doesn't exist" ); |
211 | throw new UploadStashBadPathException( |
212 | wfMessage( 'uploadstash-bad-path' ) |
213 | ); |
214 | } |
215 | |
216 | // File props is expensive to generate for large files, so reuse if possible. |
217 | if ( !$fileProps ) { |
218 | $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() ); |
219 | $fileProps = $mwProps->getPropsFromPath( $path, true ); |
220 | } |
221 | wfDebug( __METHOD__ . " stashing file at '$path'" ); |
222 | |
223 | // we will be initializing from some tmpnam files that don't have extensions. |
224 | // most of MediaWiki assumes all uploaded files have good extensions. So, we fix this. |
225 | $extension = self::getExtensionForPath( $path ); |
226 | if ( !preg_match( "/\\.\\Q$extension\\E$/", $path ) ) { |
227 | $pathWithGoodExtension = "$path.$extension"; |
228 | } else { |
229 | $pathWithGoodExtension = $path; |
230 | } |
231 | |
232 | // If no key was supplied, make one. a mysql insertid would be totally |
233 | // reasonable here, except that for historical reasons, the key is this |
234 | // random thing instead. At least it's not guessable. |
235 | // Some things that when combined will make a suitably unique key. |
236 | // see: http://www.jwz.org/doc/mid.html |
237 | [ $usec, $sec ] = explode( ' ', microtime() ); |
238 | $usec = substr( $usec, 2 ); |
239 | $key = Wikimedia\base_convert( $sec . $usec, 10, 36 ) . '.' . |
240 | Wikimedia\base_convert( (string)mt_rand(), 10, 36 ) . '.' . |
241 | $this->user->getId() . '.' . |
242 | $extension; |
243 | |
244 | $this->fileProps[$key] = $fileProps; |
245 | |
246 | if ( !preg_match( self::KEY_FORMAT_REGEX, $key ) ) { |
247 | throw new UploadStashBadPathException( |
248 | wfMessage( 'uploadstash-bad-path-bad-format', $key ) |
249 | ); |
250 | } |
251 | |
252 | wfDebug( __METHOD__ . " key for '$path': $key" ); |
253 | |
254 | // if not already in a temporary area, put it there |
255 | $storeStatus = $this->repo->storeTemp( basename( $pathWithGoodExtension ), $path ); |
256 | |
257 | if ( !$storeStatus->isOK() ) { |
258 | // It is a convention in MediaWiki to only return one error per API |
259 | // exception, even if multiple errors are available.[citation needed] |
260 | // Pick the "first" thing that was wrong, preferring errors to warnings. |
261 | // This is a bit lame, as we may have more info in the |
262 | // $storeStatus and we're throwing it away, but to fix it means |
263 | // redesigning API errors significantly. |
264 | // $storeStatus->value just contains the virtual URL (if anything) |
265 | // which is probably useless to the caller. |
266 | foreach ( $storeStatus->getMessages( 'error' ) as $msg ) { |
267 | throw new UploadStashFileException( $msg ); |
268 | } |
269 | foreach ( $storeStatus->getMessages( 'warning' ) as $msg ) { |
270 | throw new UploadStashFileException( $msg ); |
271 | } |
272 | // XXX: This isn't a real message, hopefully this case is unreachable |
273 | throw new UploadStashFileException( [ 'unknown', 'no error recorded' ] ); |
274 | } |
275 | $stashPath = $storeStatus->value; |
276 | |
277 | // fetch the current user ID |
278 | if ( !$this->user->isRegistered() ) { |
279 | throw new UploadStashNotLoggedInException( |
280 | wfMessage( 'uploadstash-not-logged-in' ) |
281 | ); |
282 | } |
283 | |
284 | // insert the file metadata into the db. |
285 | wfDebug( __METHOD__ . " inserting $stashPath under $key" ); |
286 | $dbw = $this->repo->getPrimaryDB(); |
287 | |
288 | $serializedFileProps = serialize( $fileProps ); |
289 | if ( strlen( $serializedFileProps ) > self::MAX_US_PROPS_SIZE ) { |
290 | // Database is going to truncate this and make the field invalid. |
291 | // Prioritize important metadata over file handler metadata. |
292 | // File handler should be prepared to regenerate invalid metadata if needed. |
293 | $fileProps['metadata'] = []; |
294 | $serializedFileProps = serialize( $fileProps ); |
295 | } |
296 | |
297 | $insertRow = [ |
298 | 'us_user' => $this->user->getId(), |
299 | 'us_key' => $key, |
300 | 'us_orig_path' => $path, |
301 | 'us_path' => $stashPath, // virtual URL |
302 | 'us_props' => $dbw->encodeBlob( $serializedFileProps ), |
303 | 'us_size' => $fileProps['size'], |
304 | 'us_sha1' => $fileProps['sha1'], |
305 | 'us_mime' => $fileProps['mime'], |
306 | 'us_media_type' => $fileProps['media_type'], |
307 | 'us_image_width' => $fileProps['width'], |
308 | 'us_image_height' => $fileProps['height'], |
309 | 'us_image_bits' => $fileProps['bits'], |
310 | 'us_source_type' => $sourceType, |
311 | 'us_timestamp' => $dbw->timestamp(), |
312 | 'us_status' => 'finished' |
313 | ]; |
314 | |
315 | $dbw->newInsertQueryBuilder() |
316 | ->insertInto( 'uploadstash' ) |
317 | ->row( $insertRow ) |
318 | ->caller( __METHOD__ )->execute(); |
319 | |
320 | // store the insertid in the class variable so immediate retrieval |
321 | // (possibly laggy) isn't necessary. |
322 | $insertRow['us_id'] = $dbw->insertId(); |
323 | |
324 | $this->fileMetadata[$key] = $insertRow; |
325 | |
326 | # create the UploadStashFile object for this file. |
327 | $this->initFile( $key ); |
328 | |
329 | return $this->getFile( $key ); |
330 | } |
331 | |
332 | /** |
333 | * Remove all files from the stash. |
334 | * Does not clean up files in the repo, just the record of them. |
335 | * |
336 | * @throws UploadStashNotLoggedInException |
337 | * @return bool Success |
338 | */ |
339 | public function clear() { |
340 | if ( !$this->user->isRegistered() ) { |
341 | throw new UploadStashNotLoggedInException( |
342 | wfMessage( 'uploadstash-not-logged-in' ) |
343 | ); |
344 | } |
345 | |
346 | wfDebug( __METHOD__ . ' clearing all rows for user ' . $this->user->getId() ); |
347 | $dbw = $this->repo->getPrimaryDB(); |
348 | $dbw->newDeleteQueryBuilder() |
349 | ->deleteFrom( 'uploadstash' ) |
350 | ->where( [ 'us_user' => $this->user->getId() ] ) |
351 | ->caller( __METHOD__ )->execute(); |
352 | |
353 | # destroy objects. |
354 | $this->files = []; |
355 | $this->fileMetadata = []; |
356 | |
357 | return true; |
358 | } |
359 | |
360 | /** |
361 | * Remove a particular file from the stash. Also removes it from the repo. |
362 | * |
363 | * @param string $key |
364 | * @throws UploadStashNoSuchKeyException|UploadStashNotLoggedInException |
365 | * @throws UploadStashWrongOwnerException |
366 | * @return bool Success |
367 | */ |
368 | public function removeFile( $key ) { |
369 | if ( !$this->user->isRegistered() ) { |
370 | throw new UploadStashNotLoggedInException( |
371 | wfMessage( 'uploadstash-not-logged-in' ) |
372 | ); |
373 | } |
374 | |
375 | $dbw = $this->repo->getPrimaryDB(); |
376 | |
377 | // this is a cheap query. it runs on the primary DB so that this function |
378 | // still works when there's lag. It won't be called all that often. |
379 | $row = $dbw->newSelectQueryBuilder() |
380 | ->select( 'us_user' ) |
381 | ->from( 'uploadstash' ) |
382 | ->where( [ 'us_key' => $key ] ) |
383 | ->caller( __METHOD__ )->fetchRow(); |
384 | |
385 | if ( !$row ) { |
386 | throw new UploadStashNoSuchKeyException( |
387 | wfMessage( 'uploadstash-no-such-key', $key ) |
388 | ); |
389 | } |
390 | |
391 | if ( $row->us_user != $this->user->getId() ) { |
392 | throw new UploadStashWrongOwnerException( |
393 | wfMessage( 'uploadstash-wrong-owner', $key ) |
394 | ); |
395 | } |
396 | |
397 | return $this->removeFileNoAuth( $key ); |
398 | } |
399 | |
400 | /** |
401 | * Remove a file (see removeFile), but doesn't check ownership first. |
402 | * |
403 | * @param string $key |
404 | * @return bool Success |
405 | */ |
406 | public function removeFileNoAuth( $key ) { |
407 | wfDebug( __METHOD__ . " clearing row $key" ); |
408 | |
409 | // Ensure we have the UploadStashFile loaded for this key |
410 | $this->getFile( $key, true ); |
411 | |
412 | $dbw = $this->repo->getPrimaryDB(); |
413 | |
414 | $dbw->newDeleteQueryBuilder() |
415 | ->deleteFrom( 'uploadstash' ) |
416 | ->where( [ 'us_key' => $key ] ) |
417 | ->caller( __METHOD__ )->execute(); |
418 | |
419 | /** @todo Look into UnregisteredLocalFile and find out why the rv here is |
420 | * sometimes wrong (false when file was removed). For now, ignore. |
421 | */ |
422 | $this->files[$key]->remove(); |
423 | |
424 | unset( $this->files[$key] ); |
425 | unset( $this->fileMetadata[$key] ); |
426 | |
427 | return true; |
428 | } |
429 | |
430 | /** |
431 | * List all files in the stash. |
432 | * |
433 | * @throws UploadStashNotLoggedInException |
434 | * @return array|false |
435 | */ |
436 | public function listFiles() { |
437 | if ( !$this->user->isRegistered() ) { |
438 | throw new UploadStashNotLoggedInException( |
439 | wfMessage( 'uploadstash-not-logged-in' ) |
440 | ); |
441 | } |
442 | |
443 | $res = $this->repo->getReplicaDB()->newSelectQueryBuilder() |
444 | ->select( 'us_key' ) |
445 | ->from( 'uploadstash' ) |
446 | ->where( [ 'us_user' => $this->user->getId() ] ) |
447 | ->caller( __METHOD__ )->fetchResultSet(); |
448 | |
449 | if ( $res->numRows() == 0 ) { |
450 | // nothing to do. |
451 | return false; |
452 | } |
453 | |
454 | // finish the read before starting writes. |
455 | $keys = []; |
456 | foreach ( $res as $row ) { |
457 | $keys[] = $row->us_key; |
458 | } |
459 | |
460 | return $keys; |
461 | } |
462 | |
463 | /** |
464 | * Find or guess extension -- ensuring that our extension matches our MIME type. |
465 | * Since these files are constructed from php tempnames they may not start off |
466 | * with an extension. |
467 | * XXX this is somewhat redundant with the checks that ApiUpload.php does with incoming |
468 | * uploads versus the desired filename. Maybe we can get that passed to us... |
469 | * @param string $path |
470 | * @return string |
471 | */ |
472 | public static function getExtensionForPath( $path ) { |
473 | $prohibitedFileExtensions = MediaWikiServices::getInstance() |
474 | ->getMainConfig()->get( MainConfigNames::ProhibitedFileExtensions ); |
475 | // Does this have an extension? |
476 | $n = strrpos( $path, '.' ); |
477 | |
478 | if ( $n !== false ) { |
479 | $extension = $n ? substr( $path, $n + 1 ) : ''; |
480 | } else { |
481 | // If not, assume that it should be related to the MIME type of the original file. |
482 | $magic = MediaWikiServices::getInstance()->getMimeAnalyzer(); |
483 | $mimeType = $magic->guessMimeType( $path ); |
484 | $extension = $magic->getExtensionFromMimeTypeOrNull( $mimeType ) ?? ''; |
485 | } |
486 | |
487 | $extension = File::normalizeExtension( $extension ); |
488 | if ( in_array( $extension, $prohibitedFileExtensions ) ) { |
489 | // The file should already be checked for being evil. |
490 | // However, if somehow we got here, we definitely |
491 | // don't want to give it an extension of .php and |
492 | // put it in a web accessible directory. |
493 | return ''; |
494 | } |
495 | |
496 | return $extension; |
497 | } |
498 | |
499 | /** |
500 | * Helper function: do the actual database query to fetch file metadata. |
501 | * |
502 | * @param string $key |
503 | * @param int $readFromDB Constant (default: DB_REPLICA) |
504 | * @return bool |
505 | */ |
506 | protected function fetchFileMetadata( $key, $readFromDB = DB_REPLICA ) { |
507 | // populate $fileMetadata[$key] |
508 | if ( $readFromDB === DB_PRIMARY ) { |
509 | // sometimes reading from the primary DB is necessary, if there's replication lag. |
510 | $dbr = $this->repo->getPrimaryDB(); |
511 | } else { |
512 | $dbr = $this->repo->getReplicaDB(); |
513 | } |
514 | |
515 | $row = $dbr->newSelectQueryBuilder() |
516 | ->select( [ |
517 | 'us_user', 'us_key', 'us_orig_path', 'us_path', 'us_props', |
518 | 'us_size', 'us_sha1', 'us_mime', 'us_media_type', |
519 | 'us_image_width', 'us_image_height', 'us_image_bits', |
520 | 'us_source_type', 'us_timestamp', 'us_status', |
521 | ] ) |
522 | ->from( 'uploadstash' ) |
523 | ->where( [ 'us_key' => $key ] ) |
524 | ->caller( __METHOD__ )->fetchRow(); |
525 | |
526 | if ( !is_object( $row ) ) { |
527 | // key wasn't present in the database. this will happen sometimes. |
528 | return false; |
529 | } |
530 | |
531 | $this->fileMetadata[$key] = (array)$row; |
532 | $this->fileMetadata[$key]['us_props'] = $dbr->decodeBlob( $row->us_props ); |
533 | |
534 | return true; |
535 | } |
536 | |
537 | /** |
538 | * Helper function: Initialize the UploadStashFile for a given file. |
539 | * |
540 | * @param string $key Key under which to store the object |
541 | * @throws UploadStashZeroLengthFileException |
542 | * @return bool |
543 | */ |
544 | protected function initFile( $key ) { |
545 | $file = new UploadStashFile( |
546 | $this->repo, |
547 | $this->fileMetadata[$key]['us_path'], |
548 | $key, |
549 | $this->fileMetadata[$key]['us_sha1'] |
550 | ); |
551 | if ( $file->getSize() === 0 ) { |
552 | throw new UploadStashZeroLengthFileException( |
553 | wfMessage( 'uploadstash-zero-length' ) |
554 | ); |
555 | } |
556 | $this->files[$key] = $file; |
557 | |
558 | return true; |
559 | } |
560 | } |