MediaWiki  master
UploadStash.php
Go to the documentation of this file.
1 <?php
26 
57 class UploadStash {
58  // Format of the key for files -- has to be suitable as a filename itself (e.g. ab12cd34ef.jpg)
59  public const KEY_FORMAT_REGEX = '/^[\w\-\.]+\.\w*$/';
60  private const MAX_US_PROPS_SIZE = 65535;
61 
68  public $repo;
69 
71  protected $files = [];
72 
74  protected $fileMetadata = [];
75 
77  protected $fileProps = [];
78 
80  private $user;
81 
90  public function __construct( FileRepo $repo, UserIdentity $user = null ) {
91  // this might change based on wiki's configuration.
92  $this->repo = $repo;
93 
94  // if a user was passed, use it. otherwise, attempt to use the global request context.
95  // this keeps FileRepo from breaking when it creates an UploadStash object
96  $this->user = $user ?? RequestContext::getMain()->getUser();
97  }
98 
112  public function getFile( $key, $noAuth = false ) {
113  if ( !preg_match( self::KEY_FORMAT_REGEX, $key ) ) {
114  throw new UploadStashBadPathException(
115  wfMessage( 'uploadstash-bad-path-bad-format', $key )
116  );
117  }
118 
119  if ( !$noAuth && !$this->user->isRegistered() ) {
121  wfMessage( 'uploadstash-not-logged-in' )
122  );
123  }
124 
125  if ( !isset( $this->fileMetadata[$key] ) ) {
126  if ( !$this->fetchFileMetadata( $key ) ) {
127  // If nothing was received, it's likely due to replication lag.
128  // Check the primary DB to see if the record is there.
129  $this->fetchFileMetadata( $key, DB_PRIMARY );
130  }
131 
132  if ( !isset( $this->fileMetadata[$key] ) ) {
134  wfMessage( 'uploadstash-file-not-found', $key )
135  );
136  }
137 
138  // create $this->files[$key]
139  $this->initFile( $key );
140 
141  // fetch fileprops
142  if (
143  isset( $this->fileMetadata[$key]['us_props'] ) && strlen( $this->fileMetadata[$key]['us_props'] )
144  ) {
145  $this->fileProps[$key] = unserialize( $this->fileMetadata[$key]['us_props'] );
146  } else { // b/c for rows with no us_props
147  wfDebug( __METHOD__ . " fetched props for $key from file" );
148  $path = $this->fileMetadata[$key]['us_path'];
149  $this->fileProps[$key] = $this->repo->getFileProps( $path );
150  }
151  }
152 
153  if ( !$this->files[$key]->exists() ) {
154  wfDebug( __METHOD__ . " tried to get file at $key, but it doesn't exist" );
155  // @todo Is this not an UploadStashFileNotFoundException case?
156  throw new UploadStashBadPathException(
157  wfMessage( 'uploadstash-bad-path' )
158  );
159  }
160 
161  if ( !$noAuth && $this->fileMetadata[$key]['us_user'] != $this->user->getId() ) {
163  wfMessage( 'uploadstash-wrong-owner', $key )
164  );
165  }
166 
167  return $this->files[$key];
168  }
169 
176  public function getMetadata( $key ) {
177  $this->getFile( $key );
178 
179  return $this->fileMetadata[$key];
180  }
181 
188  public function getFileProps( $key ) {
189  $this->getFile( $key );
190 
191  return $this->fileProps[$key];
192  }
193 
206  public function stashFile( $path, $sourceType = null ) {
207  if ( !is_file( $path ) ) {
208  wfDebug( __METHOD__ . " tried to stash file at '$path', but it doesn't exist" );
209  throw new UploadStashBadPathException(
210  wfMessage( 'uploadstash-bad-path' )
211  );
212  }
213 
214  $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
215  $fileProps = $mwProps->getPropsFromPath( $path, true );
216  wfDebug( __METHOD__ . " stashing file at '$path'" );
217 
218  // we will be initializing from some tmpnam files that don't have extensions.
219  // most of MediaWiki assumes all uploaded files have good extensions. So, we fix this.
220  $extension = self::getExtensionForPath( $path );
221  if ( !preg_match( "/\\.\\Q$extension\\E$/", $path ) ) {
222  $pathWithGoodExtension = "$path.$extension";
223  } else {
224  $pathWithGoodExtension = $path;
225  }
226 
227  // If no key was supplied, make one. a mysql insertid would be totally
228  // reasonable here, except that for historical reasons, the key is this
229  // random thing instead. At least it's not guessable.
230  // Some things that when combined will make a suitably unique key.
231  // see: http://www.jwz.org/doc/mid.html
232  [ $usec, $sec ] = explode( ' ', microtime() );
233  $usec = substr( $usec, 2 );
234  $key = Wikimedia\base_convert( $sec . $usec, 10, 36 ) . '.' .
235  Wikimedia\base_convert( (string)mt_rand(), 10, 36 ) . '.' .
236  $this->user->getId() . '.' .
237  $extension;
238 
239  $this->fileProps[$key] = $fileProps;
240 
241  if ( !preg_match( self::KEY_FORMAT_REGEX, $key ) ) {
242  throw new UploadStashBadPathException(
243  wfMessage( 'uploadstash-bad-path-bad-format', $key )
244  );
245  }
246 
247  wfDebug( __METHOD__ . " key for '$path': $key" );
248 
249  // if not already in a temporary area, put it there
250  $storeStatus = $this->repo->storeTemp( basename( $pathWithGoodExtension ), $path );
251 
252  if ( !$storeStatus->isOK() ) {
253  // It is a convention in MediaWiki to only return one error per API
254  // exception, even if multiple errors are available. We use reset()
255  // to pick the "first" thing that was wrong, preferring errors to
256  // warnings. This is a bit lame, as we may have more info in the
257  // $storeStatus and we're throwing it away, but to fix it means
258  // redesigning API errors significantly.
259  // $storeStatus->value just contains the virtual URL (if anything)
260  // which is probably useless to the caller.
261  $error = $storeStatus->getErrorsArray();
262  $error = reset( $error );
263  if ( !count( $error ) ) {
264  $error = $storeStatus->getWarningsArray();
265  $error = reset( $error );
266  if ( !count( $error ) ) {
267  $error = [ 'unknown', 'no error recorded' ];
268  }
269  }
270  // At this point, $error should contain the single "most important"
271  // error, plus any parameters.
272  $errorMsg = array_shift( $error );
273  throw new UploadStashFileException( wfMessage( $errorMsg, $error ) );
274  }
275  $stashPath = $storeStatus->value;
276 
277  // fetch the current user ID
278  if ( !$this->user->isRegistered() ) {
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->insert(
316  'uploadstash',
317  $insertRow,
318  __METHOD__
319  );
320 
321  // store the insertid in the class variable so immediate retrieval
322  // (possibly laggy) isn't necessary.
323  $insertRow['us_id'] = $dbw->insertId();
324 
325  $this->fileMetadata[$key] = $insertRow;
326 
327  # create the UploadStashFile object for this file.
328  $this->initFile( $key );
329 
330  return $this->getFile( $key );
331  }
332 
340  public function clear() {
341  if ( !$this->user->isRegistered() ) {
343  wfMessage( 'uploadstash-not-logged-in' )
344  );
345  }
346 
347  wfDebug( __METHOD__ . ' clearing all rows for user ' . $this->user->getId() );
348  $dbw = $this->repo->getPrimaryDB();
349  $dbw->delete(
350  'uploadstash',
351  [ 'us_user' => $this->user->getId() ],
352  __METHOD__
353  );
354 
355  # destroy objects.
356  $this->files = [];
357  $this->fileMetadata = [];
358 
359  return true;
360  }
361 
370  public function removeFile( $key ) {
371  if ( !$this->user->isRegistered() ) {
373  wfMessage( 'uploadstash-not-logged-in' )
374  );
375  }
376 
377  $dbw = $this->repo->getPrimaryDB();
378 
379  // this is a cheap query. it runs on the primary DB so that this function
380  // still works when there's lag. It won't be called all that often.
381  $row = $dbw->newSelectQueryBuilder()
382  ->select( 'us_user' )
383  ->from( 'uploadstash' )
384  ->where( [ 'us_key' => $key ] )
385  ->caller( __METHOD__ )->fetchRow();
386 
387  if ( !$row ) {
389  wfMessage( 'uploadstash-no-such-key', $key )
390  );
391  }
392 
393  if ( $row->us_user != $this->user->getId() ) {
395  wfMessage( 'uploadstash-wrong-owner', $key )
396  );
397  }
398 
399  return $this->removeFileNoAuth( $key );
400  }
401 
408  public function removeFileNoAuth( $key ) {
409  wfDebug( __METHOD__ . " clearing row $key" );
410 
411  // Ensure we have the UploadStashFile loaded for this key
412  $this->getFile( $key, true );
413 
414  $dbw = $this->repo->getPrimaryDB();
415 
416  $dbw->delete(
417  'uploadstash',
418  [ 'us_key' => $key ],
419  __METHOD__
420  );
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 ( !is_object( $res ) || $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  array_push( $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( $this->repo, $this->fileMetadata[$key]['us_path'], $key );
549  if ( $file->getSize() === 0 ) {
551  wfMessage( 'uploadstash-zero-length' )
552  );
553  }
554  $this->files[$key] = $file;
555 
556  return true;
557  }
558 }
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.
Base class for file repositories.
Definition: FileRepo.php:47
static normalizeExtension( $extension)
Normalize a file extension to the common form, making it lowercase and checking some synonyms,...
Definition: File.php:255
MimeMagic helper wrapper.
Definition: MWFileProps.php:28
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
static getMain()
Get the RequestContext object associated with the main request.
UploadStash is intended to accomplish a few things:
Definition: UploadStash.php:57
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
Definition: UploadStash.php:59
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
Definition: UploadStash.php:74
array $fileProps
fileprops cache
Definition: UploadStash.php:77
listFiles()
List all files in the stash.
__construct(FileRepo $repo, UserIdentity $user=null)
Represents a temporary filestore, with metadata in the database.
Definition: UploadStash.php:90
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.
LocalRepo $repo
repository that this uses to store temp files public because we sometimes need to get a LocalFile wit...
Definition: UploadStash.php:68
array $files
array of initialized repo objects
Definition: UploadStash.php:71
Interface for objects representing user identity.
const DB_REPLICA
Definition: defines.php:26
const DB_PRIMARY
Definition: defines.php:28
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42