MediaWiki REL1_37
UploadStash.php
Go to the documentation of this file.
1<?php
24
56 // Format of the key for files -- has to be suitable as a filename itself (e.g. ab12cd34ef.jpg)
57 public const KEY_FORMAT_REGEX = '/^[\w\-\.]+\.\w*$/';
58 private const MAX_US_PROPS_SIZE = 65535;
59
66 public $repo;
67
69 protected $files = [];
70
72 protected $fileMetadata = [];
73
75 protected $fileProps = [];
76
78 private $user;
79
88 public function __construct( FileRepo $repo, UserIdentity $user = null ) {
89 // this might change based on wiki's configuration.
90 $this->repo = $repo;
91
92 // if a user was passed, use it. otherwise, attempt to use the global request context.
93 // this keeps FileRepo from breaking when it creates an UploadStash object
94 $this->user = $user ?? RequestContext::getMain()->getUser();
95 }
96
110 public function getFile( $key, $noAuth = false ) {
111 if ( !preg_match( self::KEY_FORMAT_REGEX, $key ) ) {
113 wfMessage( 'uploadstash-bad-path-bad-format', $key )
114 );
115 }
116
117 if ( !$noAuth && !$this->user->isRegistered() ) {
119 wfMessage( 'uploadstash-not-logged-in' )
120 );
121 }
122
123 if ( !isset( $this->fileMetadata[$key] ) ) {
124 if ( !$this->fetchFileMetadata( $key ) ) {
125 // If nothing was received, it's likely due to replication lag.
126 // Check the primary DB to see if the record is there.
127 $this->fetchFileMetadata( $key, DB_PRIMARY );
128 }
129
130 if ( !isset( $this->fileMetadata[$key] ) ) {
132 wfMessage( 'uploadstash-file-not-found', $key )
133 );
134 }
135
136 // create $this->files[$key]
137 $this->initFile( $key );
138
139 // fetch fileprops
140 if (
141 isset( $this->fileMetadata[$key]['us_props'] ) && strlen( $this->fileMetadata[$key]['us_props'] )
142 ) {
143 $this->fileProps[$key] = unserialize( $this->fileMetadata[$key]['us_props'] );
144 } else { // b/c for rows with no us_props
145 wfDebug( __METHOD__ . " fetched props for $key from file" );
146 $path = $this->fileMetadata[$key]['us_path'];
147 $this->fileProps[$key] = $this->repo->getFileProps( $path );
148 }
149 }
150
151 if ( !$this->files[$key]->exists() ) {
152 wfDebug( __METHOD__ . " tried to get file at $key, but it doesn't exist" );
153 // @todo Is this not an UploadStashFileNotFoundException case?
155 wfMessage( 'uploadstash-bad-path' )
156 );
157 }
158
159 if ( !$noAuth && $this->fileMetadata[$key]['us_user'] != $this->user->getId() ) {
161 wfMessage( 'uploadstash-wrong-owner', $key )
162 );
163 }
164
165 return $this->files[$key];
166 }
167
174 public function getMetadata( $key ) {
175 $this->getFile( $key );
176
177 return $this->fileMetadata[$key];
178 }
179
186 public function getFileProps( $key ) {
187 $this->getFile( $key );
188
189 return $this->fileProps[$key];
190 }
191
204 public function stashFile( $path, $sourceType = null ) {
205 if ( !is_file( $path ) ) {
206 wfDebug( __METHOD__ . " tried to stash file at '$path', but it doesn't exist" );
208 wfMessage( 'uploadstash-bad-path' )
209 );
210 }
211
212 $mwProps = new MWFileProps( MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer() );
213 $fileProps = $mwProps->getPropsFromPath( $path, true );
214 wfDebug( __METHOD__ . " stashing file at '$path'" );
215
216 // we will be initializing from some tmpnam files that don't have extensions.
217 // most of MediaWiki assumes all uploaded files have good extensions. So, we fix this.
218 $extension = self::getExtensionForPath( $path );
219 if ( !preg_match( "/\\.\\Q$extension\\E$/", $path ) ) {
220 $pathWithGoodExtension = "$path.$extension";
221 } else {
222 $pathWithGoodExtension = $path;
223 }
224
225 // If no key was supplied, make one. a mysql insertid would be totally
226 // reasonable here, except that for historical reasons, the key is this
227 // random thing instead. At least it's not guessable.
228 // Some things that when combined will make a suitably unique key.
229 // see: http://www.jwz.org/doc/mid.html
230 list( $usec, $sec ) = explode( ' ', microtime() );
231 $usec = substr( $usec, 2 );
232 $key = Wikimedia\base_convert( $sec . $usec, 10, 36 ) . '.' .
233 Wikimedia\base_convert( mt_rand(), 10, 36 ) . '.' .
234 $this->user->getId() . '.' .
235 $extension;
236
237 $this->fileProps[$key] = $fileProps;
238
239 if ( !preg_match( self::KEY_FORMAT_REGEX, $key ) ) {
241 wfMessage( 'uploadstash-bad-path-bad-format', $key )
242 );
243 }
244
245 wfDebug( __METHOD__ . " key for '$path': $key" );
246
247 // if not already in a temporary area, put it there
248 $storeStatus = $this->repo->storeTemp( basename( $pathWithGoodExtension ), $path );
249
250 if ( !$storeStatus->isOK() ) {
251 // It is a convention in MediaWiki to only return one error per API
252 // exception, even if multiple errors are available. We use reset()
253 // to pick the "first" thing that was wrong, preferring errors to
254 // warnings. This is a bit lame, as we may have more info in the
255 // $storeStatus and we're throwing it away, but to fix it means
256 // redesigning API errors significantly.
257 // $storeStatus->value just contains the virtual URL (if anything)
258 // which is probably useless to the caller.
259 $error = $storeStatus->getErrorsArray();
260 $error = reset( $error );
261 if ( !count( $error ) ) {
262 $error = $storeStatus->getWarningsArray();
263 $error = reset( $error );
264 if ( !count( $error ) ) {
265 $error = [ 'unknown', 'no error recorded' ];
266 }
267 }
268 // At this point, $error should contain the single "most important"
269 // error, plus any parameters.
270 $errorMsg = array_shift( $error );
271 throw new UploadStashFileException( wfMessage( $errorMsg, $error ) );
272 }
273 $stashPath = $storeStatus->value;
274
275 // fetch the current user ID
276 if ( !$this->user->isRegistered() ) {
278 wfMessage( 'uploadstash-not-logged-in' )
279 );
280 }
281
282 // insert the file metadata into the db.
283 wfDebug( __METHOD__ . " inserting $stashPath under $key" );
284 $dbw = $this->repo->getPrimaryDB();
285
286 $serializedFileProps = serialize( $fileProps );
287 if ( strlen( $serializedFileProps ) > self::MAX_US_PROPS_SIZE ) {
288 // Database is going to truncate this and make the field invalid.
289 // Prioritize important metadata over file handler metadata.
290 // File handler should be prepared to regenerate invalid metadata if needed.
291 $fileProps['metadata'] = [];
292 $serializedFileProps = serialize( $fileProps );
293 }
294
295 $insertRow = [
296 'us_user' => $this->user->getId(),
297 'us_key' => $key,
298 'us_orig_path' => $path,
299 'us_path' => $stashPath, // virtual URL
300 'us_props' => $dbw->encodeBlob( $serializedFileProps ),
301 'us_size' => $fileProps['size'],
302 'us_sha1' => $fileProps['sha1'],
303 'us_mime' => $fileProps['mime'],
304 'us_media_type' => $fileProps['media_type'],
305 'us_image_width' => $fileProps['width'],
306 'us_image_height' => $fileProps['height'],
307 'us_image_bits' => $fileProps['bits'],
308 'us_source_type' => $sourceType,
309 'us_timestamp' => $dbw->timestamp(),
310 'us_status' => 'finished'
311 ];
312
313 $dbw->insert(
314 'uploadstash',
315 $insertRow,
316 __METHOD__
317 );
318
319 // store the insertid in the class variable so immediate retrieval
320 // (possibly laggy) isn't necessary.
321 $insertRow['us_id'] = $dbw->insertId();
322
323 $this->fileMetadata[$key] = $insertRow;
324
325 # create the UploadStashFile object for this file.
326 $this->initFile( $key );
327
328 return $this->getFile( $key );
329 }
330
338 public function clear() {
339 if ( !$this->user->isRegistered() ) {
341 wfMessage( 'uploadstash-not-logged-in' )
342 );
343 }
344
345 wfDebug( __METHOD__ . ' clearing all rows for user ' . $this->user->getId() );
346 $dbw = $this->repo->getPrimaryDB();
347 $dbw->delete(
348 'uploadstash',
349 [ 'us_user' => $this->user->getId() ],
350 __METHOD__
351 );
352
353 # destroy objects.
354 $this->files = [];
355 $this->fileMetadata = [];
356
357 return true;
358 }
359
368 public function removeFile( $key ) {
369 if ( !$this->user->isRegistered() ) {
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->selectRow(
380 'uploadstash',
381 'us_user',
382 [ 'us_key' => $key ],
383 __METHOD__
384 );
385
386 if ( !$row ) {
388 wfMessage( 'uploadstash-no-such-key', $key )
389 );
390 }
391
392 if ( $row->us_user != $this->user->getId() ) {
394 wfMessage( 'uploadstash-wrong-owner', $key )
395 );
396 }
397
398 return $this->removeFileNoAuth( $key );
399 }
400
407 public function removeFileNoAuth( $key ) {
408 wfDebug( __METHOD__ . " clearing row $key" );
409
410 // Ensure we have the UploadStashFile loaded for this key
411 $this->getFile( $key, true );
412
413 $dbw = $this->repo->getPrimaryDB();
414
415 $dbw->delete(
416 'uploadstash',
417 [ 'us_key' => $key ],
418 __METHOD__
419 );
420
424 $this->files[$key]->remove();
425
426 unset( $this->files[$key] );
427 unset( $this->fileMetadata[$key] );
428
429 return true;
430 }
431
438 public function listFiles() {
439 if ( !$this->user->isRegistered() ) {
441 wfMessage( 'uploadstash-not-logged-in' )
442 );
443 }
444
445 $dbr = $this->repo->getReplicaDB();
446 $res = $dbr->select(
447 'uploadstash',
448 'us_key',
449 [ 'us_user' => $this->user->getId() ],
450 __METHOD__
451 );
452
453 if ( !is_object( $res ) || $res->numRows() == 0 ) {
454 // nothing to do.
455 return false;
456 }
457
458 // finish the read before starting writes.
459 $keys = [];
460 foreach ( $res as $row ) {
461 array_push( $keys, $row->us_key );
462 }
463
464 return $keys;
465 }
466
476 public static function getExtensionForPath( $path ) {
478 // Does this have an extension?
479 $n = strrpos( $path, '.' );
480
481 if ( $n !== false ) {
482 $extension = $n ? substr( $path, $n + 1 ) : '';
483 } else {
484 // If not, assume that it should be related to the MIME type of the original file.
485 $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
486 $mimeType = $magic->guessMimeType( $path );
487 $extension = $magic->getExtensionFromMimeTypeOrNull( $mimeType );
488 }
489
490 $extension = File::normalizeExtension( $extension );
491 if ( in_array( $extension, $wgProhibitedFileExtensions ) ) {
492 // The file should already be checked for being evil.
493 // However, if somehow we got here, we definitely
494 // don't want to give it an extension of .php and
495 // put it in a web accessible directory.
496 return '';
497 }
498
499 return $extension;
500 }
501
509 protected function fetchFileMetadata( $key, $readFromDB = DB_REPLICA ) {
510 // populate $fileMetadata[$key]
511 $dbr = null;
512 if ( $readFromDB === DB_PRIMARY ) {
513 // sometimes reading from the primary DB is necessary, if there's replication lag.
514 $dbr = $this->repo->getPrimaryDB();
515 } else {
516 $dbr = $this->repo->getReplicaDB();
517 }
518
519 $row = $dbr->selectRow(
520 'uploadstash',
521 [
522 'us_user', 'us_key', 'us_orig_path', 'us_path', 'us_props',
523 'us_size', 'us_sha1', 'us_mime', 'us_media_type',
524 'us_image_width', 'us_image_height', 'us_image_bits',
525 'us_source_type', 'us_timestamp', 'us_status',
526 ],
527 [ 'us_key' => $key ],
528 __METHOD__
529 );
530
531 if ( !is_object( $row ) ) {
532 // key wasn't present in the database. this will happen sometimes.
533 return false;
534 }
535
536 $this->fileMetadata[$key] = (array)$row;
537 $this->fileMetadata[$key]['us_props'] = $dbr->decodeBlob( $row->us_props );
538
539 return true;
540 }
541
549 protected function initFile( $key ) {
550 $file = new UploadStashFile( $this->repo, $this->fileMetadata[$key]['us_path'], $key );
551 if ( $file->getSize() === 0 ) {
553 wfMessage( 'uploadstash-zero-length' )
554 );
555 }
556 $this->files[$key] = $file;
557
558 return true;
559 }
560}
serialize()
unserialize( $serialized)
$wgProhibitedFileExtensions
Files with these extensions will never be allowed as uploads.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
getFile()
Get the file for this page, if one exists.
Base class for file repositories.
Definition FileRepo.php:45
A repository that stores files in the local filesystem and registers them in the wiki's own database.
Definition LocalRepo.php:41
MimeMagic helper wrapper.
UploadStash is intended to accomplish a few things:
static getExtensionForPath( $path)
Find or guess extension – ensuring that our extension matches our MIME type.
removeFile( $key)
Remove a particular file from the stash.
const KEY_FORMAT_REGEX
fetchFileMetadata( $key, $readFromDB=DB_REPLICA)
Helper function: do the actual database query to fetch file metadata.
getFileProps( $key)
Getter for fileProps.
stashFile( $path, $sourceType=null)
Stash a file in a temp directory and record that we did this in the database, along with other metada...
clear()
Remove all files from the stash.
array $fileMetadata
cache of the file metadata that's stored in the database
array $fileProps
fileprops cache
const MAX_US_PROPS_SIZE
listFiles()
List all files in the stash.
__construct(FileRepo $repo, UserIdentity $user=null)
Represents a temporary filestore, with metadata in the database.
getMetadata( $key)
Getter for file metadata.
removeFileNoAuth( $key)
Remove a file (see removeFile), but doesn't check ownership first.
initFile( $key)
Helper function: Initialize the UploadStashFile for a given file.
getFile( $key, $noAuth=false)
Get a file and its metadata from the stash.
UserIdentity $user
LocalRepo $repo
repository that this uses to store temp files public because we sometimes need to get a LocalFile wit...
array $files
array of initialized repo objects
Interface for objects representing user identity.
A helper class for throttling authentication attempts.
const DB_REPLICA
Definition defines.php:25
const DB_PRIMARY
Definition defines.php:27
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42