MediaWiki  1.27.2
LocalFile.php
Go to the documentation of this file.
1 <?php
27 define( 'MW_FILE_VERSION', 9 );
28 
46 class LocalFile extends File {
47  const CACHE_FIELD_MAX_LEN = 1000;
48 
50  protected $fileExists;
51 
53  protected $width;
54 
56  protected $height;
57 
59  protected $bits;
60 
62  protected $media_type;
63 
65  protected $mime;
66 
68  protected $size;
69 
71  protected $metadata;
72 
74  protected $sha1;
75 
77  protected $dataLoaded;
78 
80  protected $extraDataLoaded;
81 
83  protected $deleted;
84 
86  protected $repoClass = 'LocalRepo';
87 
89  private $historyLine;
90 
92  private $historyRes;
93 
95  private $major_mime;
96 
98  private $minor_mime;
99 
101  private $timestamp;
102 
104  private $user;
105 
107  private $user_text;
108 
110  private $description;
111 
114 
116  private $upgraded;
117 
119  private $locked;
120 
122  private $lockedOwnTrx;
123 
125  private $missing;
126 
127  // @note: higher than IDBAccessObject constants
128  const LOAD_ALL = 16; // integer; load all the lazy fields too (like metadata)
129 
142  static function newFromTitle( $title, $repo, $unused = null ) {
143  return new self( $title, $repo );
144  }
145 
155  static function newFromRow( $row, $repo ) {
156  $title = Title::makeTitle( NS_FILE, $row->img_name );
157  $file = new self( $title, $repo );
158  $file->loadFromRow( $row );
159 
160  return $file;
161  }
162 
172  static function newFromKey( $sha1, $repo, $timestamp = false ) {
173  $dbr = $repo->getSlaveDB();
174 
175  $conds = [ 'img_sha1' => $sha1 ];
176  if ( $timestamp ) {
177  $conds['img_timestamp'] = $dbr->timestamp( $timestamp );
178  }
179 
180  $row = $dbr->selectRow( 'image', self::selectFields(), $conds, __METHOD__ );
181  if ( $row ) {
182  return self::newFromRow( $row, $repo );
183  } else {
184  return false;
185  }
186  }
187 
192  static function selectFields() {
193  return [
194  'img_name',
195  'img_size',
196  'img_width',
197  'img_height',
198  'img_metadata',
199  'img_bits',
200  'img_media_type',
201  'img_major_mime',
202  'img_minor_mime',
203  'img_description',
204  'img_user',
205  'img_user_text',
206  'img_timestamp',
207  'img_sha1',
208  ];
209  }
210 
217  function __construct( $title, $repo ) {
218  parent::__construct( $title, $repo );
219 
220  $this->metadata = '';
221  $this->historyLine = 0;
222  $this->historyRes = null;
223  $this->dataLoaded = false;
224  $this->extraDataLoaded = false;
225 
226  $this->assertRepoDefined();
227  $this->assertTitleDefined();
228  }
229 
235  function getCacheKey() {
236  $hashedName = md5( $this->getName() );
237 
238  return $this->repo->getSharedCacheKey( 'file', $hashedName );
239  }
240 
245  private function loadFromCache() {
246  $this->dataLoaded = false;
247  $this->extraDataLoaded = false;
248  $key = $this->getCacheKey();
249 
250  if ( !$key ) {
251  return false;
252  }
253 
255  $cachedValues = $cache->get( $key );
256 
257  // Check if the key existed and belongs to this version of MediaWiki
258  if ( is_array( $cachedValues ) && $cachedValues['version'] == MW_FILE_VERSION ) {
259  $this->fileExists = $cachedValues['fileExists'];
260  if ( $this->fileExists ) {
261  $this->setProps( $cachedValues );
262  }
263  $this->dataLoaded = true;
264  $this->extraDataLoaded = true;
265  foreach ( $this->getLazyCacheFields( '' ) as $field ) {
266  $this->extraDataLoaded = $this->extraDataLoaded && isset( $cachedValues[$field] );
267  }
268  }
269 
270  return $this->dataLoaded;
271  }
272 
276  private function saveToCache() {
277  $this->load();
278 
279  $key = $this->getCacheKey();
280  if ( !$key ) {
281  return;
282  }
283 
284  $fields = $this->getCacheFields( '' );
285  $cacheVal = [ 'version' => MW_FILE_VERSION ];
286  $cacheVal['fileExists'] = $this->fileExists;
287 
288  if ( $this->fileExists ) {
289  foreach ( $fields as $field ) {
290  $cacheVal[$field] = $this->$field;
291  }
292  }
293 
294  // Strip off excessive entries from the subset of fields that can become large.
295  // If the cache value gets to large it will not fit in memcached and nothing will
296  // get cached at all, causing master queries for any file access.
297  foreach ( $this->getLazyCacheFields( '' ) as $field ) {
298  if ( isset( $cacheVal[$field] ) && strlen( $cacheVal[$field] ) > 100 * 1024 ) {
299  unset( $cacheVal[$field] ); // don't let the value get too big
300  }
301  }
302 
303  // Cache presence for 1 week and negatives for 1 day
304  $ttl = $this->fileExists ? 86400 * 7 : 86400;
305  $opts = Database::getCacheSetOptions( $this->repo->getSlaveDB() );
306  ObjectCache::getMainWANInstance()->set( $key, $cacheVal, $ttl, $opts );
307  }
308 
312  public function invalidateCache() {
313  $key = $this->getCacheKey();
314  if ( !$key ) {
315  return;
316  }
317 
318  $this->repo->getMasterDB()->onTransactionPreCommitOrIdle( function() use ( $key ) {
320  } );
321  }
322 
326  function loadFromFile() {
327  $props = $this->repo->getFileProps( $this->getVirtualUrl() );
328  $this->setProps( $props );
329  }
330 
335  function getCacheFields( $prefix = 'img_' ) {
336  static $fields = [ 'size', 'width', 'height', 'bits', 'media_type',
337  'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user',
338  'user_text', 'description' ];
339  static $results = [];
340 
341  if ( $prefix == '' ) {
342  return $fields;
343  }
344 
345  if ( !isset( $results[$prefix] ) ) {
346  $prefixedFields = [];
347  foreach ( $fields as $field ) {
348  $prefixedFields[] = $prefix . $field;
349  }
350  $results[$prefix] = $prefixedFields;
351  }
352 
353  return $results[$prefix];
354  }
355 
360  function getLazyCacheFields( $prefix = 'img_' ) {
361  static $fields = [ 'metadata' ];
362  static $results = [];
363 
364  if ( $prefix == '' ) {
365  return $fields;
366  }
367 
368  if ( !isset( $results[$prefix] ) ) {
369  $prefixedFields = [];
370  foreach ( $fields as $field ) {
371  $prefixedFields[] = $prefix . $field;
372  }
373  $results[$prefix] = $prefixedFields;
374  }
375 
376  return $results[$prefix];
377  }
378 
383  function loadFromDB( $flags = 0 ) {
384  $fname = get_class( $this ) . '::' . __FUNCTION__;
385 
386  # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
387  $this->dataLoaded = true;
388  $this->extraDataLoaded = true;
389 
390  $dbr = ( $flags & self::READ_LATEST )
391  ? $this->repo->getMasterDB()
392  : $this->repo->getSlaveDB();
393 
394  $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
395  [ 'img_name' => $this->getName() ], $fname );
396 
397  if ( $row ) {
398  $this->loadFromRow( $row );
399  } else {
400  $this->fileExists = false;
401  }
402  }
403 
408  protected function loadExtraFromDB() {
409  $fname = get_class( $this ) . '::' . __FUNCTION__;
410 
411  # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
412  $this->extraDataLoaded = true;
413 
414  $fieldMap = $this->loadFieldsWithTimestamp( $this->repo->getSlaveDB(), $fname );
415  if ( !$fieldMap ) {
416  $fieldMap = $this->loadFieldsWithTimestamp( $this->repo->getMasterDB(), $fname );
417  }
418 
419  if ( $fieldMap ) {
420  foreach ( $fieldMap as $name => $value ) {
421  $this->$name = $value;
422  }
423  } else {
424  throw new MWException( "Could not find data for image '{$this->getName()}'." );
425  }
426  }
427 
433  private function loadFieldsWithTimestamp( $dbr, $fname ) {
434  $fieldMap = false;
435 
436  $row = $dbr->selectRow( 'image', $this->getLazyCacheFields( 'img_' ),
437  [ 'img_name' => $this->getName(), 'img_timestamp' => $this->getTimestamp() ],
438  $fname );
439  if ( $row ) {
440  $fieldMap = $this->unprefixRow( $row, 'img_' );
441  } else {
442  # File may have been uploaded over in the meantime; check the old versions
443  $row = $dbr->selectRow( 'oldimage', $this->getLazyCacheFields( 'oi_' ),
444  [ 'oi_name' => $this->getName(), 'oi_timestamp' => $this->getTimestamp() ],
445  $fname );
446  if ( $row ) {
447  $fieldMap = $this->unprefixRow( $row, 'oi_' );
448  }
449  }
450 
451  return $fieldMap;
452  }
453 
460  protected function unprefixRow( $row, $prefix = 'img_' ) {
461  $array = (array)$row;
462  $prefixLength = strlen( $prefix );
463 
464  // Sanity check prefix once
465  if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
466  throw new MWException( __METHOD__ . ': incorrect $prefix parameter' );
467  }
468 
469  $decoded = [];
470  foreach ( $array as $name => $value ) {
471  $decoded[substr( $name, $prefixLength )] = $value;
472  }
473 
474  return $decoded;
475  }
476 
485  function decodeRow( $row, $prefix = 'img_' ) {
486  $decoded = $this->unprefixRow( $row, $prefix );
487 
488  $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
489 
490  $decoded['metadata'] = $this->repo->getSlaveDB()->decodeBlob( $decoded['metadata'] );
491 
492  if ( empty( $decoded['major_mime'] ) ) {
493  $decoded['mime'] = 'unknown/unknown';
494  } else {
495  if ( !$decoded['minor_mime'] ) {
496  $decoded['minor_mime'] = 'unknown';
497  }
498  $decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime'];
499  }
500 
501  // Trim zero padding from char/binary field
502  $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
503 
504  // Normalize some fields to integer type, per their database definition.
505  // Use unary + so that overflows will be upgraded to double instead of
506  // being trucated as with intval(). This is important to allow >2GB
507  // files on 32-bit systems.
508  foreach ( [ 'size', 'width', 'height', 'bits' ] as $field ) {
509  $decoded[$field] = +$decoded[$field];
510  }
511 
512  return $decoded;
513  }
514 
521  function loadFromRow( $row, $prefix = 'img_' ) {
522  $this->dataLoaded = true;
523  $this->extraDataLoaded = true;
524 
525  $array = $this->decodeRow( $row, $prefix );
526 
527  foreach ( $array as $name => $value ) {
528  $this->$name = $value;
529  }
530 
531  $this->fileExists = true;
532  $this->maybeUpgradeRow();
533  }
534 
539  function load( $flags = 0 ) {
540  if ( !$this->dataLoaded ) {
541  if ( ( $flags & self::READ_LATEST ) || !$this->loadFromCache() ) {
542  $this->loadFromDB( $flags );
543  $this->saveToCache();
544  }
545  $this->dataLoaded = true;
546  }
547  if ( ( $flags & self::LOAD_ALL ) && !$this->extraDataLoaded ) {
548  // @note: loads on name/timestamp to reduce race condition problems
549  $this->loadExtraFromDB();
550  }
551  }
552 
556  function maybeUpgradeRow() {
558  if ( wfReadOnly() ) {
559  return;
560  }
561 
562  $upgrade = false;
563  if ( is_null( $this->media_type ) ||
564  $this->mime == 'image/svg'
565  ) {
566  $upgrade = true;
567  } else {
568  $handler = $this->getHandler();
569  if ( $handler ) {
570  $validity = $handler->isMetadataValid( $this, $this->getMetadata() );
571  if ( $validity === MediaHandler::METADATA_BAD
572  || ( $validity === MediaHandler::METADATA_COMPATIBLE && $wgUpdateCompatibleMetadata )
573  ) {
574  $upgrade = true;
575  }
576  }
577  }
578 
579  if ( $upgrade ) {
580  try {
581  $this->upgradeRow();
582  } catch ( LocalFileLockError $e ) {
583  // let the other process handle it (or do it next time)
584  }
585  $this->upgraded = true; // avoid rework/retries
586  }
587  }
588 
589  function getUpgraded() {
590  return $this->upgraded;
591  }
592 
596  function upgradeRow() {
597  $this->lock(); // begin
598 
599  $this->loadFromFile();
600 
601  # Don't destroy file info of missing files
602  if ( !$this->fileExists ) {
603  $this->unlock();
604  wfDebug( __METHOD__ . ": file does not exist, aborting\n" );
605 
606  return;
607  }
608 
609  $dbw = $this->repo->getMasterDB();
610  list( $major, $minor ) = self::splitMime( $this->mime );
611 
612  if ( wfReadOnly() ) {
613  $this->unlock();
614 
615  return;
616  }
617  wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema\n" );
618 
619  $dbw->update( 'image',
620  [
621  'img_size' => $this->size, // sanity
622  'img_width' => $this->width,
623  'img_height' => $this->height,
624  'img_bits' => $this->bits,
625  'img_media_type' => $this->media_type,
626  'img_major_mime' => $major,
627  'img_minor_mime' => $minor,
628  'img_metadata' => $dbw->encodeBlob( $this->metadata ),
629  'img_sha1' => $this->sha1,
630  ],
631  [ 'img_name' => $this->getName() ],
632  __METHOD__
633  );
634 
635  $this->invalidateCache();
636 
637  $this->unlock(); // done
638 
639  }
640 
651  function setProps( $info ) {
652  $this->dataLoaded = true;
653  $fields = $this->getCacheFields( '' );
654  $fields[] = 'fileExists';
655 
656  foreach ( $fields as $field ) {
657  if ( isset( $info[$field] ) ) {
658  $this->$field = $info[$field];
659  }
660  }
661 
662  // Fix up mime fields
663  if ( isset( $info['major_mime'] ) ) {
664  $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
665  } elseif ( isset( $info['mime'] ) ) {
666  $this->mime = $info['mime'];
667  list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
668  }
669  }
670 
682  function isMissing() {
683  if ( $this->missing === null ) {
684  list( $fileExists ) = $this->repo->fileExists( $this->getVirtualUrl() );
685  $this->missing = !$fileExists;
686  }
687 
688  return $this->missing;
689  }
690 
697  public function getWidth( $page = 1 ) {
698  $this->load();
699 
700  if ( $this->isMultipage() ) {
701  $handler = $this->getHandler();
702  if ( !$handler ) {
703  return 0;
704  }
705  $dim = $handler->getPageDimensions( $this, $page );
706  if ( $dim ) {
707  return $dim['width'];
708  } else {
709  // For non-paged media, the false goes through an
710  // intval, turning failure into 0, so do same here.
711  return 0;
712  }
713  } else {
714  return $this->width;
715  }
716  }
717 
724  public function getHeight( $page = 1 ) {
725  $this->load();
726 
727  if ( $this->isMultipage() ) {
728  $handler = $this->getHandler();
729  if ( !$handler ) {
730  return 0;
731  }
732  $dim = $handler->getPageDimensions( $this, $page );
733  if ( $dim ) {
734  return $dim['height'];
735  } else {
736  // For non-paged media, the false goes through an
737  // intval, turning failure into 0, so do same here.
738  return 0;
739  }
740  } else {
741  return $this->height;
742  }
743  }
744 
751  function getUser( $type = 'text' ) {
752  $this->load();
753 
754  if ( $type == 'text' ) {
755  return $this->user_text;
756  } elseif ( $type == 'id' ) {
757  return (int)$this->user;
758  }
759  }
760 
768  public function getDescriptionShortUrl() {
769  $pageId = $this->title->getArticleID();
770 
771  if ( $pageId !== null ) {
772  $url = $this->repo->makeUrl( [ 'curid' => $pageId ] );
773  if ( $url !== false ) {
774  return $url;
775  }
776  }
777  return null;
778  }
779 
784  function getMetadata() {
785  $this->load( self::LOAD_ALL ); // large metadata is loaded in another step
786  return $this->metadata;
787  }
788 
792  function getBitDepth() {
793  $this->load();
794 
795  return (int)$this->bits;
796  }
797 
802  public function getSize() {
803  $this->load();
804 
805  return $this->size;
806  }
807 
812  function getMimeType() {
813  $this->load();
814 
815  return $this->mime;
816  }
817 
823  function getMediaType() {
824  $this->load();
825 
826  return $this->media_type;
827  }
828 
839  public function exists() {
840  $this->load();
841 
842  return $this->fileExists;
843  }
844 
860  function getThumbnails( $archiveName = false ) {
861  if ( $archiveName ) {
862  $dir = $this->getArchiveThumbPath( $archiveName );
863  } else {
864  $dir = $this->getThumbPath();
865  }
866 
867  $backend = $this->repo->getBackend();
868  $files = [ $dir ];
869  try {
870  $iterator = $backend->getFileList( [ 'dir' => $dir ] );
871  foreach ( $iterator as $file ) {
872  $files[] = $file;
873  }
874  } catch ( FileBackendError $e ) {
875  } // suppress (bug 54674)
876 
877  return $files;
878  }
879 
883  function purgeMetadataCache() {
884  $this->invalidateCache();
885  }
886 
894  function purgeCache( $options = [] ) {
895  // Refresh metadata cache
896  $this->purgeMetadataCache();
897 
898  // Delete thumbnails
899  $this->purgeThumbnails( $options );
900 
901  // Purge CDN cache for this file
903  new CdnCacheUpdate( [ $this->getUrl() ] ),
905  );
906  }
907 
912  function purgeOldThumbnails( $archiveName ) {
913  // Get a list of old thumbnails and URLs
914  $files = $this->getThumbnails( $archiveName );
915 
916  // Purge any custom thumbnail caches
917  Hooks::run( 'LocalFilePurgeThumbnails', [ $this, $archiveName ] );
918 
919  // Delete thumbnails
920  $dir = array_shift( $files );
921  $this->purgeThumbList( $dir, $files );
922 
923  // Purge the CDN
924  $urls = [];
925  foreach ( $files as $file ) {
926  $urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
927  }
929  }
930 
935  public function purgeThumbnails( $options = [] ) {
936  $files = $this->getThumbnails();
937  // Always purge all files from CDN regardless of handler filters
938  $urls = [];
939  foreach ( $files as $file ) {
940  $urls[] = $this->getThumbUrl( $file );
941  }
942  array_shift( $urls ); // don't purge directory
943 
944  // Give media handler a chance to filter the file purge list
945  if ( !empty( $options['forThumbRefresh'] ) ) {
946  $handler = $this->getHandler();
947  if ( $handler ) {
949  }
950  }
951 
952  // Purge any custom thumbnail caches
953  Hooks::run( 'LocalFilePurgeThumbnails', [ $this, false ] );
954 
955  // Delete thumbnails
956  $dir = array_shift( $files );
957  $this->purgeThumbList( $dir, $files );
958 
959  // Purge the CDN
961  }
962 
968  protected function purgeThumbList( $dir, $files ) {
969  $fileListDebug = strtr(
970  var_export( $files, true ),
971  [ "\n" => '' ]
972  );
973  wfDebug( __METHOD__ . ": $fileListDebug\n" );
974 
975  $purgeList = [];
976  foreach ( $files as $file ) {
977  # Check that the base file name is part of the thumb name
978  # This is a basic sanity check to avoid erasing unrelated directories
979  if ( strpos( $file, $this->getName() ) !== false
980  || strpos( $file, "-thumbnail" ) !== false // "short" thumb name
981  ) {
982  $purgeList[] = "{$dir}/{$file}";
983  }
984  }
985 
986  # Delete the thumbnails
987  $this->repo->quickPurgeBatch( $purgeList );
988  # Clear out the thumbnail directory if empty
989  $this->repo->quickCleanDir( $dir );
990  }
991 
1002  function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
1003  $dbr = $this->repo->getSlaveDB();
1004  $tables = [ 'oldimage' ];
1005  $fields = OldLocalFile::selectFields();
1006  $conds = $opts = $join_conds = [];
1007  $eq = $inc ? '=' : '';
1008  $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
1009 
1010  if ( $start ) {
1011  $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
1012  }
1013 
1014  if ( $end ) {
1015  $conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
1016  }
1017 
1018  if ( $limit ) {
1019  $opts['LIMIT'] = $limit;
1020  }
1021 
1022  // Search backwards for time > x queries
1023  $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
1024  $opts['ORDER BY'] = "oi_timestamp $order";
1025  $opts['USE INDEX'] = [ 'oldimage' => 'oi_name_timestamp' ];
1026 
1027  Hooks::run( 'LocalFile::getHistory', [ &$this, &$tables, &$fields,
1028  &$conds, &$opts, &$join_conds ] );
1029 
1030  $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
1031  $r = [];
1032 
1033  foreach ( $res as $row ) {
1034  $r[] = $this->repo->newFileFromRow( $row );
1035  }
1036 
1037  if ( $order == 'ASC' ) {
1038  $r = array_reverse( $r ); // make sure it ends up descending
1039  }
1040 
1041  return $r;
1042  }
1043 
1053  public function nextHistoryLine() {
1054  # Polymorphic function name to distinguish foreign and local fetches
1055  $fname = get_class( $this ) . '::' . __FUNCTION__;
1056 
1057  $dbr = $this->repo->getSlaveDB();
1058 
1059  if ( $this->historyLine == 0 ) { // called for the first time, return line from cur
1060  $this->historyRes = $dbr->select( 'image',
1061  [
1062  '*',
1063  "'' AS oi_archive_name",
1064  '0 as oi_deleted',
1065  'img_sha1'
1066  ],
1067  [ 'img_name' => $this->title->getDBkey() ],
1068  $fname
1069  );
1070 
1071  if ( 0 == $dbr->numRows( $this->historyRes ) ) {
1072  $this->historyRes = null;
1073 
1074  return false;
1075  }
1076  } elseif ( $this->historyLine == 1 ) {
1077  $this->historyRes = $dbr->select( 'oldimage', '*',
1078  [ 'oi_name' => $this->title->getDBkey() ],
1079  $fname,
1080  [ 'ORDER BY' => 'oi_timestamp DESC' ]
1081  );
1082  }
1083  $this->historyLine++;
1084 
1085  return $dbr->fetchObject( $this->historyRes );
1086  }
1087 
1091  public function resetHistory() {
1092  $this->historyLine = 0;
1093 
1094  if ( !is_null( $this->historyRes ) ) {
1095  $this->historyRes = null;
1096  }
1097  }
1098 
1129  function upload( $src, $comment, $pageText, $flags = 0, $props = false,
1130  $timestamp = false, $user = null, $tags = []
1131  ) {
1133 
1134  if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1135  return $this->readOnlyFatalStatus();
1136  }
1137 
1138  $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
1139  if ( !$props ) {
1140  if ( $this->repo->isVirtualUrl( $srcPath )
1141  || FileBackend::isStoragePath( $srcPath )
1142  ) {
1143  $props = $this->repo->getFileProps( $srcPath );
1144  } else {
1145  $props = FSFile::getPropsFromPath( $srcPath );
1146  }
1147  }
1148 
1149  $options = [];
1150  $handler = MediaHandler::getHandler( $props['mime'] );
1151  if ( $handler ) {
1152  $options['headers'] = $handler->getStreamHeaders( $props['metadata'] );
1153  } else {
1154  $options['headers'] = [];
1155  }
1156 
1157  // Trim spaces on user supplied text
1158  $comment = trim( $comment );
1159 
1160  // Truncate nicely or the DB will do it for us
1161  // non-nicely (dangling multi-byte chars, non-truncated version in cache).
1162  $comment = $wgContLang->truncate( $comment, 255 );
1163  $this->lock(); // begin
1164  $status = $this->publish( $src, $flags, $options );
1165 
1166  if ( $status->successCount >= 2 ) {
1167  // There will be a copy+(one of move,copy,store).
1168  // The first succeeding does not commit us to updating the DB
1169  // since it simply copied the current version to a timestamped file name.
1170  // It is only *preferable* to avoid leaving such files orphaned.
1171  // Once the second operation goes through, then the current version was
1172  // updated and we must therefore update the DB too.
1173  $oldver = $status->value;
1174  if ( !$this->recordUpload2( $oldver, $comment, $pageText, $props, $timestamp, $user, $tags ) ) {
1175  $status->fatal( 'filenotfound', $srcPath );
1176  }
1177  }
1178 
1179  $this->unlock(); // done
1180 
1181  return $status;
1182  }
1183 
1196  function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
1197  $watch = false, $timestamp = false, User $user = null ) {
1198  if ( !$user ) {
1199  global $wgUser;
1200  $user = $wgUser;
1201  }
1202 
1203  $pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
1204 
1205  if ( !$this->recordUpload2( $oldver, $desc, $pageText, false, $timestamp, $user ) ) {
1206  return false;
1207  }
1208 
1209  if ( $watch ) {
1210  $user->addWatch( $this->getTitle() );
1211  }
1212 
1213  return true;
1214  }
1215 
1227  function recordUpload2(
1228  $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null, $tags = []
1229  ) {
1230  if ( is_null( $user ) ) {
1231  global $wgUser;
1232  $user = $wgUser;
1233  }
1234 
1235  $dbw = $this->repo->getMasterDB();
1236 
1237  # Imports or such might force a certain timestamp; otherwise we generate
1238  # it and can fudge it slightly to keep (name,timestamp) unique on re-upload.
1239  if ( $timestamp === false ) {
1240  $timestamp = $dbw->timestamp();
1241  $allowTimeKludge = true;
1242  } else {
1243  $allowTimeKludge = false;
1244  }
1245 
1246  $props = $props ?: $this->repo->getFileProps( $this->getVirtualUrl() );
1247  $props['description'] = $comment;
1248  $props['user'] = $user->getId();
1249  $props['user_text'] = $user->getName();
1250  $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
1251  $this->setProps( $props );
1252 
1253  # Fail now if the file isn't there
1254  if ( !$this->fileExists ) {
1255  wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
1256 
1257  return false;
1258  }
1259 
1260  $dbw->startAtomic( __METHOD__ );
1261 
1262  # Test to see if the row exists using INSERT IGNORE
1263  # This avoids race conditions by locking the row until the commit, and also
1264  # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
1265  $dbw->insert( 'image',
1266  [
1267  'img_name' => $this->getName(),
1268  'img_size' => $this->size,
1269  'img_width' => intval( $this->width ),
1270  'img_height' => intval( $this->height ),
1271  'img_bits' => $this->bits,
1272  'img_media_type' => $this->media_type,
1273  'img_major_mime' => $this->major_mime,
1274  'img_minor_mime' => $this->minor_mime,
1275  'img_timestamp' => $timestamp,
1276  'img_description' => $comment,
1277  'img_user' => $user->getId(),
1278  'img_user_text' => $user->getName(),
1279  'img_metadata' => $dbw->encodeBlob( $this->metadata ),
1280  'img_sha1' => $this->sha1
1281  ],
1282  __METHOD__,
1283  'IGNORE'
1284  );
1285 
1286  $reupload = ( $dbw->affectedRows() == 0 );
1287  if ( $reupload ) {
1288  if ( $allowTimeKludge ) {
1289  # Use LOCK IN SHARE MODE to ignore any transaction snapshotting
1290  $ltimestamp = $dbw->selectField(
1291  'image',
1292  'img_timestamp',
1293  [ 'img_name' => $this->getName() ],
1294  __METHOD__,
1295  [ 'LOCK IN SHARE MODE' ]
1296  );
1297  $lUnixtime = $ltimestamp ? wfTimestamp( TS_UNIX, $ltimestamp ) : false;
1298  # Avoid a timestamp that is not newer than the last version
1299  # TODO: the image/oldimage tables should be like page/revision with an ID field
1300  if ( $lUnixtime && wfTimestamp( TS_UNIX, $timestamp ) <= $lUnixtime ) {
1301  sleep( 1 ); // fast enough re-uploads would go far in the future otherwise
1302  $timestamp = $dbw->timestamp( $lUnixtime + 1 );
1303  $this->timestamp = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
1304  }
1305  }
1306 
1307  # (bug 34993) Note: $oldver can be empty here, if the previous
1308  # version of the file was broken. Allow registration of the new
1309  # version to continue anyway, because that's better than having
1310  # an image that's not fixable by user operations.
1311  # Collision, this is an update of a file
1312  # Insert previous contents into oldimage
1313  $dbw->insertSelect( 'oldimage', 'image',
1314  [
1315  'oi_name' => 'img_name',
1316  'oi_archive_name' => $dbw->addQuotes( $oldver ),
1317  'oi_size' => 'img_size',
1318  'oi_width' => 'img_width',
1319  'oi_height' => 'img_height',
1320  'oi_bits' => 'img_bits',
1321  'oi_timestamp' => 'img_timestamp',
1322  'oi_description' => 'img_description',
1323  'oi_user' => 'img_user',
1324  'oi_user_text' => 'img_user_text',
1325  'oi_metadata' => 'img_metadata',
1326  'oi_media_type' => 'img_media_type',
1327  'oi_major_mime' => 'img_major_mime',
1328  'oi_minor_mime' => 'img_minor_mime',
1329  'oi_sha1' => 'img_sha1'
1330  ],
1331  [ 'img_name' => $this->getName() ],
1332  __METHOD__
1333  );
1334 
1335  # Update the current image row
1336  $dbw->update( 'image',
1337  [
1338  'img_size' => $this->size,
1339  'img_width' => intval( $this->width ),
1340  'img_height' => intval( $this->height ),
1341  'img_bits' => $this->bits,
1342  'img_media_type' => $this->media_type,
1343  'img_major_mime' => $this->major_mime,
1344  'img_minor_mime' => $this->minor_mime,
1345  'img_timestamp' => $timestamp,
1346  'img_description' => $comment,
1347  'img_user' => $user->getId(),
1348  'img_user_text' => $user->getName(),
1349  'img_metadata' => $dbw->encodeBlob( $this->metadata ),
1350  'img_sha1' => $this->sha1
1351  ],
1352  [ 'img_name' => $this->getName() ],
1353  __METHOD__
1354  );
1355  }
1356 
1357  $descTitle = $this->getTitle();
1358  $descId = $descTitle->getArticleID();
1359  $wikiPage = new WikiFilePage( $descTitle );
1360  $wikiPage->setFile( $this );
1361 
1362  // Add the log entry...
1363  $logEntry = new ManualLogEntry( 'upload', $reupload ? 'overwrite' : 'upload' );
1364  $logEntry->setTimestamp( $this->timestamp );
1365  $logEntry->setPerformer( $user );
1366  $logEntry->setComment( $comment );
1367  $logEntry->setTarget( $descTitle );
1368  // Allow people using the api to associate log entries with the upload.
1369  // Log has a timestamp, but sometimes different from upload timestamp.
1370  $logEntry->setParameters(
1371  [
1372  'img_sha1' => $this->sha1,
1373  'img_timestamp' => $timestamp,
1374  ]
1375  );
1376  // Note we keep $logId around since during new image
1377  // creation, page doesn't exist yet, so log_page = 0
1378  // but we want it to point to the page we're making,
1379  // so we later modify the log entry.
1380  // For a similar reason, we avoid making an RC entry
1381  // now and wait until the page exists.
1382  $logId = $logEntry->insert();
1383 
1384  if ( $descTitle->exists() ) {
1385  // Use own context to get the action text in content language
1386  $formatter = LogFormatter::newFromEntry( $logEntry );
1387  $formatter->setContext( RequestContext::newExtraneousContext( $descTitle ) );
1388  $editSummary = $formatter->getPlainActionText();
1389 
1390  $nullRevision = Revision::newNullRevision(
1391  $dbw,
1392  $descId,
1393  $editSummary,
1394  false,
1395  $user
1396  );
1397  if ( $nullRevision ) {
1398  $nullRevision->insertOn( $dbw );
1399  Hooks::run(
1400  'NewRevisionFromEditComplete',
1401  [ $wikiPage, $nullRevision, $nullRevision->getParentId(), $user ]
1402  );
1403  $wikiPage->updateRevisionOn( $dbw, $nullRevision );
1404  // Associate null revision id
1405  $logEntry->setAssociatedRevId( $nullRevision->getId() );
1406  }
1407 
1408  $newPageContent = null;
1409  } else {
1410  // Make the description page and RC log entry post-commit
1411  $newPageContent = ContentHandler::makeContent( $pageText, $descTitle );
1412  }
1413 
1414  # Defer purges, page creation, and link updates in case they error out.
1415  # The most important thing is that files and the DB registry stay synced.
1416  $dbw->endAtomic( __METHOD__ );
1417 
1418  # Do some cache purges after final commit so that:
1419  # a) Changes are more likely to be seen post-purge
1420  # b) They won't cause rollback of the log publish/update above
1421  $that = $this;
1422  $dbw->onTransactionIdle( function () use (
1423  $that, $reupload, $wikiPage, $newPageContent, $comment, $user, $logEntry, $logId, $descId, $tags
1424  ) {
1425  # Update memcache after the commit
1426  $that->invalidateCache();
1427 
1428  $updateLogPage = false;
1429  if ( $newPageContent ) {
1430  # New file page; create the description page.
1431  # There's already a log entry, so don't make a second RC entry
1432  # CDN and file cache for the description page are purged by doEditContent.
1433  $status = $wikiPage->doEditContent(
1434  $newPageContent,
1435  $comment,
1437  false,
1438  $user
1439  );
1440 
1441  if ( isset( $status->value['revision'] ) ) {
1442  // Associate new page revision id
1443  $logEntry->setAssociatedRevId( $status->value['revision']->getId() );
1444  }
1445  // This relies on the resetArticleID() call in WikiPage::insertOn(),
1446  // which is triggered on $descTitle by doEditContent() above.
1447  if ( isset( $status->value['revision'] ) ) {
1449  $rev = $status->value['revision'];
1450  $updateLogPage = $rev->getPage();
1451  }
1452  } else {
1453  # Existing file page: invalidate description page cache
1454  $wikiPage->getTitle()->invalidateCache();
1455  $wikiPage->getTitle()->purgeSquid();
1456  # Allow the new file version to be patrolled from the page footer
1458  }
1459 
1460  # Update associated rev id. This should be done by $logEntry->insert() earlier,
1461  # but setAssociatedRevId() wasn't called at that point yet...
1462  $logParams = $logEntry->getParameters();
1463  $logParams['associated_rev_id'] = $logEntry->getAssociatedRevId();
1464  $update = [ 'log_params' => LogEntryBase::makeParamBlob( $logParams ) ];
1465  if ( $updateLogPage ) {
1466  # Also log page, in case where we just created it above
1467  $update['log_page'] = $updateLogPage;
1468  }
1469  $that->getRepo()->getMasterDB()->update(
1470  'logging',
1471  $update,
1472  [ 'log_id' => $logId ],
1473  __METHOD__
1474  );
1475  $that->getRepo()->getMasterDB()->insert(
1476  'log_search',
1477  [
1478  'ls_field' => 'associated_rev_id',
1479  'ls_value' => $logEntry->getAssociatedRevId(),
1480  'ls_log_id' => $logId,
1481  ],
1482  __METHOD__
1483  );
1484 
1485  # Add change tags, if any
1486  if ( $tags ) {
1487  $logEntry->setTags( $tags );
1488  }
1489 
1490  # Uploads can be patrolled
1491  $logEntry->setIsPatrollable( true );
1492 
1493  # Now that the log entry is up-to-date, make an RC entry.
1494  $logEntry->publish( $logId );
1495 
1496  # Run hook for other updates (typically more cache purging)
1497  Hooks::run( 'FileUpload', [ $that, $reupload, !$newPageContent ] );
1498 
1499  if ( $reupload ) {
1500  # Delete old thumbnails
1501  $that->purgeThumbnails();
1502  # Remove the old file from the CDN cache
1504  new CdnCacheUpdate( [ $that->getUrl() ] ),
1506  );
1507  } else {
1508  # Update backlink pages pointing to this title if created
1509  LinksUpdate::queueRecursiveJobsForTable( $that->getTitle(), 'imagelinks' );
1510  }
1511  } );
1512 
1513  if ( !$reupload ) {
1514  # This is a new file, so update the image count
1515  DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => 1 ] ) );
1516  }
1517 
1518  # Invalidate cache for all pages using this file
1519  DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' ) );
1520 
1521  return true;
1522  }
1523 
1539  function publish( $src, $flags = 0, array $options = [] ) {
1540  return $this->publishTo( $src, $this->getRel(), $flags, $options );
1541  }
1542 
1558  function publishTo( $src, $dstRel, $flags = 0, array $options = [] ) {
1559  $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
1560 
1561  $repo = $this->getRepo();
1562  if ( $repo->getReadOnlyReason() !== false ) {
1563  return $this->readOnlyFatalStatus();
1564  }
1565 
1566  $this->lock(); // begin
1567 
1568  $archiveName = wfTimestamp( TS_MW ) . '!' . $this->getName();
1569  $archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
1570 
1571  if ( $repo->hasSha1Storage() ) {
1572  $sha1 = $repo->isVirtualUrl( $srcPath )
1573  ? $repo->getFileSha1( $srcPath )
1574  : FSFile::getSha1Base36FromPath( $srcPath );
1575  $dst = $repo->getBackend()->getPathForSHA1( $sha1 );
1576  $status = $repo->quickImport( $src, $dst );
1577  if ( $flags & File::DELETE_SOURCE ) {
1578  unlink( $srcPath );
1579  }
1580 
1581  if ( $this->exists() ) {
1582  $status->value = $archiveName;
1583  }
1584  } else {
1586  $status = $repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
1587 
1588  if ( $status->value == 'new' ) {
1589  $status->value = '';
1590  } else {
1591  $status->value = $archiveName;
1592  }
1593  }
1594 
1595  $this->unlock(); // done
1596 
1597  return $status;
1598  }
1599 
1617  function move( $target ) {
1618  if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1619  return $this->readOnlyFatalStatus();
1620  }
1621 
1622  wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
1623  $batch = new LocalFileMoveBatch( $this, $target );
1624 
1625  $this->lock(); // begin
1626  $batch->addCurrent();
1627  $archiveNames = $batch->addOlds();
1628  $status = $batch->execute();
1629  $this->unlock(); // done
1630 
1631  wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
1632 
1633  // Purge the source and target files...
1634  $oldTitleFile = wfLocalFile( $this->title );
1635  $newTitleFile = wfLocalFile( $target );
1636  // Hack: the lock()/unlock() pair is nested in a transaction so the locking is not
1637  // tied to BEGIN/COMMIT. To avoid slow purges in the transaction, move them outside.
1638  $this->getRepo()->getMasterDB()->onTransactionIdle(
1639  function () use ( $oldTitleFile, $newTitleFile, $archiveNames ) {
1640  $oldTitleFile->purgeEverything();
1641  foreach ( $archiveNames as $archiveName ) {
1642  $oldTitleFile->purgeOldThumbnails( $archiveName );
1643  }
1644  $newTitleFile->purgeEverything();
1645  }
1646  );
1647 
1648  if ( $status->isOK() ) {
1649  // Now switch the object
1650  $this->title = $target;
1651  // Force regeneration of the name and hashpath
1652  unset( $this->name );
1653  unset( $this->hashPath );
1654  }
1655 
1656  return $status;
1657  }
1658 
1672  function delete( $reason, $suppress = false, $user = null ) {
1673  if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1674  return $this->readOnlyFatalStatus();
1675  }
1676 
1677  $batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
1678 
1679  $this->lock(); // begin
1680  $batch->addCurrent();
1681  # Get old version relative paths
1682  $archiveNames = $batch->addOlds();
1683  $status = $batch->execute();
1684  $this->unlock(); // done
1685 
1686  if ( $status->isOK() ) {
1687  DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => -1 ] ) );
1688  }
1689 
1690  // Hack: the lock()/unlock() pair is nested in a transaction so the locking is not
1691  // tied to BEGIN/COMMIT. To avoid slow purges in the transaction, move them outside.
1692  $that = $this;
1693  $this->getRepo()->getMasterDB()->onTransactionIdle(
1694  function () use ( $that, $archiveNames ) {
1695  $that->purgeEverything();
1696  foreach ( $archiveNames as $archiveName ) {
1697  $that->purgeOldThumbnails( $archiveName );
1698  }
1699  }
1700  );
1701 
1702  // Purge the CDN
1703  $purgeUrls = [];
1704  foreach ( $archiveNames as $archiveName ) {
1705  $purgeUrls[] = $this->getArchiveUrl( $archiveName );
1706  }
1708 
1709  return $status;
1710  }
1711 
1727  function deleteOld( $archiveName, $reason, $suppress = false, $user = null ) {
1728  if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1729  return $this->readOnlyFatalStatus();
1730  }
1731 
1732  $batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
1733 
1734  $this->lock(); // begin
1735  $batch->addOld( $archiveName );
1736  $status = $batch->execute();
1737  $this->unlock(); // done
1738 
1739  $this->purgeOldThumbnails( $archiveName );
1740  if ( $status->isOK() ) {
1741  $this->purgeDescription();
1742  }
1743 
1745  new CdnCacheUpdate( [ $this->getArchiveUrl( $archiveName ) ] ),
1747  );
1748 
1749  return $status;
1750  }
1751 
1763  function restore( $versions = [], $unsuppress = false ) {
1764  if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1765  return $this->readOnlyFatalStatus();
1766  }
1767 
1768  $batch = new LocalFileRestoreBatch( $this, $unsuppress );
1769 
1770  $this->lock(); // begin
1771  if ( !$versions ) {
1772  $batch->addAll();
1773  } else {
1774  $batch->addIds( $versions );
1775  }
1776  $status = $batch->execute();
1777  if ( $status->isGood() ) {
1778  $cleanupStatus = $batch->cleanup();
1779  $cleanupStatus->successCount = 0;
1780  $cleanupStatus->failCount = 0;
1781  $status->merge( $cleanupStatus );
1782  }
1783  $this->unlock(); // done
1784 
1785  return $status;
1786  }
1787 
1797  function getDescriptionUrl() {
1798  return $this->title->getLocalURL();
1799  }
1800 
1809  function getDescriptionText( $lang = null ) {
1810  $revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
1811  if ( !$revision ) {
1812  return false;
1813  }
1814  $content = $revision->getContent();
1815  if ( !$content ) {
1816  return false;
1817  }
1818  $pout = $content->getParserOutput( $this->title, null, new ParserOptions( null, $lang ) );
1819 
1820  return $pout->getText();
1821  }
1822 
1828  function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) {
1829  $this->load();
1830  if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
1831  return '';
1832  } elseif ( $audience == self::FOR_THIS_USER
1833  && !$this->userCan( self::DELETED_COMMENT, $user )
1834  ) {
1835  return '';
1836  } else {
1837  return $this->description;
1838  }
1839  }
1840 
1844  function getTimestamp() {
1845  $this->load();
1846 
1847  return $this->timestamp;
1848  }
1849 
1853  public function getDescriptionTouched() {
1854  // The DB lookup might return false, e.g. if the file was just deleted, or the shared DB repo
1855  // itself gets it from elsewhere. To avoid repeating the DB lookups in such a case, we
1856  // need to differentiate between null (uninitialized) and false (failed to load).
1857  if ( $this->descriptionTouched === null ) {
1858  $cond = [
1859  'page_namespace' => $this->title->getNamespace(),
1860  'page_title' => $this->title->getDBkey()
1861  ];
1862  $touched = $this->repo->getSlaveDB()->selectField( 'page', 'page_touched', $cond, __METHOD__ );
1863  $this->descriptionTouched = $touched ? wfTimestamp( TS_MW, $touched ) : false;
1864  }
1865 
1867  }
1868 
1872  function getSha1() {
1873  $this->load();
1874  // Initialise now if necessary
1875  if ( $this->sha1 == '' && $this->fileExists ) {
1876  $this->lock(); // begin
1877 
1878  $this->sha1 = $this->repo->getFileSha1( $this->getPath() );
1879  if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
1880  $dbw = $this->repo->getMasterDB();
1881  $dbw->update( 'image',
1882  [ 'img_sha1' => $this->sha1 ],
1883  [ 'img_name' => $this->getName() ],
1884  __METHOD__ );
1885  $this->invalidateCache();
1886  }
1887 
1888  $this->unlock(); // done
1889  }
1890 
1891  return $this->sha1;
1892  }
1893 
1897  function isCacheable() {
1898  $this->load();
1899 
1900  // If extra data (metadata) was not loaded then it must have been large
1901  return $this->extraDataLoaded
1902  && strlen( serialize( $this->metadata ) ) <= self::CACHE_FIELD_MAX_LEN;
1903  }
1904 
1911  function lock() {
1912  if ( !$this->locked ) {
1913  $dbw = $this->repo->getMasterDB();
1914  if ( !$dbw->trxLevel() ) {
1915  $dbw->begin( __METHOD__ );
1916  $this->lockedOwnTrx = true;
1917  }
1918  // Bug 54736: use simple lock to handle when the file does not exist.
1919  // SELECT FOR UPDATE prevents changes, not other SELECTs with FOR UPDATE.
1920  // Also, that would cause contention on INSERT of similarly named rows.
1921  $backend = $this->getRepo()->getBackend();
1922  $lockPaths = [ $this->getPath() ]; // represents all versions of the file
1923  $status = $backend->lockFiles( $lockPaths, LockManager::LOCK_EX, 5 );
1924  if ( !$status->isGood() ) {
1925  if ( $this->lockedOwnTrx ) {
1926  $dbw->rollback( __METHOD__ );
1927  }
1928  throw new LocalFileLockError( "Could not acquire lock for '{$this->getName()}.'" );
1929  }
1930  // Release the lock *after* commit to avoid row-level contention
1931  $this->locked++;
1932  $dbw->onTransactionIdle( function () use ( $backend, $lockPaths ) {
1933  $backend->unlockFiles( $lockPaths, LockManager::LOCK_EX );
1934  } );
1935  }
1936 
1937  return $this->lockedOwnTrx;
1938  }
1939 
1944  function unlock() {
1945  if ( $this->locked ) {
1946  --$this->locked;
1947  if ( !$this->locked && $this->lockedOwnTrx ) {
1948  $dbw = $this->repo->getMasterDB();
1949  $dbw->commit( __METHOD__ );
1950  $this->lockedOwnTrx = false;
1951  }
1952  }
1953  }
1954 
1958  function unlockAndRollback() {
1959  $this->locked = false;
1960  $dbw = $this->repo->getMasterDB();
1961  $dbw->rollback( __METHOD__ );
1962  $this->lockedOwnTrx = false;
1963  }
1964 
1968  protected function readOnlyFatalStatus() {
1969  return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(),
1970  $this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
1971  }
1972 
1976  function __destruct() {
1977  $this->unlock();
1978  }
1979 } // LocalFile class
1980 
1981 # ------------------------------------------------------------------------------
1982 
1989  private $file;
1990 
1992  private $reason;
1993 
1995  private $srcRels = [];
1996 
1998  private $archiveUrls = [];
1999 
2002 
2004  private $suppress;
2005 
2007  private $status;
2008 
2010  private $user;
2011 
2018  function __construct( File $file, $reason = '', $suppress = false, $user = null ) {
2019  $this->file = $file;
2020  $this->reason = $reason;
2021  $this->suppress = $suppress;
2022  if ( $user ) {
2023  $this->user = $user;
2024  } else {
2025  global $wgUser;
2026  $this->user = $wgUser;
2027  }
2028  $this->status = $file->repo->newGood();
2029  }
2030 
2031  public function addCurrent() {
2032  $this->srcRels['.'] = $this->file->getRel();
2033  }
2034 
2038  public function addOld( $oldName ) {
2039  $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
2040  $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
2041  }
2042 
2047  public function addOlds() {
2048  $archiveNames = [];
2049 
2050  $dbw = $this->file->repo->getMasterDB();
2051  $result = $dbw->select( 'oldimage',
2052  [ 'oi_archive_name' ],
2053  [ 'oi_name' => $this->file->getName() ],
2054  __METHOD__
2055  );
2056 
2057  foreach ( $result as $row ) {
2058  $this->addOld( $row->oi_archive_name );
2059  $archiveNames[] = $row->oi_archive_name;
2060  }
2061 
2062  return $archiveNames;
2063  }
2064 
2068  protected function getOldRels() {
2069  if ( !isset( $this->srcRels['.'] ) ) {
2070  $oldRels =& $this->srcRels;
2071  $deleteCurrent = false;
2072  } else {
2073  $oldRels = $this->srcRels;
2074  unset( $oldRels['.'] );
2075  $deleteCurrent = true;
2076  }
2077 
2078  return [ $oldRels, $deleteCurrent ];
2079  }
2080 
2084  protected function getHashes() {
2085  $hashes = [];
2086  list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2087 
2088  if ( $deleteCurrent ) {
2089  $hashes['.'] = $this->file->getSha1();
2090  }
2091 
2092  if ( count( $oldRels ) ) {
2093  $dbw = $this->file->repo->getMasterDB();
2094  $res = $dbw->select(
2095  'oldimage',
2096  [ 'oi_archive_name', 'oi_sha1' ],
2097  [ 'oi_archive_name' => array_keys( $oldRels ),
2098  'oi_name' => $this->file->getName() ], // performance
2099  __METHOD__
2100  );
2101 
2102  foreach ( $res as $row ) {
2103  if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
2104  // Get the hash from the file
2105  $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
2106  $props = $this->file->repo->getFileProps( $oldUrl );
2107 
2108  if ( $props['fileExists'] ) {
2109  // Upgrade the oldimage row
2110  $dbw->update( 'oldimage',
2111  [ 'oi_sha1' => $props['sha1'] ],
2112  [ 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ],
2113  __METHOD__ );
2114  $hashes[$row->oi_archive_name] = $props['sha1'];
2115  } else {
2116  $hashes[$row->oi_archive_name] = false;
2117  }
2118  } else {
2119  $hashes[$row->oi_archive_name] = $row->oi_sha1;
2120  }
2121  }
2122  }
2123 
2124  $missing = array_diff_key( $this->srcRels, $hashes );
2125 
2126  foreach ( $missing as $name => $rel ) {
2127  $this->status->error( 'filedelete-old-unregistered', $name );
2128  }
2129 
2130  foreach ( $hashes as $name => $hash ) {
2131  if ( !$hash ) {
2132  $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
2133  unset( $hashes[$name] );
2134  }
2135  }
2136 
2137  return $hashes;
2138  }
2139 
2140  protected function doDBInserts() {
2141  $dbw = $this->file->repo->getMasterDB();
2142  $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
2143  $encUserId = $dbw->addQuotes( $this->user->getId() );
2144  $encReason = $dbw->addQuotes( $this->reason );
2145  $encGroup = $dbw->addQuotes( 'deleted' );
2146  $ext = $this->file->getExtension();
2147  $dotExt = $ext === '' ? '' : ".$ext";
2148  $encExt = $dbw->addQuotes( $dotExt );
2149  list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2150 
2151  // Bitfields to further suppress the content
2152  if ( $this->suppress ) {
2153  $bitfield = 0;
2154  // This should be 15...
2155  $bitfield |= Revision::DELETED_TEXT;
2156  $bitfield |= Revision::DELETED_COMMENT;
2157  $bitfield |= Revision::DELETED_USER;
2158  $bitfield |= Revision::DELETED_RESTRICTED;
2159  } else {
2160  $bitfield = 'oi_deleted';
2161  }
2162 
2163  if ( $deleteCurrent ) {
2164  $concat = $dbw->buildConcat( [ "img_sha1", $encExt ] );
2165  $where = [ 'img_name' => $this->file->getName() ];
2166  $dbw->insertSelect( 'filearchive', 'image',
2167  [
2168  'fa_storage_group' => $encGroup,
2169  'fa_storage_key' => $dbw->conditional(
2170  [ 'img_sha1' => '' ],
2171  $dbw->addQuotes( '' ),
2172  $concat
2173  ),
2174  'fa_deleted_user' => $encUserId,
2175  'fa_deleted_timestamp' => $encTimestamp,
2176  'fa_deleted_reason' => $encReason,
2177  'fa_deleted' => $this->suppress ? $bitfield : 0,
2178 
2179  'fa_name' => 'img_name',
2180  'fa_archive_name' => 'NULL',
2181  'fa_size' => 'img_size',
2182  'fa_width' => 'img_width',
2183  'fa_height' => 'img_height',
2184  'fa_metadata' => 'img_metadata',
2185  'fa_bits' => 'img_bits',
2186  'fa_media_type' => 'img_media_type',
2187  'fa_major_mime' => 'img_major_mime',
2188  'fa_minor_mime' => 'img_minor_mime',
2189  'fa_description' => 'img_description',
2190  'fa_user' => 'img_user',
2191  'fa_user_text' => 'img_user_text',
2192  'fa_timestamp' => 'img_timestamp',
2193  'fa_sha1' => 'img_sha1',
2194  ], $where, __METHOD__ );
2195  }
2196 
2197  if ( count( $oldRels ) ) {
2198  $concat = $dbw->buildConcat( [ "oi_sha1", $encExt ] );
2199  $where = [
2200  'oi_name' => $this->file->getName(),
2201  'oi_archive_name' => array_keys( $oldRels ) ];
2202  $dbw->insertSelect( 'filearchive', 'oldimage',
2203  [
2204  'fa_storage_group' => $encGroup,
2205  'fa_storage_key' => $dbw->conditional(
2206  [ 'oi_sha1' => '' ],
2207  $dbw->addQuotes( '' ),
2208  $concat
2209  ),
2210  'fa_deleted_user' => $encUserId,
2211  'fa_deleted_timestamp' => $encTimestamp,
2212  'fa_deleted_reason' => $encReason,
2213  'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted',
2214 
2215  'fa_name' => 'oi_name',
2216  'fa_archive_name' => 'oi_archive_name',
2217  'fa_size' => 'oi_size',
2218  'fa_width' => 'oi_width',
2219  'fa_height' => 'oi_height',
2220  'fa_metadata' => 'oi_metadata',
2221  'fa_bits' => 'oi_bits',
2222  'fa_media_type' => 'oi_media_type',
2223  'fa_major_mime' => 'oi_major_mime',
2224  'fa_minor_mime' => 'oi_minor_mime',
2225  'fa_description' => 'oi_description',
2226  'fa_user' => 'oi_user',
2227  'fa_user_text' => 'oi_user_text',
2228  'fa_timestamp' => 'oi_timestamp',
2229  'fa_sha1' => 'oi_sha1',
2230  ], $where, __METHOD__ );
2231  }
2232  }
2233 
2234  function doDBDeletes() {
2235  $dbw = $this->file->repo->getMasterDB();
2236  list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2237 
2238  if ( count( $oldRels ) ) {
2239  $dbw->delete( 'oldimage',
2240  [
2241  'oi_name' => $this->file->getName(),
2242  'oi_archive_name' => array_keys( $oldRels )
2243  ], __METHOD__ );
2244  }
2245 
2246  if ( $deleteCurrent ) {
2247  $dbw->delete( 'image', [ 'img_name' => $this->file->getName() ], __METHOD__ );
2248  }
2249  }
2250 
2255  public function execute() {
2256  $repo = $this->file->getRepo();
2257  $this->file->lock();
2258 
2259  // Prepare deletion batch
2260  $hashes = $this->getHashes();
2261  $this->deletionBatch = [];
2262  $ext = $this->file->getExtension();
2263  $dotExt = $ext === '' ? '' : ".$ext";
2264 
2265  foreach ( $this->srcRels as $name => $srcRel ) {
2266  // Skip files that have no hash (e.g. missing DB record, or sha1 field and file source)
2267  if ( isset( $hashes[$name] ) ) {
2268  $hash = $hashes[$name];
2269  $key = $hash . $dotExt;
2270  $dstRel = $repo->getDeletedHashPath( $key ) . $key;
2271  $this->deletionBatch[$name] = [ $srcRel, $dstRel ];
2272  }
2273  }
2274 
2275  // Lock the filearchive rows so that the files don't get deleted by a cleanup operation
2276  // We acquire this lock by running the inserts now, before the file operations.
2277  // This potentially has poor lock contention characteristics -- an alternative
2278  // scheme would be to insert stub filearchive entries with no fa_name and commit
2279  // them in a separate transaction, then run the file ops, then update the fa_name fields.
2280  $this->doDBInserts();
2281 
2282  if ( !$repo->hasSha1Storage() ) {
2283  // Removes non-existent file from the batch, so we don't get errors.
2284  // This also handles files in the 'deleted' zone deleted via revision deletion.
2285  $checkStatus = $this->removeNonexistentFiles( $this->deletionBatch );
2286  if ( !$checkStatus->isGood() ) {
2287  $this->status->merge( $checkStatus );
2288  return $this->status;
2289  }
2290  $this->deletionBatch = $checkStatus->value;
2291 
2292  // Execute the file deletion batch
2293  $status = $this->file->repo->deleteBatch( $this->deletionBatch );
2294 
2295  if ( !$status->isGood() ) {
2296  $this->status->merge( $status );
2297  }
2298  }
2299 
2300  if ( !$this->status->isOK() ) {
2301  // Critical file deletion error
2302  // Roll back inserts, release lock and abort
2303  // TODO: delete the defunct filearchive rows if we are using a non-transactional DB
2304  $this->file->unlockAndRollback();
2305 
2306  return $this->status;
2307  }
2308 
2309  // Delete image/oldimage rows
2310  $this->doDBDeletes();
2311 
2312  // Commit and return
2313  $this->file->unlock();
2314 
2315  return $this->status;
2316  }
2317 
2323  protected function removeNonexistentFiles( $batch ) {
2324  $files = $newBatch = [];
2325 
2326  foreach ( $batch as $batchItem ) {
2327  list( $src, ) = $batchItem;
2328  $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
2329  }
2330 
2331  $result = $this->file->repo->fileExistsBatch( $files );
2332  if ( in_array( null, $result, true ) ) {
2333  return Status::newFatal( 'backend-fail-internal',
2334  $this->file->repo->getBackend()->getName() );
2335  }
2336 
2337  foreach ( $batch as $batchItem ) {
2338  if ( $result[$batchItem[0]] ) {
2339  $newBatch[] = $batchItem;
2340  }
2341  }
2342 
2343  return Status::newGood( $newBatch );
2344  }
2345 }
2346 
2347 # ------------------------------------------------------------------------------
2348 
2355  private $file;
2356 
2358  private $cleanupBatch;
2359 
2361  private $ids;
2362 
2364  private $all;
2365 
2367  private $unsuppress = false;
2368 
2373  function __construct( File $file, $unsuppress = false ) {
2374  $this->file = $file;
2375  $this->cleanupBatch = $this->ids = [];
2376  $this->ids = [];
2377  $this->unsuppress = $unsuppress;
2378  }
2379 
2384  public function addId( $fa_id ) {
2385  $this->ids[] = $fa_id;
2386  }
2387 
2392  public function addIds( $ids ) {
2393  $this->ids = array_merge( $this->ids, $ids );
2394  }
2395 
2399  public function addAll() {
2400  $this->all = true;
2401  }
2402 
2411  public function execute() {
2412  global $wgLang;
2413 
2414  $repo = $this->file->getRepo();
2415  if ( !$this->all && !$this->ids ) {
2416  // Do nothing
2417  return $repo->newGood();
2418  }
2419 
2420  $lockOwnsTrx = $this->file->lock();
2421 
2422  $dbw = $this->file->repo->getMasterDB();
2423  $status = $this->file->repo->newGood();
2424 
2425  $exists = (bool)$dbw->selectField( 'image', '1',
2426  [ 'img_name' => $this->file->getName() ],
2427  __METHOD__,
2428  // The lock() should already prevents changes, but this still may need
2429  // to bypass any transaction snapshot. However, if lock() started the
2430  // trx (which it probably did) then snapshot is post-lock and up-to-date.
2431  $lockOwnsTrx ? [] : [ 'LOCK IN SHARE MODE' ]
2432  );
2433 
2434  // Fetch all or selected archived revisions for the file,
2435  // sorted from the most recent to the oldest.
2436  $conditions = [ 'fa_name' => $this->file->getName() ];
2437 
2438  if ( !$this->all ) {
2439  $conditions['fa_id'] = $this->ids;
2440  }
2441 
2442  $result = $dbw->select(
2443  'filearchive',
2445  $conditions,
2446  __METHOD__,
2447  [ 'ORDER BY' => 'fa_timestamp DESC' ]
2448  );
2449 
2450  $idsPresent = [];
2451  $storeBatch = [];
2452  $insertBatch = [];
2453  $insertCurrent = false;
2454  $deleteIds = [];
2455  $first = true;
2456  $archiveNames = [];
2457 
2458  foreach ( $result as $row ) {
2459  $idsPresent[] = $row->fa_id;
2460 
2461  if ( $row->fa_name != $this->file->getName() ) {
2462  $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
2463  $status->failCount++;
2464  continue;
2465  }
2466 
2467  if ( $row->fa_storage_key == '' ) {
2468  // Revision was missing pre-deletion
2469  $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
2470  $status->failCount++;
2471  continue;
2472  }
2473 
2474  $deletedRel = $repo->getDeletedHashPath( $row->fa_storage_key ) .
2475  $row->fa_storage_key;
2476  $deletedUrl = $repo->getVirtualUrl() . '/deleted/' . $deletedRel;
2477 
2478  if ( isset( $row->fa_sha1 ) ) {
2479  $sha1 = $row->fa_sha1;
2480  } else {
2481  // old row, populate from key
2482  $sha1 = LocalRepo::getHashFromKey( $row->fa_storage_key );
2483  }
2484 
2485  # Fix leading zero
2486  if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
2487  $sha1 = substr( $sha1, 1 );
2488  }
2489 
2490  if ( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
2491  || is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
2492  || is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
2493  || is_null( $row->fa_metadata )
2494  ) {
2495  // Refresh our metadata
2496  // Required for a new current revision; nice for older ones too. :)
2497  $props = RepoGroup::singleton()->getFileProps( $deletedUrl );
2498  } else {
2499  $props = [
2500  'minor_mime' => $row->fa_minor_mime,
2501  'major_mime' => $row->fa_major_mime,
2502  'media_type' => $row->fa_media_type,
2503  'metadata' => $row->fa_metadata
2504  ];
2505  }
2506 
2507  if ( $first && !$exists ) {
2508  // This revision will be published as the new current version
2509  $destRel = $this->file->getRel();
2510  $insertCurrent = [
2511  'img_name' => $row->fa_name,
2512  'img_size' => $row->fa_size,
2513  'img_width' => $row->fa_width,
2514  'img_height' => $row->fa_height,
2515  'img_metadata' => $props['metadata'],
2516  'img_bits' => $row->fa_bits,
2517  'img_media_type' => $props['media_type'],
2518  'img_major_mime' => $props['major_mime'],
2519  'img_minor_mime' => $props['minor_mime'],
2520  'img_description' => $row->fa_description,
2521  'img_user' => $row->fa_user,
2522  'img_user_text' => $row->fa_user_text,
2523  'img_timestamp' => $row->fa_timestamp,
2524  'img_sha1' => $sha1
2525  ];
2526 
2527  // The live (current) version cannot be hidden!
2528  if ( !$this->unsuppress && $row->fa_deleted ) {
2529  $status->fatal( 'undeleterevdel' );
2530  $this->file->unlock();
2531  return $status;
2532  }
2533  } else {
2534  $archiveName = $row->fa_archive_name;
2535 
2536  if ( $archiveName == '' ) {
2537  // This was originally a current version; we
2538  // have to devise a new archive name for it.
2539  // Format is <timestamp of archiving>!<name>
2540  $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
2541 
2542  do {
2543  $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
2544  $timestamp++;
2545  } while ( isset( $archiveNames[$archiveName] ) );
2546  }
2547 
2548  $archiveNames[$archiveName] = true;
2549  $destRel = $this->file->getArchiveRel( $archiveName );
2550  $insertBatch[] = [
2551  'oi_name' => $row->fa_name,
2552  'oi_archive_name' => $archiveName,
2553  'oi_size' => $row->fa_size,
2554  'oi_width' => $row->fa_width,
2555  'oi_height' => $row->fa_height,
2556  'oi_bits' => $row->fa_bits,
2557  'oi_description' => $row->fa_description,
2558  'oi_user' => $row->fa_user,
2559  'oi_user_text' => $row->fa_user_text,
2560  'oi_timestamp' => $row->fa_timestamp,
2561  'oi_metadata' => $props['metadata'],
2562  'oi_media_type' => $props['media_type'],
2563  'oi_major_mime' => $props['major_mime'],
2564  'oi_minor_mime' => $props['minor_mime'],
2565  'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted,
2566  'oi_sha1' => $sha1 ];
2567  }
2568 
2569  $deleteIds[] = $row->fa_id;
2570 
2571  if ( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
2572  // private files can stay where they are
2573  $status->successCount++;
2574  } else {
2575  $storeBatch[] = [ $deletedUrl, 'public', $destRel ];
2576  $this->cleanupBatch[] = $row->fa_storage_key;
2577  }
2578 
2579  $first = false;
2580  }
2581 
2582  unset( $result );
2583 
2584  // Add a warning to the status object for missing IDs
2585  $missingIds = array_diff( $this->ids, $idsPresent );
2586 
2587  foreach ( $missingIds as $id ) {
2588  $status->error( 'undelete-missing-filearchive', $id );
2589  }
2590 
2591  if ( !$repo->hasSha1Storage() ) {
2592  // Remove missing files from batch, so we don't get errors when undeleting them
2593  $checkStatus = $this->removeNonexistentFiles( $storeBatch );
2594  if ( !$checkStatus->isGood() ) {
2595  $status->merge( $checkStatus );
2596  return $status;
2597  }
2598  $storeBatch = $checkStatus->value;
2599 
2600  // Run the store batch
2601  // Use the OVERWRITE_SAME flag to smooth over a common error
2602  $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
2603  $status->merge( $storeStatus );
2604 
2605  if ( !$status->isGood() ) {
2606  // Even if some files could be copied, fail entirely as that is the
2607  // easiest thing to do without data loss
2608  $this->cleanupFailedBatch( $storeStatus, $storeBatch );
2609  $status->ok = false;
2610  $this->file->unlock();
2611 
2612  return $status;
2613  }
2614  }
2615 
2616  // Run the DB updates
2617  // Because we have locked the image row, key conflicts should be rare.
2618  // If they do occur, we can roll back the transaction at this time with
2619  // no data loss, but leaving unregistered files scattered throughout the
2620  // public zone.
2621  // This is not ideal, which is why it's important to lock the image row.
2622  if ( $insertCurrent ) {
2623  $dbw->insert( 'image', $insertCurrent, __METHOD__ );
2624  }
2625 
2626  if ( $insertBatch ) {
2627  $dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
2628  }
2629 
2630  if ( $deleteIds ) {
2631  $dbw->delete( 'filearchive',
2632  [ 'fa_id' => $deleteIds ],
2633  __METHOD__ );
2634  }
2635 
2636  // If store batch is empty (all files are missing), deletion is to be considered successful
2637  if ( $status->successCount > 0 || !$storeBatch || $repo->hasSha1Storage() ) {
2638  if ( !$exists ) {
2639  wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" );
2640 
2641  DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => 1 ] ) );
2642 
2643  $this->file->purgeEverything();
2644  } else {
2645  wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" );
2646  $this->file->purgeDescription();
2647  }
2648  }
2649 
2650  $this->file->unlock();
2651 
2652  return $status;
2653  }
2654 
2660  protected function removeNonexistentFiles( $triplets ) {
2661  $files = $filteredTriplets = [];
2662  foreach ( $triplets as $file ) {
2663  $files[$file[0]] = $file[0];
2664  }
2665 
2666  $result = $this->file->repo->fileExistsBatch( $files );
2667  if ( in_array( null, $result, true ) ) {
2668  return Status::newFatal( 'backend-fail-internal',
2669  $this->file->repo->getBackend()->getName() );
2670  }
2671 
2672  foreach ( $triplets as $file ) {
2673  if ( $result[$file[0]] ) {
2674  $filteredTriplets[] = $file;
2675  }
2676  }
2677 
2678  return Status::newGood( $filteredTriplets );
2679  }
2680 
2686  protected function removeNonexistentFromCleanup( $batch ) {
2687  $files = $newBatch = [];
2688  $repo = $this->file->repo;
2689 
2690  foreach ( $batch as $file ) {
2691  $files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' .
2692  rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
2693  }
2694 
2695  $result = $repo->fileExistsBatch( $files );
2696 
2697  foreach ( $batch as $file ) {
2698  if ( $result[$file] ) {
2699  $newBatch[] = $file;
2700  }
2701  }
2702 
2703  return $newBatch;
2704  }
2705 
2711  public function cleanup() {
2712  if ( !$this->cleanupBatch ) {
2713  return $this->file->repo->newGood();
2714  }
2715 
2716  $this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch );
2717 
2718  $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
2719 
2720  return $status;
2721  }
2722 
2730  protected function cleanupFailedBatch( $storeStatus, $storeBatch ) {
2731  $cleanupBatch = [];
2732 
2733  foreach ( $storeStatus->success as $i => $success ) {
2734  // Check if this item of the batch was successfully copied
2735  if ( $success ) {
2736  // Item was successfully copied and needs to be removed again
2737  // Extract ($dstZone, $dstRel) from the batch
2738  $cleanupBatch[] = [ $storeBatch[$i][1], $storeBatch[$i][2] ];
2739  }
2740  }
2741  $this->file->repo->cleanupBatch( $cleanupBatch );
2742  }
2743 }
2744 
2745 # ------------------------------------------------------------------------------
2746 
2753  protected $file;
2754 
2756  protected $target;
2757 
2758  protected $cur;
2759 
2760  protected $olds;
2761 
2762  protected $oldCount;
2763 
2764  protected $archive;
2765 
2767  protected $db;
2768 
2774  $this->file = $file;
2775  $this->target = $target;
2776  $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
2777  $this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() );
2778  $this->oldName = $this->file->getName();
2779  $this->newName = $this->file->repo->getNameFromTitle( $this->target );
2780  $this->oldRel = $this->oldHash . $this->oldName;
2781  $this->newRel = $this->newHash . $this->newName;
2782  $this->db = $file->getRepo()->getMasterDB();
2783  }
2784 
2788  public function addCurrent() {
2789  $this->cur = [ $this->oldRel, $this->newRel ];
2790  }
2791 
2796  public function addOlds() {
2797  $archiveBase = 'archive';
2798  $this->olds = [];
2799  $this->oldCount = 0;
2800  $archiveNames = [];
2801 
2802  $result = $this->db->select( 'oldimage',
2803  [ 'oi_archive_name', 'oi_deleted' ],
2804  [ 'oi_name' => $this->oldName ],
2805  __METHOD__,
2806  [ 'LOCK IN SHARE MODE' ] // ignore snapshot
2807  );
2808 
2809  foreach ( $result as $row ) {
2810  $archiveNames[] = $row->oi_archive_name;
2811  $oldName = $row->oi_archive_name;
2812  $bits = explode( '!', $oldName, 2 );
2813 
2814  if ( count( $bits ) != 2 ) {
2815  wfDebug( "Old file name missing !: '$oldName' \n" );
2816  continue;
2817  }
2818 
2819  list( $timestamp, $filename ) = $bits;
2820 
2821  if ( $this->oldName != $filename ) {
2822  wfDebug( "Old file name doesn't match: '$oldName' \n" );
2823  continue;
2824  }
2825 
2826  $this->oldCount++;
2827 
2828  // Do we want to add those to oldCount?
2829  if ( $row->oi_deleted & File::DELETED_FILE ) {
2830  continue;
2831  }
2832 
2833  $this->olds[] = [
2834  "{$archiveBase}/{$this->oldHash}{$oldName}",
2835  "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
2836  ];
2837  }
2838 
2839  return $archiveNames;
2840  }
2841 
2846  public function execute() {
2847  $repo = $this->file->repo;
2848  $status = $repo->newGood();
2849 
2850  $triplets = $this->getMoveTriplets();
2851  $checkStatus = $this->removeNonexistentFiles( $triplets );
2852  if ( !$checkStatus->isGood() ) {
2853  $status->merge( $checkStatus );
2854  return $status;
2855  }
2856  $triplets = $checkStatus->value;
2857  $destFile = wfLocalFile( $this->target );
2858 
2859  $this->file->lock(); // begin
2860  $destFile->lock(); // quickly fail if destination is not available
2861  // Rename the file versions metadata in the DB.
2862  // This implicitly locks the destination file, which avoids race conditions.
2863  // If we moved the files from A -> C before DB updates, another process could
2864  // move files from B -> C at this point, causing storeBatch() to fail and thus
2865  // cleanupTarget() to trigger. It would delete the C files and cause data loss.
2866  $statusDb = $this->doDBUpdates();
2867  if ( !$statusDb->isGood() ) {
2868  $destFile->unlock();
2869  $this->file->unlockAndRollback();
2870  $statusDb->ok = false;
2871 
2872  return $statusDb;
2873  }
2874  wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: " .
2875  "{$statusDb->successCount} successes, {$statusDb->failCount} failures" );
2876 
2877  if ( !$repo->hasSha1Storage() ) {
2878  // Copy the files into their new location.
2879  // If a prior process fataled copying or cleaning up files we tolerate any
2880  // of the existing files if they are identical to the ones being stored.
2881  $statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME );
2882  wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: " .
2883  "{$statusMove->successCount} successes, {$statusMove->failCount} failures" );
2884  if ( !$statusMove->isGood() ) {
2885  // Delete any files copied over (while the destination is still locked)
2886  $this->cleanupTarget( $triplets );
2887  $destFile->unlock();
2888  $this->file->unlockAndRollback(); // unlocks the destination
2889  wfDebugLog( 'imagemove', "Error in moving files: "
2890  . $statusMove->getWikiText( false, false, 'en' ) );
2891  $statusMove->ok = false;
2892 
2893  return $statusMove;
2894  }
2895  $status->merge( $statusMove );
2896  }
2897 
2898  $destFile->unlock();
2899  $this->file->unlock(); // done
2900 
2901  // Everything went ok, remove the source files
2902  $this->cleanupSource( $triplets );
2903 
2904  $status->merge( $statusDb );
2905 
2906  return $status;
2907  }
2908 
2915  protected function doDBUpdates() {
2916  $repo = $this->file->repo;
2917  $status = $repo->newGood();
2918  $dbw = $this->db;
2919 
2920  // Update current image
2921  $dbw->update(
2922  'image',
2923  [ 'img_name' => $this->newName ],
2924  [ 'img_name' => $this->oldName ],
2925  __METHOD__
2926  );
2927 
2928  if ( $dbw->affectedRows() ) {
2929  $status->successCount++;
2930  } else {
2931  $status->failCount++;
2932  $status->fatal( 'imageinvalidfilename' );
2933 
2934  return $status;
2935  }
2936 
2937  // Update old images
2938  $dbw->update(
2939  'oldimage',
2940  [
2941  'oi_name' => $this->newName,
2942  'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name',
2943  $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
2944  ],
2945  [ 'oi_name' => $this->oldName ],
2946  __METHOD__
2947  );
2948 
2949  $affected = $dbw->affectedRows();
2950  $total = $this->oldCount;
2951  $status->successCount += $affected;
2952  // Bug 34934: $total is based on files that actually exist.
2953  // There may be more DB rows than such files, in which case $affected
2954  // can be greater than $total. We use max() to avoid negatives here.
2955  $status->failCount += max( 0, $total - $affected );
2956  if ( $status->failCount ) {
2957  $status->error( 'imageinvalidfilename' );
2958  }
2959 
2960  return $status;
2961  }
2962 
2967  protected function getMoveTriplets() {
2968  $moves = array_merge( [ $this->cur ], $this->olds );
2969  $triplets = []; // The format is: (srcUrl, destZone, destUrl)
2970 
2971  foreach ( $moves as $move ) {
2972  // $move: (oldRelativePath, newRelativePath)
2973  $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
2974  $triplets[] = [ $srcUrl, 'public', $move[1] ];
2975  wfDebugLog(
2976  'imagemove',
2977  "Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}"
2978  );
2979  }
2980 
2981  return $triplets;
2982  }
2983 
2989  protected function removeNonexistentFiles( $triplets ) {
2990  $files = [];
2991 
2992  foreach ( $triplets as $file ) {
2993  $files[$file[0]] = $file[0];
2994  }
2995 
2996  $result = $this->file->repo->fileExistsBatch( $files );
2997  if ( in_array( null, $result, true ) ) {
2998  return Status::newFatal( 'backend-fail-internal',
2999  $this->file->repo->getBackend()->getName() );
3000  }
3001 
3002  $filteredTriplets = [];
3003  foreach ( $triplets as $file ) {
3004  if ( $result[$file[0]] ) {
3005  $filteredTriplets[] = $file;
3006  } else {
3007  wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
3008  }
3009  }
3010 
3011  return Status::newGood( $filteredTriplets );
3012  }
3013 
3019  protected function cleanupTarget( $triplets ) {
3020  // Create dest pairs from the triplets
3021  $pairs = [];
3022  foreach ( $triplets as $triplet ) {
3023  // $triplet: (old source virtual URL, dst zone, dest rel)
3024  $pairs[] = [ $triplet[1], $triplet[2] ];
3025  }
3026 
3027  $this->file->repo->cleanupBatch( $pairs );
3028  }
3029 
3035  protected function cleanupSource( $triplets ) {
3036  // Create source file names from the triplets
3037  $files = [];
3038  foreach ( $triplets as $triplet ) {
3039  $files[] = $triplet[0];
3040  }
3041 
3042  $this->file->repo->cleanupBatch( $files );
3043  }
3044 }
3045 
3047 
3048 }
static purgePatrolFooterCache($articleID)
Purge the cache used to check if it is worth showing the patrol footer For example, it is done during re-uploads when file patrol is used.
Definition: Article.php:1233
removeNonexistentFiles($batch)
Removes non-existent files from a deletion batch.
Definition: LocalFile.php:2323
getArchiveThumbPath($archiveName, $suffix=false)
Get the path of an archived file's thumbs, or a particular thumb if $suffix is specified.
Definition: File.php:1598
static getMainWANInstance()
Get the main WAN cache object.
exists()
canRender inherited
Definition: LocalFile.php:839
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
invalidateCache()
Purge the file object/metadata cache.
Definition: LocalFile.php:312
the array() calling protocol came about after MediaWiki 1.4rc1.
MediaHandler $handler
Definition: File.php:113
string $media_type
MEDIATYPE_xxx (bitmap, drawing, audio...)
Definition: LocalFile.php:62
recordUpload($oldver, $desc, $license= '', $copyStatus= '', $source= '', $watch=false, $timestamp=false, User $user=null)
Record a file upload in the upload log and the image table.
Definition: LocalFile.php:1196
magic word the default is to use $key to get the and $key value or $key value text $key value html to format the value $key
Definition: hooks.txt:2321
bool $extraDataLoaded
Whether or not lazy-loaded data has been loaded from the database.
Definition: LocalFile.php:80
if(count($args)==0) $dir
assertTitleDefined()
Assert that $this->title is set to a Title.
Definition: File.php:2258
$success
userCan($field, User $user=null)
Determine if the current user is allowed to view a particular field of this file, if it's marked as d...
Definition: File.php:2147
addAll()
Add all revisions of the file.
Definition: LocalFile.php:2399
cleanupTarget($triplets)
Cleanup a partially moved array of triplets by deleting the target files.
Definition: LocalFile.php:3019
cleanupSource($triplets)
Cleanup a fully moved array of triplets by deleting the source files.
Definition: LocalFile.php:3035
loadFromRow($row, $prefix= 'img_')
Load file metadata from a DB result row.
Definition: LocalFile.php:521
string $minor_mime
Minor MIME type.
Definition: LocalFile.php:98
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
getHistory($limit=null, $start=null, $end=null, $inc=true)
purgeDescription inherited
Definition: LocalFile.php:1002
restore($versions=[], $unsuppress=false)
Restore all or specified deleted revisions to the given file.
Definition: LocalFile.php:1763
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException'returning false will NOT prevent logging $e
Definition: hooks.txt:1932
Set options of the Parser.
purgeMetadataCache()
Refresh metadata in memcached, but don't touch thumbnails or CDN.
Definition: LocalFile.php:883
const DELETE_SOURCE
Definition: File.php:65
if(!isset($args[0])) $lang
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:189
static newFromEntry(LogEntry $entry)
Constructs a new formatter suitable for given entry.
__construct(File $file, $reason= '', $suppress=false, $user=null)
Definition: LocalFile.php:2018
cleanup()
Delete unused files in the deleted zone.
Definition: LocalFile.php:2711
getUser($type= 'text')
Returns ID or name of user who uploaded the file.
Definition: LocalFile.php:751
getThumbPath($suffix=false)
Get the path of the thumbnail directory, or a particular file if $suffix is specified.
Definition: File.php:1611
width
recordUpload2($oldver, $comment, $pageText, $props=false, $timestamp=false, $user=null, $tags=[])
Record a file upload in the upload log and the image table.
Definition: LocalFile.php:1227
addCurrent()
Add the current image to the batch.
Definition: LocalFile.php:2788
const DELETE_SOURCE
Definition: FileRepo.php:38
getSize()
Returns the size of the image file, in bytes.
Definition: LocalFile.php:802
Handles purging appropriate CDN URLs given a title (or titles)
$comment
Helper class for file undeletion.
Definition: LocalFile.php:2353
unlock()
Decrement the lock reference count.
Definition: LocalFile.php:1944
string $major_mime
Major MIME type.
Definition: LocalFile.php:95
string $sha1
SHA-1 base 36 content hash.
Definition: LocalFile.php:74
$source
$value
isMissing()
splitMime inherited
Definition: LocalFile.php:682
getArchiveThumbUrl($archiveName, $suffix=false)
Get the URL of the archived file's thumbs, or a particular thumb if $suffix is specified.
Definition: File.php:1655
static isVirtualUrl($url)
Determine if a string is an mwrepo:// URL.
Definition: FileRepo.php:254
$files
assertRepoDefined()
Assert that $this->repo is set to a valid FileRepo instance.
Definition: File.php:2248
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition: hooks.txt:2548
getName()
Return the name of this file.
Definition: File.php:296
string $name
The name of a file from its title object.
Definition: File.php:122
static newFromKey($sha1, $repo, $timestamp=false)
Create a LocalFile from a SHA-1 key Do not call this except from inside a repo class.
Definition: LocalFile.php:172
getRepo()
Returns the repository.
Definition: File.php:1854
unprefixRow($row, $prefix= 'img_')
Definition: LocalFile.php:460
const MW_FILE_VERSION
Bump this number when serialized cache records may be incompatible.
Definition: LocalFile.php:27
array $cleanupBatch
List of file IDs to restore.
Definition: LocalFile.php:2358
int $user
User ID of uploader.
Definition: LocalFile.php:104
Represents a title within MediaWiki.
Definition: Title.php:34
setProps($info)
Set properties in this object to be equal to those given in the associative array $info...
Definition: LocalFile.php:651
when a variable name is used in a it is silently declared as a new local masking the global
Definition: design.txt:93
const METADATA_BAD
static makeParamBlob($params)
Create a blob from a parameter array.
Definition: LogEntry.php:140
static newFatal($message)
Factory function for fatal errors.
Definition: Status.php:89
int $bits
Returned by getimagesize (loadFromXxx)
Definition: LocalFile.php:59
static newFromTitle(LinkTarget $linkTarget, $id=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target...
Definition: Revision.php:117
publish($src, $flags=0, array $options=[])
Move or copy a file to its public location.
Definition: LocalFile.php:1539
bool $fileExists
Does the file exist on disk? (loadFromXxx)
Definition: LocalFile.php:50
wfLocalFile($title)
Get an object referring to a locally registered file.
doDBUpdates()
Do the database updates and return a new FileRepoStatus indicating how many rows where updated...
Definition: LocalFile.php:2915
getHeight($page=1)
Return the height of the image.
Definition: LocalFile.php:724
getTitle()
Return the associated title object.
Definition: File.php:325
Title string bool $title
Definition: File.php:98
getHashPath()
Get the filename hash component of the directory including trailing slash, e.g.
Definition: File.php:1497
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist & $tables
Definition: hooks.txt:965
__destruct()
Clean up any dangling locks.
Definition: LocalFile.php:1976
static queueRecursiveJobsForTable(Title $title, $table)
Queue a RefreshLinks job for any table.
wfDebug($text, $dest= 'all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
const DELETED_FILE
Definition: File.php:52
getBackend()
Get the file backend instance.
Definition: FileRepo.php:215
update($table, $values, $conds, $fname=__METHOD__, $options=[])
UPDATE wrapper.
Definition: Database.php:1506
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as $wgLang
Definition: design.txt:56
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message.Please note the header message cannot receive/use parameters. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item.Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page.Return false to stop further processing of the tag $reader:XMLReader object &$pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision.Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag.Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload.Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports.&$fullInterwikiPrefix:Interwiki prefix, may contain colons.&$pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable.Can be used to lazy-load the import sources list.&$importSources:The value of $wgImportSources.Modify as necessary.See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page.$context:IContextSource object &$pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect.&$title:Title object for the current page &$request:WebRequest &$ignoreRedirect:boolean to skip redirect check &$target:Title/string of redirect target &$article:Article object 'InternalParseBeforeLinks':during Parser's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings.&$parser:Parser object &$text:string containing partially parsed text &$stripState:Parser's internal StripState object 'InternalParseBeforeSanitize':during Parser's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings.Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments.&$parser:Parser object &$text:string containing partially parsed text &$stripState:Parser's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not.Return true without providing an interwiki to continue interwiki search.$prefix:interwiki prefix we are looking for.&$iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user's email has been invalidated successfully.$user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification.Callee may modify $url and $query, URL will be constructed as $url.$query &$url:URL to index.php &$query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) &$article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() &$ip:IP being check &$result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from &$allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn't match your organization.$addr:The e-mail address entered by the user &$result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user &$result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we're looking for a messages file for &$file:The messages file path, you can override this to change the location. 'LanguageGetMagic':DEPRECATED!Use $magicWords in a file listed in $wgExtensionMessagesFiles instead.Use this to define synonyms of magic words depending of the language &$magicExtensions:associative array of magic words synonyms $lang:language code(string) 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces.Do not use this hook to add namespaces.Use CanonicalNamespaces for that.&$namespaces:Array of namespaces indexed by their numbers 'LanguageGetSpecialPageAliases':DEPRECATED!Use $specialPageAliases in a file listed in $wgExtensionMessagesFiles instead.Use to define aliases of special pages names depending of the language &$specialPageAliases:associative array of magic words synonyms $lang:language code(string) 'LanguageGetTranslatedLanguageNames':Provide translated language names.&$names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page's language links.This is called in various places to allow extensions to define the effective language links for a page.$title:The page's Title.&$links:Associative array mapping language codes to prefixed links of the form"language:title".&$linkFlags:Associative array mapping prefixed links to arrays of flags.Currently unused, but planned to provide support for marking individual language links in the UI, e.g.for featured articles. 'LanguageSelector':Hook to change the language selector available on a page.$out:The output page.$cssClassName:CSS class name of the language selector. 'LinkBegin':Used when generating internal and interwiki links in Linker::link(), before processing starts.Return false to skip default processing and return $ret.See documentation for Linker::link() for details on the expected meanings of parameters.$skin:the Skin object $target:the Title that the link is pointing to &$html:the contents that the< a > tag should have(raw HTML) $result
Definition: hooks.txt:1796
getThumbnails()
Get all thumbnail names previously generated for this file STUB Overridden by LocalFile.
Definition: File.php:1410
$batch
Definition: linkcache.txt:23
getDescriptionShortUrl()
Get short description URL for a file based on the page ID.
Definition: LocalFile.php:768
nextHistoryLine()
Returns the history of this file, line by line.
Definition: LocalFile.php:1053
wfTimestamp($outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
unlockAndRollback()
Roll back the DB transaction and mark the image unlocked.
Definition: LocalFile.php:1958
__construct(File $file, Title $target)
Definition: LocalFile.php:2773
getPath()
Return the storage path to the file.
Definition: File.php:416
wfDebugLog($logGroup, $text, $dest= 'all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not...
removeNonexistentFiles($triplets)
Removes non-existent files from move batch.
Definition: LocalFile.php:2989
int $historyRes
Result of the query for the file's history (nextHistoryLine)
Definition: LocalFile.php:92
bool $locked
True if the image row is locked.
Definition: LocalFile.php:119
getFileSha1($virtualUrl)
Get the sha1 (base 36) of a file with a given virtual URL/storage path.
Definition: FileRepo.php:1586
deleteOld($archiveName, $reason, $suppress=false, $user=null)
Delete an old version of the file.
Definition: LocalFile.php:1727
getReadOnlyReason()
Get an explanatory message if this repo is read-only.
Definition: FileRepo.php:225
__construct(File $file, $unsuppress=false)
Definition: LocalFile.php:2373
wfReadOnly()
Check whether the wiki is in read-only mode.
string $metadata
Handler-specific metadata.
Definition: LocalFile.php:71
string $descriptionTouched
TS_MW timestamp of the last change of the file description.
Definition: LocalFile.php:113
We ve cleaned up the code here by removing clumps of infrequently used code and moving them off somewhere else It s much easier for someone working with this code to see what s _really_ going and make changes or fix bugs In we can take all the code that deals with the little used title reversing we can concentrate it all in an extension file
Definition: hooks.txt:93
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add in any and then calling but I prefer the flexibility This should also do the output encoding The system allocates a global one in $wgOut Title Represents the title of an and does all the work of translating among various forms such as plain database key
Definition: design.txt:25
publishTo($src, $dstRel, $flags=0, array $options=[])
Move or copy a file to a specified location.
Definition: LocalFile.php:1558
string $repoClass
Definition: LocalFile.php:86
File backend exception for checked exceptions (e.g.
isDeleted($field)
Is this file a "deleted" file in a private archive? STUB.
Definition: File.php:1875
Class to invalidate the HTML cache of all the pages linking to a given title.
isMultipage()
Returns 'true' if this file is a type which supports multiple pages, e.g.
Definition: File.php:1959
const LOAD_ALL
Definition: LocalFile.php:128
getHandler()
Get a MediaHandler instance for this file.
Definition: File.php:1365
getCacheKey()
Get the memcached key for the main data for this file, or false if there is no access to the shared c...
Definition: LocalFile.php:235
isMetadataValid($image, $metadata)
Check if the metadata string is valid for this handler.
static factory(array $deltas)
if($limit) $timestamp
static selectFields()
Fields in the filearchive table.
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context as context $options
Definition: hooks.txt:1004
getDescriptionTouched()
Definition: LocalFile.php:1853
const LOCK_EX
Definition: LockManager.php:62
getRel()
Get the path of the file relative to the public zone root.
Definition: File.php:1512
$res
Definition: database.txt:21
static singleton()
Get a RepoGroup instance.
Definition: RepoGroup.php:59
static isStoragePath($path)
Check if a given path is a "mwstore://" path.
bool $unsuppress
Whether to remove all settings for suppressed fields.
Definition: LocalFile.php:2367
static newNullRevision($dbw, $pageId, $summary, $minor, $user=null)
Create a new null-revision for insertion into a page's history.
Definition: Revision.php:1624
static selectFields()
Fields in the image table.
Definition: LocalFile.php:192
string $user_text
User name of uploader.
Definition: LocalFile.php:107
__construct($title, $repo)
Constructor.
Definition: LocalFile.php:217
const CACHE_FIELD_MAX_LEN
Definition: LocalFile.php:47
purgeThumbList($dir, $files)
Delete a list of thumbnails visible at urls.
Definition: LocalFile.php:968
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
DatabaseBase $db
Definition: LocalFile.php:2767
upload($src, $comment, $pageText, $flags=0, $props=false, $timestamp=false, $user=null, $tags=[])
getHashPath inherited
Definition: LocalFile.php:1129
loadExtraFromDB()
Load lazy file metadata from the DB.
Definition: LocalFile.php:408
$cache
Definition: mcc.php:33
const EDIT_SUPPRESS_RC
Definition: Defines.php:182
static newFromRow($row, $repo)
Create a LocalFile from a title Do not call this except from inside a repo class. ...
Definition: LocalFile.php:155
maybeUpgradeRow()
Upgrade a row if it needs it.
Definition: LocalFile.php:556
static getInitialPageText($comment= '', $license= '', $copyStatus= '', $source= '', Config $config=null)
Get the initial image page text based on a comment and optional file status information.
quickImport($src, $dst, $options=null)
Import a file from the local file system into the repo.
Definition: FileRepo.php:966
readOnlyFatalStatus()
Definition: LocalFile.php:1968
Helper class for file movement.
Definition: LocalFile.php:2751
bool $upgraded
Whether the row was upgraded on load.
Definition: LocalFile.php:116
static newExtraneousContext(Title $title, $request=[])
Create a new extraneous context.
static selectFields()
Fields in the oldimage table.
static newFromTitle($title, $repo, $unused=null)
Create a LocalFile from a title Do not call this except from inside a repo class. ...
Definition: LocalFile.php:142
loadFieldsWithTimestamp($dbr, $fname)
Definition: LocalFile.php:433
execute()
Run the transaction.
Definition: LocalFile.php:2255
title
const DELETED_RESTRICTED
Definition: Revision.php:79
getVirtualUrl($suffix=false)
Get the public zone virtual URL for a current version source file.
Definition: File.php:1713
bool $all
Add all revisions of the file.
Definition: LocalFile.php:2364
decodeRow($row, $prefix= 'img_')
Decode a row from the database (either object or array) to an array with timestamps and MIME types de...
Definition: LocalFile.php:485
bool $lockedOwnTrx
True if the image row is locked with a lock initiated transaction.
Definition: LocalFile.php:122
getStreamHeaders($metadata)
Get useful response headers for GET/HEAD requests for a file with the given metadata.
static addUpdate(DeferrableUpdate $update, $type=self::POSTSEND)
Add an update to the deferred list.
getThumbnails($archiveName=false)
getTransformScript inherited
Definition: LocalFile.php:860
static run($event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:131
array $ids
List of file IDs to restore.
Definition: LocalFile.php:2361
bool $dataLoaded
Whether or not core data has been loaded from the database (loadFromXxx)
Definition: LocalFile.php:77
const NS_FILE
Definition: Defines.php:75
FileRepo LocalRepo ForeignAPIRepo bool $repo
Some member variables can be lazy-initialised using __get().
Definition: File.php:95
static makeContent($text, Title $title=null, $modelId=null, $format=null)
Convenience function for creating a Content object from a given textual representation.
getMetadata()
Get handler-specific metadata.
Definition: LocalFile.php:784
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition: hooks.txt:1584
static getSha1Base36FromPath($path)
Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case encoding, zero padded to 31 digits.
Definition: FSFile.php:275
static getCacheSetOptions(IDatabase $db1)
Merge the result of getSessionLagStatus() for several DBs using the most pessimistic values to estima...
Definition: Database.php:2907
string $mime
MIME type, determined by MimeMagic::guessMimeType.
Definition: LocalFile.php:65
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
Special handling for file pages.
execute()
Perform the move.
Definition: LocalFile.php:2846
Helper class for file deletion.
Definition: LocalFile.php:1987
array $deletionBatch
Items to be processed in the deletion batch.
Definition: LocalFile.php:2001
purgeOldThumbnails($archiveName)
Delete cached transformed files for an archived version only.
Definition: LocalFile.php:912
const DELETED_TEXT
Definition: Revision.php:76
getWidth($page=1)
Return the width of the image.
Definition: LocalFile.php:697
FileRepoStatus $status
Definition: LocalFile.php:2007
removeNonexistentFiles($triplets)
Removes non-existent files from a store batch.
Definition: LocalFile.php:2660
const TS_MW
MediaWiki concatenated string timestamp (YYYYMMDDHHMMSS)
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
removeNonexistentFromCleanup($batch)
Removes non-existent files from a cleanup batch.
Definition: LocalFile.php:2686
const DELETED_USER
Definition: Revision.php:78
Class for creating log entries manually, to inject them into the database.
Definition: LogEntry.php:394
if(!defined( 'MEDIAWIKI')) $fname
This file is not a valid entry point, perform no further processing unless MEDIAWIKI is defined...
Definition: Setup.php:35
upgradeRow()
Fix assorted version-related problems with the image row by reloading it from the file...
Definition: LocalFile.php:596
Class representing a non-directory file on the file system.
Definition: FSFile.php:29
const OVERWRITE_SAME
Definition: FileRepo.php:40
static getHashFromKey($key)
Gets the SHA1 hash from a storage key.
Definition: LocalRepo.php:181
const EDIT_NEW
Definition: Defines.php:179
getCacheFields($prefix= 'img_')
Definition: LocalFile.php:335
cleanupFailedBatch($storeStatus, $storeBatch)
Cleanup a failed batch.
Definition: LocalFile.php:2730
lock()
Start a transaction and lock the image for update Increments a reference counter if the lock is alrea...
Definition: LocalFile.php:1911
getDescriptionText($lang=null)
Get the HTML text of the description page This is not used by ImagePage for local files...
Definition: LocalFile.php:1809
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content $content
Definition: hooks.txt:1004
string $url
The URL corresponding to one of the four basic zones.
Definition: File.php:116
int $deleted
Bitfield akin to rev_deleted.
Definition: LocalFile.php:83
move($target)
getLinksTo inherited
Definition: LocalFile.php:1617
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at name
Definition: design.txt:12
filterThumbnailPurgeList(&$files, $options)
Remove files from the purge list.
getUrl()
Return the URL of the file.
Definition: File.php:347
$license
bool $missing
True if file is not present in file system.
Definition: LocalFile.php:125
static getHandler($type)
Get a MediaHandler for a given MIME type from the instance cache.
const METADATA_COMPATIBLE
getDescriptionUrl()
isMultipage inherited
Definition: LocalFile.php:1797
getPageDimensions(File $image, $page)
Get an associative array of page dimensions Currently "width" and "height" are understood, but this might be expanded in the future.
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context as context the output can only depend on parameters provided to this hook not on global state indicating whether full HTML should be generated If generation of HTML may be but other information should still be present in the ParserOutput object to manipulate or replace but no entry for that model exists in $wgContentHandlers if desired whether it is OK to use $contentModel on $title Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok inclusive $limit
Definition: hooks.txt:1004
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as and the local content language as $wgContLang
Definition: design.txt:56
getDescription($audience=self::FOR_PUBLIC, User $user=null)
Definition: LocalFile.php:1828
Class to represent a local file in the wiki's own database.
Definition: LocalFile.php:46
addOlds()
Add the old versions of the image to the batch.
Definition: LocalFile.php:2796
getThumbUrl($suffix=false)
Get the URL of the thumbnail directory, or a particular file if $suffix is specified.
Definition: File.php:1693
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set $status
Definition: hooks.txt:1004
getMoveTriplets()
Generate triplets for FileRepo::storeBatch().
Definition: LocalFile.php:2967
string $timestamp
Upload timestamp.
Definition: LocalFile.php:101
loadFromDB($flags=0)
Load file metadata from the DB.
Definition: LocalFile.php:383
getLazyCacheFields($prefix= 'img_')
Definition: LocalFile.php:360
publish($src, $dstRel, $archiveRel, $flags=0, array $options=[])
Copy or move a file either from a storage path, virtual URL, or file system path, into this repositor...
Definition: FileRepo.php:1170
int $height
Image height.
Definition: LocalFile.php:56
loadFromFile()
Load metadata from the file itself.
Definition: LocalFile.php:326
const TS_UNIX
Unix time - the number of seconds since 1970-01-01 00:00:00 UTC.
serialize()
Definition: ApiMessage.php:94
const DELETED_COMMENT
Definition: Revision.php:77
string $description
Description of current revision of the file.
Definition: LocalFile.php:110
load($flags=0)
Load file metadata from cache or DB, unless already loaded.
Definition: LocalFile.php:539
purgeCache($options=[])
Delete all previously generated thumbnails, refresh metadata in memcached and purge the CDN...
Definition: LocalFile.php:894
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition: File.php:50
isGood()
Returns whether the operation completed and didn't have any error or warnings.
Definition: Status.php:132
bool $suppress
Whether to suppress all suppressable fields when deleting.
Definition: LocalFile.php:2004
getArchiveUrl($suffix=false)
Get the URL of the archive directory, or a particular file if $suffix is specified.
Definition: File.php:1635
getMediaType()
Returns the type of the media in the file.
Definition: LocalFile.php:823
loadFromCache()
Try to load file metadata from memcached.
Definition: LocalFile.php:245
int $width
Image width.
Definition: LocalFile.php:53
purgeThumbnails($options=[])
Delete cached transformed files for the current version only.
Definition: LocalFile.php:935
getMimeType()
Returns the MIME type of the file.
Definition: LocalFile.php:812
resetHistory()
Reset the history pointer to the first element of the history.
Definition: LocalFile.php:1091
static getPropsFromPath($path, $ext=true)
Get an associative array containing information about a file in the local filesystem.
Definition: FSFile.php:259
int $size
Size in bytes (loadFromXxx)
Definition: LocalFile.php:68
hasSha1Storage()
Returns whether or not storage is SHA-1 based.
Definition: FileRepo.php:1911
addOlds()
Add the old versions of the image to the batch.
Definition: LocalFile.php:2047
do that in ParserLimitReportFormat instead use this to modify the parameters of the image and a DIV can begin in one section and end in another Make sure your code can handle that case gracefully See the EditSectionClearerLink extension for an example zero but section is usually empty its values are the globals values before the output is cached one of or reset my talk my contributions etc etc otherwise the built in rate limiting checks are if enabled allows for interception of redirect as a string mapping parameter names to values & $type
Definition: hooks.txt:2338
purgeDescription()
Purge the file description page, but don't go after pages using the file.
Definition: File.php:1429
static & makeTitle($ns, $title, $fragment= '', $interwiki= '')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:524
$wgUpdateCompatibleMetadata
If to automatically update the img_metadata field if the metadata field is outdated but compatible wi...
addIds($ids)
Add a whole lot of files by ID.
Definition: LocalFile.php:2392
static newGood($value=null)
Factory function for good results.
Definition: Status.php:101
do that in ParserLimitReportFormat instead use this to modify the parameters of the image and a DIV can begin in one section and end in another Make sure your code can handle that case gracefully See the EditSectionClearerLink extension for an example zero but section is usually empty its values are the globals values before the output is cached $page
Definition: hooks.txt:2338
saveToCache()
Save the file metadata to memcached.
Definition: LocalFile.php:276
int $historyLine
Number of line to return by nextHistoryLine() (constructor)
Definition: LocalFile.php:89
execute()
Run the transaction, except the cleanup batch.
Definition: LocalFile.php:2411
$wgUser
Definition: Setup.php:794
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:310
addId($fa_id)
Add a file by ID.
Definition: LocalFile.php:2384