MediaWiki  master
UploadStash.php
Go to the documentation of this file.
1 <?php
53 class UploadStash {
54  // Format of the key for files -- has to be suitable as a filename itself (e.g. ab12cd34ef.jpg)
55  const KEY_FORMAT_REGEX = '/^[\w\-\.]+\.\w*$/';
56  const MAX_US_PROPS_SIZE = 65535;
57 
64  public $repo;
65 
66  // array of initialized repo objects
67  protected $files = [];
68 
69  // cache of the file metadata that's stored in the database
70  protected $fileMetadata = [];
71 
72  // fileprops cache
73  protected $fileProps = [];
74 
75  // current user
76  protected $user, $userId, $isLoggedIn;
77 
86  public function __construct( FileRepo $repo, $user = null ) {
87  // this might change based on wiki's configuration.
88  $this->repo = $repo;
89 
90  // if a user was passed, use it. otherwise, attempt to use the global.
91  // this keeps FileRepo from breaking when it creates an UploadStash object
92  global $wgUser;
93  $this->user = $user ?: $wgUser;
94 
95  if ( is_object( $this->user ) ) {
96  $this->userId = $this->user->getId();
97  $this->isLoggedIn = $this->user->isLoggedIn();
98  }
99  }
100 
114  public function getFile( $key, $noAuth = false ) {
115  if ( !preg_match( self::KEY_FORMAT_REGEX, $key ) ) {
116  throw new UploadStashBadPathException(
117  wfMessage( 'uploadstash-bad-path-bad-format', $key )
118  );
119  }
120 
121  if ( !$noAuth && !$this->isLoggedIn ) {
123  wfMessage( 'uploadstash-not-logged-in' )
124  );
125  }
126 
127  if ( !isset( $this->fileMetadata[$key] ) ) {
128  if ( !$this->fetchFileMetadata( $key ) ) {
129  // If nothing was received, it's likely due to replication lag.
130  // Check the master to see if the record is there.
131  $this->fetchFileMetadata( $key, DB_MASTER );
132  }
133 
134  if ( !isset( $this->fileMetadata[$key] ) ) {
136  wfMessage( 'uploadstash-file-not-found', $key )
137  );
138  }
139 
140  // create $this->files[$key]
141  $this->initFile( $key );
142 
143  // fetch fileprops
144  if ( strlen( $this->fileMetadata[$key]['us_props'] ) ) {
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\n" );
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\n" );
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->userId ) {
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\n" );
209  throw new UploadStashBadPathException(
210  wfMessage( 'uploadstash-bad-path' )
211  );
212  }
213 
214  $mwProps = new MWFileProps( MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer() );
215  $fileProps = $mwProps->getPropsFromPath( $path, true );
216  wfDebug( __METHOD__ . " stashing file at '$path'\n" );
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  list( $usec, $sec ) = explode( ' ', microtime() );
233  $usec = substr( $usec, 2 );
234  $key = Wikimedia\base_convert( $sec . $usec, 10, 36 ) . '.' .
235  Wikimedia\base_convert( mt_rand(), 10, 36 ) . '.' .
236  $this->userId . '.' .
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\n" );
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->isLoggedIn ) {
280  wfMessage( 'uploadstash-not-logged-in' )
281  );
282  }
283 
284  // insert the file metadata into the db.
285  wfDebug( __METHOD__ . " inserting $stashPath under $key\n" );
286  $dbw = $this->repo->getMasterDB();
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'] = false;
294  $serializedFileProps = serialize( $fileProps );
295  }
296 
297  $this->fileMetadata[$key] = [
298  'us_user' => $this->userId,
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  $this->fileMetadata[$key],
318  __METHOD__
319  );
320 
321  // store the insertid in the class variable so immediate retrieval
322  // (possibly laggy) isn't necessary.
323  $this->fileMetadata[$key]['us_id'] = $dbw->insertId();
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->isLoggedIn ) {
341  wfMessage( 'uploadstash-not-logged-in' )
342  );
343  }
344 
345  wfDebug( __METHOD__ . ' clearing all rows for user ' . $this->userId . "\n" );
346  $dbw = $this->repo->getMasterDB();
347  $dbw->delete(
348  'uploadstash',
349  [ 'us_user' => $this->userId ],
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->isLoggedIn ) {
371  wfMessage( 'uploadstash-not-logged-in' )
372  );
373  }
374 
375  $dbw = $this->repo->getMasterDB();
376 
377  // this is a cheap query. it runs on the master 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->userId ) {
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\n" );
409 
410  // Ensure we have the UploadStashFile loaded for this key
411  $this->getFile( $key, true );
412 
413  $dbw = $this->repo->getMasterDB();
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->isLoggedIn ) {
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->userId ],
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 
477  public static function getExtensionForPath( $path ) {
478  global $wgFileBlacklist;
479  // Does this have an extension?
480  $n = strrpos( $path, '.' );
481  $extension = null;
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 = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
487  $mimeType = $magic->guessMimeType( $path );
488  $extensions = explode( ' ', $magic->getExtensionsForType( $mimeType ) );
489  if ( count( $extensions ) ) {
490  $extension = $extensions[0];
491  }
492  }
493 
494  if ( is_null( $extension ) ) {
495  throw new UploadStashFileException(
496  wfMessage( 'uploadstash-no-extension' )
497  );
498  }
499 
500  $extension = File::normalizeExtension( $extension );
501  if ( in_array( $extension, $wgFileBlacklist ) ) {
502  // The file should already be checked for being evil.
503  // However, if somehow we got here, we definitely
504  // don't want to give it an extension of .php and
505  // put it in a web accesible directory.
506  return '';
507  }
508 
509  return $extension;
510  }
511 
519  protected function fetchFileMetadata( $key, $readFromDB = DB_REPLICA ) {
520  // populate $fileMetadata[$key]
521  $dbr = null;
522  if ( $readFromDB === DB_MASTER ) {
523  // sometimes reading from the master is necessary, if there's replication lag.
524  $dbr = $this->repo->getMasterDB();
525  } else {
526  $dbr = $this->repo->getReplicaDB();
527  }
528 
529  $row = $dbr->selectRow(
530  'uploadstash',
531  [
532  'us_user', 'us_key', 'us_orig_path', 'us_path', 'us_props',
533  'us_size', 'us_sha1', 'us_mime', 'us_media_type',
534  'us_image_width', 'us_image_height', 'us_image_bits',
535  'us_source_type', 'us_timestamp', 'us_status',
536  ],
537  [ 'us_key' => $key ],
538  __METHOD__
539  );
540 
541  if ( !is_object( $row ) ) {
542  // key wasn't present in the database. this will happen sometimes.
543  return false;
544  }
545 
546  $this->fileMetadata[$key] = (array)$row;
547  $this->fileMetadata[$key]['us_props'] = $dbr->decodeBlob( $row->us_props );
548 
549  return true;
550  }
551 
559  protected function initFile( $key ) {
560  $file = new UploadStashFile( $this->repo, $this->fileMetadata[$key]['us_path'], $key );
561  if ( $file->getSize() === 0 ) {
563  wfMessage( 'uploadstash-zero-length' )
564  );
565  }
566  $this->files[$key] = $file;
567 
568  return true;
569  }
570 }
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
const KEY_FORMAT_REGEX
Definition: UploadStash.php:55
clear()
Remove all files from the stash.
serialize()
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Definition: router.php:42
getMetadata( $key)
Getter for file metadata.
listFiles()
List all files in the stash.
getFile( $key, $noAuth=false)
Get a file and its metadata from the stash.
getFileProps( $key)
Getter for fileProps.
A helper class for throttling authentication attempts.
stashFile( $path, $sourceType=null)
Stash a file in a temp directory and record that we did this in the database, along with other metada...
static getInstance()
Returns the global default instance of the top level service locator.
const DB_MASTER
Definition: defines.php:26
static getExtensionForPath( $path)
Find or guess extension – ensuring that our extension matches our MIME type.
__construct(FileRepo $repo, $user=null)
Represents a temporary filestore, with metadata in the database.
Definition: UploadStash.php:86
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt
$res
Definition: database.txt:21
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
const MAX_US_PROPS_SIZE
Definition: UploadStash.php:56
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such and we might be restricted by PHP settings such as safe mode or open_basedir We cannot assume that the software even has read access anywhere useful Many shared hosts run all users web applications under the same user
Wikitext formatted, in the key only.
Definition: distributors.txt:9
unserialize( $serialized)
removeFile( $key)
Remove a particular file from the stash.
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:767
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
UploadStash is intended to accomplish a few things:
Definition: UploadStash.php:53
initFile( $key)
Helper function: Initialize the UploadStashFile for a given file.
fetchFileMetadata( $key, $readFromDB=DB_REPLICA)
Helper function: do the actual database query to fetch file metadata.
Base class for file repositories.
Definition: FileRepo.php:39
MimeMagic helper wrapper.
Definition: MWFileProps.php:28
const DB_REPLICA
Definition: defines.php:25
static normalizeExtension( $extension)
Normalize a file extension to the common form, making it lowercase and checking some synonyms...
Definition: File.php:234
$wgFileBlacklist
Files with these extensions will never be allowed as uploads.
LocalRepo $repo
repository that this uses to store temp files public because we sometimes need to get a LocalFile wit...
Definition: UploadStash.php:64
as see the revision history and available at free of to any person obtaining a copy of this software and associated documentation files(the "Software")
removeFileNoAuth( $key)
Remove a file (see removeFile), but doesn&#39;t check ownership first.