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 ( strlen( $this->fileMetadata[$key]['us_props'] ) ) {
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?
154  throw new UploadStashBadPathException(
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" );
207  throw new UploadStashBadPathException(
208  wfMessage( 'uploadstash-bad-path' )
209  );
210  }
211 
212  $mwProps = new MWFileProps( 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( (string)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 ) ) {
240  throw new UploadStashBadPathException(
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 ) {
477  $prohibitedFileExtensions = MediaWikiServices::getInstance()
478  ->getMainConfig()->get( MainConfigNames::ProhibitedFileExtensions );
479  // Does this have an extension?
480  $n = strrpos( $path, '.' );
481 
482  if ( $n !== false ) {
483  $extension = $n ? substr( $path, $n + 1 ) : '';
484  } else {
485  // If not, assume that it should be related to the MIME type of the original file.
486  $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
487  $mimeType = $magic->guessMimeType( $path );
488  $extension = $magic->getExtensionFromMimeTypeOrNull( $mimeType ) ?? '';
489  }
490 
491  $extension = File::normalizeExtension( $extension );
492  if ( in_array( $extension, $prohibitedFileExtensions ) ) {
493  // The file should already be checked for being evil.
494  // However, if somehow we got here, we definitely
495  // don't want to give it an extension of .php and
496  // put it in a web accessible directory.
497  return '';
498  }
499 
500  return $extension;
501  }
502 
510  protected function fetchFileMetadata( $key, $readFromDB = DB_REPLICA ) {
511  // populate $fileMetadata[$key]
512  $dbr = null;
513  if ( $readFromDB === DB_PRIMARY ) {
514  // sometimes reading from the primary DB is necessary, if there's replication lag.
515  $dbr = $this->repo->getPrimaryDB();
516  } else {
517  $dbr = $this->repo->getReplicaDB();
518  }
519 
520  $row = $dbr->selectRow(
521  'uploadstash',
522  [
523  'us_user', 'us_key', 'us_orig_path', 'us_path', 'us_props',
524  'us_size', 'us_sha1', 'us_mime', 'us_media_type',
525  'us_image_width', 'us_image_height', 'us_image_bits',
526  'us_source_type', 'us_timestamp', 'us_status',
527  ],
528  [ 'us_key' => $key ],
529  __METHOD__
530  );
531 
532  if ( !is_object( $row ) ) {
533  // key wasn't present in the database. this will happen sometimes.
534  return false;
535  }
536 
537  $this->fileMetadata[$key] = (array)$row;
538  $this->fileMetadata[$key]['us_props'] = $dbr->decodeBlob( $row->us_props );
539 
540  return true;
541  }
542 
550  protected function initFile( $key ) {
551  $file = new UploadStashFile( $this->repo, $this->fileMetadata[$key]['us_path'], $key );
552  if ( $file->getSize() === 0 ) {
554  wfMessage( 'uploadstash-zero-length' )
555  );
556  }
557  $this->files[$key] = $file;
558 
559  return true;
560  }
561 }
serialize()
unserialize( $serialized)
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:256
MimeMagic helper wrapper.
Definition: MWFileProps.php:28
A class containing constants representing the names of configuration variables.
MediaWikiServices is the service locator for the application scope of MediaWiki.
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
const MAX_US_PROPS_SIZE
Definition: UploadStash.php:60
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.
UserIdentity $user
Definition: UploadStash.php:80
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: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