MediaWiki master
UploadStash.php
Go to the documentation of this file.
1<?php
30
62 // Format of the key for files -- has to be suitable as a filename itself (e.g. ab12cd34ef.jpg)
63 public const KEY_FORMAT_REGEX = '/^[\w\-\.]+\.\w*$/';
64 private const MAX_US_PROPS_SIZE = 65535;
65
72 public $repo;
73
75 protected $files = [];
76
78 protected $fileMetadata = [];
79
81 protected $fileProps = [];
82
84 private $user;
85
94 public function __construct( FileRepo $repo, ?UserIdentity $user = null ) {
95 // this might change based on wiki's configuration.
96 $this->repo = $repo;
97
98 // if a user was passed, use it. otherwise, attempt to use the global request context.
99 // this keeps FileRepo from breaking when it creates an UploadStash object
100 $this->user = $user ?? RequestContext::getMain()->getUser();
101 }
102
116 public function getFile( $key, $noAuth = false ) {
117 if ( !preg_match( self::KEY_FORMAT_REGEX, $key ) ) {
119 wfMessage( 'uploadstash-bad-path-bad-format', $key )
120 );
121 }
122
123 if ( !$noAuth && !$this->user->isRegistered() ) {
125 wfMessage( 'uploadstash-not-logged-in' )
126 );
127 }
128
129 if ( !isset( $this->fileMetadata[$key] ) ) {
130 if ( !$this->fetchFileMetadata( $key ) ) {
131 // If nothing was received, it's likely due to replication lag.
132 // Check the primary DB to see if the record is there.
133 $this->fetchFileMetadata( $key, DB_PRIMARY );
134 }
135
136 if ( !isset( $this->fileMetadata[$key] ) ) {
138 wfMessage( 'uploadstash-file-not-found', $key )
139 );
140 }
141
142 // create $this->files[$key]
143 $this->initFile( $key );
144
145 // fetch fileprops
146 if (
147 isset( $this->fileMetadata[$key]['us_props'] ) && strlen( $this->fileMetadata[$key]['us_props'] )
148 ) {
149 $this->fileProps[$key] = unserialize( $this->fileMetadata[$key]['us_props'] );
150 } else { // b/c for rows with no us_props
151 wfDebug( __METHOD__ . " fetched props for $key from file" );
152 $path = $this->fileMetadata[$key]['us_path'];
153 $this->fileProps[$key] = $this->repo->getFileProps( $path );
154 }
155 }
156
157 if ( !$this->files[$key]->exists() ) {
158 wfDebug( __METHOD__ . " tried to get file at $key, but it doesn't exist" );
159 // @todo Is this not an UploadStashFileNotFoundException case?
161 wfMessage( 'uploadstash-bad-path' )
162 );
163 }
164
165 if ( !$noAuth && $this->fileMetadata[$key]['us_user'] != $this->user->getId() ) {
167 wfMessage( 'uploadstash-wrong-owner', $key )
168 );
169 }
170
171 return $this->files[$key];
172 }
173
180 public function getMetadata( $key ) {
181 $this->getFile( $key );
182
183 return $this->fileMetadata[$key];
184 }
185
192 public function getFileProps( $key ) {
193 $this->getFile( $key );
194
195 return $this->fileProps[$key];
196 }
197
211 public function stashFile( $path, $sourceType = null, $fileProps = null ) {
212 if ( !is_file( $path ) ) {
213 wfDebug( __METHOD__ . " tried to stash file at '$path', but it doesn't exist" );
215 wfMessage( 'uploadstash-bad-path' )
216 );
217 }
218
219 // File props is expensive to generate for large files, so reuse if possible.
220 if ( !$fileProps ) {
221 $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
222 $fileProps = $mwProps->getPropsFromPath( $path, true );
223 }
224 wfDebug( __METHOD__ . " stashing file at '$path'" );
225
226 // we will be initializing from some tmpnam files that don't have extensions.
227 // most of MediaWiki assumes all uploaded files have good extensions. So, we fix this.
228 $extension = self::getExtensionForPath( $path );
229 if ( !preg_match( "/\\.\\Q$extension\\E$/", $path ) ) {
230 $pathWithGoodExtension = "$path.$extension";
231 } else {
232 $pathWithGoodExtension = $path;
233 }
234
235 // If no key was supplied, make one. a mysql insertid would be totally
236 // reasonable here, except that for historical reasons, the key is this
237 // random thing instead. At least it's not guessable.
238 // Some things that when combined will make a suitably unique key.
239 // see: http://www.jwz.org/doc/mid.html
240 [ $usec, $sec ] = explode( ' ', microtime() );
241 $usec = substr( $usec, 2 );
242 $key = Wikimedia\base_convert( $sec . $usec, 10, 36 ) . '.' .
243 Wikimedia\base_convert( (string)mt_rand(), 10, 36 ) . '.' .
244 $this->user->getId() . '.' .
245 $extension;
246
247 $this->fileProps[$key] = $fileProps;
248
249 if ( !preg_match( self::KEY_FORMAT_REGEX, $key ) ) {
251 wfMessage( 'uploadstash-bad-path-bad-format', $key )
252 );
253 }
254
255 wfDebug( __METHOD__ . " key for '$path': $key" );
256
257 // if not already in a temporary area, put it there
258 $storeStatus = $this->repo->storeTemp( basename( $pathWithGoodExtension ), $path );
259
260 if ( !$storeStatus->isOK() ) {
261 // It is a convention in MediaWiki to only return one error per API
262 // exception, even if multiple errors are available.[citation needed]
263 // Pick the "first" thing that was wrong, preferring errors to warnings.
264 // This is a bit lame, as we may have more info in the
265 // $storeStatus and we're throwing it away, but to fix it means
266 // redesigning API errors significantly.
267 // $storeStatus->value just contains the virtual URL (if anything)
268 // which is probably useless to the caller.
269 foreach ( $storeStatus->getMessages( 'error' ) as $msg ) {
270 throw new UploadStashFileException( $msg );
271 }
272 foreach ( $storeStatus->getMessages( 'warning' ) as $msg ) {
273 throw new UploadStashFileException( $msg );
274 }
275 // XXX: This isn't a real message, hopefully this case is unreachable
276 throw new UploadStashFileException( [ 'unknown', 'no error recorded' ] );
277 }
278 $stashPath = $storeStatus->value;
279
280 // fetch the current user ID
281 if ( !$this->user->isRegistered() ) {
283 wfMessage( 'uploadstash-not-logged-in' )
284 );
285 }
286
287 // insert the file metadata into the db.
288 wfDebug( __METHOD__ . " inserting $stashPath under $key" );
289 $dbw = $this->repo->getPrimaryDB();
290
291 $serializedFileProps = serialize( $fileProps );
292 if ( strlen( $serializedFileProps ) > self::MAX_US_PROPS_SIZE ) {
293 // Database is going to truncate this and make the field invalid.
294 // Prioritize important metadata over file handler metadata.
295 // File handler should be prepared to regenerate invalid metadata if needed.
296 $fileProps['metadata'] = [];
297 $serializedFileProps = serialize( $fileProps );
298 }
299
300 $insertRow = [
301 'us_user' => $this->user->getId(),
302 'us_key' => $key,
303 'us_orig_path' => $path,
304 'us_path' => $stashPath, // virtual URL
305 'us_props' => $dbw->encodeBlob( $serializedFileProps ),
306 'us_size' => $fileProps['size'],
307 'us_sha1' => $fileProps['sha1'],
308 'us_mime' => $fileProps['mime'],
309 'us_media_type' => $fileProps['media_type'],
310 'us_image_width' => $fileProps['width'],
311 'us_image_height' => $fileProps['height'],
312 'us_image_bits' => $fileProps['bits'],
313 'us_source_type' => $sourceType,
314 'us_timestamp' => $dbw->timestamp(),
315 'us_status' => 'finished'
316 ];
317
318 $dbw->newInsertQueryBuilder()
319 ->insertInto( 'uploadstash' )
320 ->row( $insertRow )
321 ->caller( __METHOD__ )->execute();
322
323 // store the insertid in the class variable so immediate retrieval
324 // (possibly laggy) isn't necessary.
325 $insertRow['us_id'] = $dbw->insertId();
326
327 $this->fileMetadata[$key] = $insertRow;
328
329 # create the UploadStashFile object for this file.
330 $this->initFile( $key );
331
332 return $this->getFile( $key );
333 }
334
342 public function clear() {
343 if ( !$this->user->isRegistered() ) {
345 wfMessage( 'uploadstash-not-logged-in' )
346 );
347 }
348
349 wfDebug( __METHOD__ . ' clearing all rows for user ' . $this->user->getId() );
350 $dbw = $this->repo->getPrimaryDB();
351 $dbw->newDeleteQueryBuilder()
352 ->deleteFrom( 'uploadstash' )
353 ->where( [ 'us_user' => $this->user->getId() ] )
354 ->caller( __METHOD__ )->execute();
355
356 # destroy objects.
357 $this->files = [];
358 $this->fileMetadata = [];
359
360 return true;
361 }
362
371 public function removeFile( $key ) {
372 if ( !$this->user->isRegistered() ) {
374 wfMessage( 'uploadstash-not-logged-in' )
375 );
376 }
377
378 $dbw = $this->repo->getPrimaryDB();
379
380 // this is a cheap query. it runs on the primary DB so that this function
381 // still works when there's lag. It won't be called all that often.
382 $row = $dbw->newSelectQueryBuilder()
383 ->select( 'us_user' )
384 ->from( 'uploadstash' )
385 ->where( [ 'us_key' => $key ] )
386 ->caller( __METHOD__ )->fetchRow();
387
388 if ( !$row ) {
390 wfMessage( 'uploadstash-no-such-key', $key )
391 );
392 }
393
394 if ( $row->us_user != $this->user->getId() ) {
396 wfMessage( 'uploadstash-wrong-owner', $key )
397 );
398 }
399
400 return $this->removeFileNoAuth( $key );
401 }
402
409 public function removeFileNoAuth( $key ) {
410 wfDebug( __METHOD__ . " clearing row $key" );
411
412 // Ensure we have the UploadStashFile loaded for this key
413 $this->getFile( $key, true );
414
415 $dbw = $this->repo->getPrimaryDB();
416
417 $dbw->newDeleteQueryBuilder()
418 ->deleteFrom( 'uploadstash' )
419 ->where( [ 'us_key' => $key ] )
420 ->caller( __METHOD__ )->execute();
421
425 $this->files[$key]->remove();
426
427 unset( $this->files[$key] );
428 unset( $this->fileMetadata[$key] );
429
430 return true;
431 }
432
439 public function listFiles() {
440 if ( !$this->user->isRegistered() ) {
442 wfMessage( 'uploadstash-not-logged-in' )
443 );
444 }
445
446 $res = $this->repo->getReplicaDB()->newSelectQueryBuilder()
447 ->select( 'us_key' )
448 ->from( 'uploadstash' )
449 ->where( [ 'us_user' => $this->user->getId() ] )
450 ->caller( __METHOD__ )->fetchResultSet();
451
452 if ( $res->numRows() == 0 ) {
453 // nothing to do.
454 return false;
455 }
456
457 // finish the read before starting writes.
458 $keys = [];
459 foreach ( $res as $row ) {
460 $keys[] = $row->us_key;
461 }
462
463 return $keys;
464 }
465
475 public static function getExtensionForPath( $path ) {
476 $prohibitedFileExtensions = MediaWikiServices::getInstance()
477 ->getMainConfig()->get( MainConfigNames::ProhibitedFileExtensions );
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 = 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, $prohibitedFileExtensions ) ) {
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 if ( $readFromDB === DB_PRIMARY ) {
512 // sometimes reading from the primary DB is necessary, if there's replication lag.
513 $dbr = $this->repo->getPrimaryDB();
514 } else {
515 $dbr = $this->repo->getReplicaDB();
516 }
517
518 $row = $dbr->newSelectQueryBuilder()
519 ->select( [
520 'us_user', 'us_key', 'us_orig_path', 'us_path', 'us_props',
521 'us_size', 'us_sha1', 'us_mime', 'us_media_type',
522 'us_image_width', 'us_image_height', 'us_image_bits',
523 'us_source_type', 'us_timestamp', 'us_status',
524 ] )
525 ->from( 'uploadstash' )
526 ->where( [ 'us_key' => $key ] )
527 ->caller( __METHOD__ )->fetchRow();
528
529 if ( !is_object( $row ) ) {
530 // key wasn't present in the database. this will happen sometimes.
531 return false;
532 }
533
534 $this->fileMetadata[$key] = (array)$row;
535 $this->fileMetadata[$key]['us_props'] = $dbr->decodeBlob( $row->us_props );
536
537 return true;
538 }
539
547 protected function initFile( $key ) {
548 $file = new UploadStashFile(
549 $this->repo,
550 $this->fileMetadata[$key]['us_path'],
551 $key,
552 $this->fileMetadata[$key]['us_sha1'],
553 $this->fileMetadata[$key]['us_mime'] ?? false
554 );
555 if ( $file->getSize() === 0 ) {
557 wfMessage( 'uploadstash-zero-length' )
558 );
559 }
560 $this->files[$key] = $file;
561
562 return true;
563 }
564}
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.
MimeMagic helper wrapper.
Group all the pieces relevant to the context of a request into one instance.
Base class for file repositories.
Definition FileRepo.php:68
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:93
Local repository that stores files in the local filesystem and registers them in the wiki's own datab...
Definition LocalRepo.php:57
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
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.
clear()
Remove all files from the stash.
array $fileMetadata
cache of the file metadata that's stored in the database
array $fileProps
fileprops cache
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.
stashFile( $path, $sourceType=null, $fileProps=null)
Stash a file in a temp directory and record that we did this in the database, along with other metada...
getFile( $key, $noAuth=false)
Get a file and its metadata from the stash.
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.
const DB_REPLICA
Definition defines.php:26
const DB_PRIMARY
Definition defines.php:28