23use HTMLCacheUpdateJob;
24use InvalidArgumentException;
56use UnexpectedValueException;
94 private const VERSION = 13;
96 private const CACHE_FIELD_MAX_LEN = 1000;
99 private const MDS_EMPTY =
'empty';
102 private const MDS_LEGACY =
'legacy';
105 private const MDS_PHP =
'php';
108 private const MDS_JSON =
'json';
111 private const MAX_PAGE_RENDER_JOBS = 50;
184 private $historyLine = 0;
187 private $historyRes =
null;
202 private $description;
205 private $descriptionTouched;
217 private $lockedOwnTrx;
223 private $metadataStorageHelper;
229 private const LOAD_ALL = 16;
231 private const ATOMIC_SECTION_LOCK =
'LocalFile::lockingTransaction';
265 $file->loadFromRow( $row );
282 $dbr =
$repo->getReplicaDB();
285 $queryBuilder->where( [
'img_sha1' =>
$sha1 ] );
288 $queryBuilder->andWhere( [
'img_timestamp' => $dbr->
timestamp( $timestamp ) ] );
291 $row = $queryBuilder->caller( __METHOD__ )->fetchRow();
293 return static::newFromRow( $row,
$repo );
325 'tables' => $queryInfo[
'tables'],
326 'fields' => $queryInfo[
'fields'],
327 'joins' => $queryInfo[
'join_conds'],
363 return $this->repo->getSharedCacheKey(
'file', sha1( $this->
getName() ) );
369 private function loadFromCache() {
370 $this->dataLoaded =
false;
371 $this->extraDataLoaded =
false;
375 $this->
loadFromDB( IDBAccessObject::READ_NORMAL );
381 $cachedValues = $cache->getWithSetCallback(
384 function ( $oldValue, &$ttl, array &$setOpts ) use ( $cache ) {
387 $this->
loadFromDB( IDBAccessObject::READ_NORMAL );
392 if ( $this->fileExists ) {
393 foreach ( $fields as $field ) {
394 $cacheVal[$field] = $this->$field;
398 $cacheVal[
'user'] = $this->user->getId();
399 $cacheVal[
'user_text'] = $this->user->getName();
403 if ( $this->metadataBlobs ) {
404 $cacheVal[
'metadata'] = array_diff_key(
405 $this->metadataArray, $this->metadataBlobs );
416 if ( isset( $cacheVal[$field] )
417 && strlen( serialize( $cacheVal[$field] ) ) > 100 * 1024
419 unset( $cacheVal[$field] );
420 if ( $field ===
'metadata' ) {
421 unset( $cacheVal[
'metadataBlobs'] );
426 if ( $this->fileExists ) {
427 $ttl = $cache->adaptiveTTL( (
int)
wfTimestamp( TS_UNIX, $this->timestamp ), $ttl );
429 $ttl = $cache::TTL_DAY;
434 [
'version' => self::VERSION ]
437 $this->fileExists = $cachedValues[
'fileExists'];
438 if ( $this->fileExists ) {
442 $this->dataLoaded =
true;
443 $this->extraDataLoaded =
true;
445 $this->extraDataLoaded = $this->extraDataLoaded && isset( $cachedValues[$field] );
458 $this->repo->getPrimaryDB()->onTransactionPreCommitOrIdle(
459 static function () use ( $key ) {
486 if ( $prefix !==
'' ) {
487 throw new InvalidArgumentException(
488 __METHOD__ .
' with a non-empty prefix is no longer supported.'
496 return [
'size',
'width',
'height',
'bits',
'media_type',
497 'major_mime',
'minor_mime',
'timestamp',
'sha1',
'description' ];
508 if ( $prefix !==
'' ) {
509 throw new InvalidArgumentException(
510 __METHOD__ .
' with a non-empty prefix is no longer supported.'
515 return [
'metadata' ];
524 $fname = static::class .
'::' . __FUNCTION__;
526 # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
527 $this->dataLoaded =
true;
528 $this->extraDataLoaded =
true;
530 $dbr = ( $flags & IDBAccessObject::READ_LATEST )
531 ? $this->repo->getPrimaryDB()
532 : $this->repo->getReplicaDB();
535 $queryBuilder->where( [
'img_name' => $this->
getName() ] );
536 $row = $queryBuilder->caller( $fname )->fetchRow();
541 $this->fileExists =
false;
551 if ( !$this->title ) {
555 $fname = static::class .
'::' . __FUNCTION__;
557 # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
558 $this->extraDataLoaded =
true;
560 $db = $this->repo->getReplicaDB();
561 $fieldMap = $this->loadExtraFieldsWithTimestamp( $db, $fname );
563 $db = $this->repo->getPrimaryDB();
564 $fieldMap = $this->loadExtraFieldsWithTimestamp( $db, $fname );
568 if ( isset( $fieldMap[
'metadata'] ) ) {
572 throw new RuntimeException(
"Could not find data for image '{$this->getName()}'." );
581 private function loadExtraFieldsWithTimestamp(
IReadableDatabase $dbr, $fname ) {
585 $queryBuilder->where( [
'img_name' => $this->
getName() ] )
586 ->andWhere( [
'img_timestamp' => $dbr->
timestamp( $this->getTimestamp() ) ] );
587 $row = $queryBuilder->caller( $fname )->fetchRow();
591 # File may have been uploaded over in the meantime; check the old versions
593 $row = $queryBuilder->where( [
'oi_name' => $this->
getName() ] )
594 ->andWhere( [
'oi_timestamp' => $dbr->
timestamp( $this->getTimestamp() ) ] )
595 ->caller( __METHOD__ )->fetchRow();
610 $array = (array)$row;
611 $prefixLength = strlen( $prefix );
614 if ( substr( array_key_first( $array ), 0, $prefixLength ) !== $prefix ) {
615 throw new InvalidArgumentException( __METHOD__ .
': incorrect $prefix parameter' );
619 foreach ( $array as
$name => $value ) {
620 $decoded[substr(
$name, $prefixLength )] = $value;
642 $this->dataLoaded =
true;
646 $this->name = $unprefixed[
'name'];
647 $this->media_type = $unprefixed[
'media_type'];
650 $this->description = $services->getCommentStore()
651 ->getComment(
"{$prefix}description", $row )->text;
653 $this->user = $services->getUserFactory()->newFromAnyId(
654 $unprefixed[
'user'] ??
null,
655 $unprefixed[
'user_text'] ??
null,
656 $unprefixed[
'actor'] ??
null
659 $this->timestamp =
wfTimestamp( TS_MW, $unprefixed[
'timestamp'] );
662 $this->repo->getReplicaDB(), $unprefixed[
'metadata'] );
664 if ( empty( $unprefixed[
'major_mime'] ) ) {
665 $this->major_mime =
'unknown';
666 $this->minor_mime =
'unknown';
667 $this->mime =
'unknown/unknown';
669 if ( !$unprefixed[
'minor_mime'] ) {
670 $unprefixed[
'minor_mime'] =
'unknown';
672 $this->major_mime = $unprefixed[
'major_mime'];
673 $this->minor_mime = $unprefixed[
'minor_mime'];
674 $this->mime = $unprefixed[
'major_mime'] .
'/' . $unprefixed[
'minor_mime'];
678 $this->sha1 = rtrim( $unprefixed[
'sha1'],
"\0" );
684 $this->size = +$unprefixed[
'size'];
685 $this->width = +$unprefixed[
'width'];
686 $this->height = +$unprefixed[
'height'];
687 $this->bits = +$unprefixed[
'bits'];
690 $extraFields = array_diff(
691 array_keys( $unprefixed ),
693 'name',
'media_type',
'description_text',
'description_data',
694 'description_cid',
'user',
'user_text',
'actor',
'timestamp',
695 'metadata',
'major_mime',
'minor_mime',
'sha1',
'size',
'width',
696 'height',
'bits',
'file_id',
'filerevision_id'
699 if ( $extraFields ) {
701 'Passing extra fields (' .
702 implode(
', ', $extraFields )
703 .
') to ' . __METHOD__ .
' was deprecated in MediaWiki 1.37. ' .
704 'Property assignment will be removed in a later version.',
706 foreach ( $extraFields as $field ) {
707 $this->$field = $unprefixed[$field];
711 $this->fileExists =
true;
719 public function load( $flags = 0 ) {
720 if ( !$this->dataLoaded ) {
721 if ( $flags & IDBAccessObject::READ_LATEST ) {
724 $this->loadFromCache();
728 if ( ( $flags & self::LOAD_ALL ) && !$this->extraDataLoaded ) {
744 $reserialize =
false;
745 if ( $this->media_type ===
null || $this->mime ==
'image/svg' ) {
751 if ( $validity === MediaHandler::METADATA_BAD ) {
753 } elseif ( $validity === MediaHandler::METADATA_COMPATIBLE
754 && $this->repo->isMetadataUpdateEnabled()
757 } elseif ( $this->repo->isJsonMetadataEnabled()
758 && $this->repo->isMetadataReserializeEnabled()
760 if ( $this->repo->isSplitMetadataEnabled() && $this->isMetadataOversize() ) {
762 } elseif ( $this->metadataSerializationFormat !== self::MDS_EMPTY &&
763 $this->metadataSerializationFormat !== self::MDS_JSON ) {
770 if ( $upgrade || $reserialize ) {
771 $this->upgrading =
true;
773 DeferredUpdates::addCallableUpdate(
function () use ( $upgrade ) {
774 $this->upgrading =
false;
792 return $this->upgraded;
802 if ( !$this->fileId ) {
803 $dbw = $this->repo->getPrimaryDB();
804 $id = $dbw->newSelectQueryBuilder()
805 ->select(
'file_id' )
808 'file_name' => $this->
getName(),
811 ->caller( __METHOD__ )
816 return $this->fileId;
826 $dbw = $this->repo->getPrimaryDB();
831 $id = $dbw->newSelectQueryBuilder()
832 ->select(
'file_id' )
835 'file_name' => $this->
getName(),
837 ->caller( __METHOD__ )
840 $dbw->newInsertQueryBuilder()
841 ->insertInto(
'file' )
843 'file_name' => $this->
getName(),
849 ->caller( __METHOD__ )->execute();
850 $insertId = $dbw->insertId();
852 throw new RuntimeException(
'File entry could not be inserted' );
857 $dbw->newUpdateQueryBuilder()
859 ->set( [
'file_deleted' => 0 ] )
860 ->where( [
'file_id' => $id ] )
861 ->caller( __METHOD__ )->execute();
867 if ( $this->fileTypeId ) {
868 return $this->fileTypeId;
871 $dbw = $this->repo->getPrimaryDB();
872 $id = $dbw->newSelectQueryBuilder()
874 ->from(
'filetypes' )
877 'ft_major_mime' => $major,
878 'ft_minor_mime' => $minor,
880 ->caller( __METHOD__ )
883 $this->fileTypeId = $id;
886 $dbw->newInsertQueryBuilder()
887 ->insertInto(
'filetypes' )
890 'ft_major_mime' => $major,
891 'ft_minor_mime' => $minor,
893 ->caller( __METHOD__ )->execute();
895 $id = $dbw->insertId();
897 throw new RuntimeException(
'File entry could not be inserted' );
900 $this->fileTypeId = $id;
909 $dbw = $this->repo->getPrimaryDB();
913 $freshnessCondition = [
'img_timestamp' => $dbw->timestamp( $this->
getTimestamp() ) ];
917 # Don't destroy file info of missing files
918 if ( !$this->fileExists ) {
919 wfDebug( __METHOD__ .
": file does not exist, aborting" );
926 wfDebug( __METHOD__ .
': upgrading ' . $this->
getName() .
" to the current schema" );
929 $dbw->newUpdateQueryBuilder()
932 'img_size' => $this->size,
933 'img_width' => $this->width,
934 'img_height' => $this->height,
935 'img_bits' => $this->bits,
936 'img_media_type' => $this->media_type,
937 'img_major_mime' => $major,
938 'img_minor_mime' => $minor,
939 'img_metadata' => $metadata,
940 'img_sha1' => $this->sha1,
942 ->where( [
'img_name' => $this->
getName() ] )
943 ->andWhere( $freshnessCondition )
944 ->caller( __METHOD__ )->execute();
947 $dbw->newUpdateQueryBuilder()
948 ->update(
'filerevision' )
950 'fr_size' => $this->size,
951 'fr_width' => $this->width,
952 'fr_height' => $this->height,
953 'fr_bits' => $this->bits,
954 'fr_metadata' => $metadata,
955 'fr_sha1' => $this->sha1,
958 ->andWhere( [
'fr_timestamp' => $dbw->timestamp( $this->getTimestamp() ) ] )
959 ->caller( __METHOD__ )->execute();
964 $this->upgraded =
true;
975 $dbw = $this->repo->getPrimaryDB();
977 $dbw->newUpdateQueryBuilder()
979 ->set( [
'img_metadata' => $metadata ] )
981 'img_name' => $this->name,
982 'img_timestamp' => $dbw->timestamp( $this->timestamp ),
984 ->caller( __METHOD__ )->execute();
986 $dbw->newUpdateQueryBuilder()
987 ->update(
'filerevision' )
988 ->set( [
'fr_metadata' => $metadata ] )
990 ->andWhere( [
'fr_timestamp' => $dbw->timestamp( $this->getTimestamp() ) ] )
991 ->caller( __METHOD__ )->execute();
993 $this->upgraded =
true;
1009 $this->dataLoaded =
true;
1011 $fields[] =
'fileExists';
1013 foreach ( $fields as $field ) {
1014 if ( isset( $info[$field] ) ) {
1015 $this->$field = $info[$field];
1020 if ( isset( $info[
'user'] ) &&
1021 isset( $info[
'user_text'] ) &&
1022 $info[
'user_text'] !==
''
1028 if ( isset( $info[
'major_mime'] ) ) {
1029 $this->mime =
"{$info['major_mime']}/{$info['minor_mime']}";
1030 } elseif ( isset( $info[
'mime'] ) ) {
1031 $this->mime = $info[
'mime'];
1032 [ $this->major_mime, $this->minor_mime ] =
self::splitMime( $this->mime );
1035 if ( isset( $info[
'metadata'] ) ) {
1036 if ( is_string( $info[
'metadata'] ) ) {
1038 } elseif ( is_array( $info[
'metadata'] ) ) {
1039 $this->metadataArray = $info[
'metadata'];
1040 if ( isset( $info[
'metadataBlobs'] ) ) {
1041 $this->metadataBlobs = $info[
'metadataBlobs'];
1042 $this->unloadedMetadataBlobs = array_diff_key(
1043 $this->metadataBlobs,
1044 $this->metadataArray
1047 $this->metadataBlobs = [];
1048 $this->unloadedMetadataBlobs = [];
1051 $logger = LoggerFactory::getInstance(
'LocalFile' );
1052 $logger->warning( __METHOD__ .
' given invalid metadata of type ' .
1053 get_debug_type( $info[
'metadata'] ) );
1054 $this->metadataArray = [];
1056 $this->extraDataLoaded =
true;
1076 if ( $this->missing ===
null ) {
1081 return $this->missing;
1106 return $dim[
'width'];
1139 return $dim[
'height'];
1158 if ( !$this->title ) {
1162 $pageId = $this->title->getArticleID();
1165 $url = $this->repo->makeUrl( [
'curid' => $pageId ] );
1166 if (
$url !==
false ) {
1183 } elseif ( array_keys( $data ) === [
'_error' ] ) {
1185 return $data[
'_error'];
1198 $this->
load( self::LOAD_ALL );
1199 if ( $this->unloadedMetadataBlobs ) {
1201 array_unique( array_merge(
1202 array_keys( $this->metadataArray ),
1203 array_keys( $this->unloadedMetadataBlobs )
1211 $this->load( self::LOAD_ALL );
1214 foreach ( $itemNames as $itemName ) {
1215 if ( array_key_exists( $itemName, $this->metadataArray ) ) {
1216 $result[$itemName] = $this->metadataArray[$itemName];
1217 } elseif ( isset( $this->unloadedMetadataBlobs[$itemName] ) ) {
1218 $addresses[$itemName] = $this->unloadedMetadataBlobs[$itemName];
1223 $resultFromBlob = $this->metadataStorageHelper->getMetadataFromBlobStore( $addresses );
1224 foreach ( $addresses as $itemName => $address ) {
1225 unset( $this->unloadedMetadataBlobs[$itemName] );
1226 $value = $resultFromBlob[$itemName] ??
null;
1227 if ( $value !==
null ) {
1228 $result[$itemName] = $value;
1229 $this->metadataArray[$itemName] = $value;
1248 $this->load( self::LOAD_ALL );
1249 if ( !$this->metadataArray && !$this->metadataBlobs ) {
1251 } elseif ( $this->repo->isJsonMetadataEnabled() ) {
1252 $s = $this->getJsonMetadata();
1254 $s = serialize( $this->getMetadataArray() );
1256 if ( !is_string( $s ) ) {
1257 throw new RuntimeException(
'Could not serialize image metadata value for DB' );
1268 private function getJsonMetadata() {
1271 'data' => array_diff_key( $this->metadataArray, $this->metadataBlobs )
1275 if ( $this->metadataBlobs ) {
1276 $envelope[
'blobs'] = $this->metadataBlobs;
1279 [ $s, $blobAddresses ] = $this->metadataStorageHelper->getJsonMetadata( $this, $envelope );
1282 $this->metadataBlobs += $blobAddresses;
1293 private function isMetadataOversize() {
1294 if ( !$this->repo->isSplitMetadataEnabled() ) {
1297 $threshold = $this->repo->getSplitMetadataThreshold();
1298 $directItems = array_diff_key( $this->metadataArray, $this->metadataBlobs );
1299 foreach ( $directItems as $value ) {
1300 if ( strlen( $this->metadataStorageHelper->jsonEncode( $value ) ) > $threshold ) {
1316 $this->loadMetadataFromString( $db->
decodeBlob( $metadataBlob ) );
1327 $this->extraDataLoaded =
true;
1328 $this->metadataArray = [];
1329 $this->metadataBlobs = [];
1330 $this->unloadedMetadataBlobs = [];
1331 $metadataString = (string)$metadataString;
1332 if ( $metadataString ===
'' ) {
1333 $this->metadataSerializationFormat = self::MDS_EMPTY;
1336 if ( $metadataString[0] ===
'{' ) {
1337 $envelope = $this->metadataStorageHelper->jsonDecode( $metadataString );
1340 $this->metadataArray = [
'_error' => $metadataString ];
1341 $this->metadataSerializationFormat = self::MDS_LEGACY;
1343 $this->metadataSerializationFormat = self::MDS_JSON;
1344 if ( isset( $envelope[
'data'] ) ) {
1345 $this->metadataArray = $envelope[
'data'];
1347 if ( isset( $envelope[
'blobs'] ) ) {
1348 $this->metadataBlobs = $this->unloadedMetadataBlobs = $envelope[
'blobs'];
1353 $data = @unserialize( $metadataString );
1354 if ( !is_array( $data ) ) {
1356 $data = [
'_error' => $metadataString ];
1357 $this->metadataSerializationFormat = self::MDS_LEGACY;
1359 $this->metadataSerializationFormat = self::MDS_PHP;
1361 $this->metadataArray = $data;
1372 return (
int)$this->bits;
1406 return $this->media_type;
1423 return $this->fileExists;
1448 if ( $archiveName ) {
1449 $dir = $this->getArchiveThumbPath( $archiveName );
1451 $dir = $this->getThumbPath();
1454 $backend = $this->repo->getBackend();
1457 $iterator = $backend->getFileList( [
'dir' => $dir,
'forWrite' =>
true ] );
1458 if ( $iterator !==
null ) {
1459 foreach ( $iterator as $file ) {
1479 $this->maybeUpgradeRow();
1480 $this->invalidateCache();
1483 $this->purgeThumbnails( $options );
1486 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1489 !empty( $options[
'forThumbRefresh'] )
1490 ? $hcu::PURGE_PRESEND
1491 : $hcu::PURGE_INTENT_TXROUND_REFLECTED
1502 $thumbs = $this->getThumbnails( $archiveName );
1505 $dir = array_shift( $thumbs );
1506 $this->purgeThumbList( $dir, $thumbs );
1509 foreach ( $thumbs as $thumb ) {
1510 $urls[] = $this->getArchiveThumbUrl( $archiveName, $thumb );
1514 $this->getHookRunner()->onLocalFilePurgeThumbnails( $this, $archiveName, $urls );
1517 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1518 $hcu->purgeUrls( $urls, $hcu::PURGE_PRESEND );
1528 $thumbs = $this->getThumbnails();
1531 $dir = array_shift( $thumbs );
1532 $this->purgeThumbList( $dir, $thumbs );
1536 foreach ( $thumbs as $thumb ) {
1537 $urls[] = $this->getThumbUrl( $thumb );
1541 if ( !empty( $options[
'forThumbRefresh'] ) ) {
1542 $handler = $this->getHandler();
1544 $handler->filterThumbnailPurgeList( $thumbs, $options );
1549 $this->getHookRunner()->onLocalFilePurgeThumbnails( $this,
false, $urls );
1552 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1555 !empty( $options[
'forThumbRefresh'] )
1556 ? $hcu::PURGE_PRESEND
1557 : $hcu::PURGE_INTENT_TXROUND_REFLECTED
1568 $uploadThumbnailRenderMap = MediaWikiServices::getInstance()
1569 ->getMainConfig()->get( MainConfigNames::UploadThumbnailRenderMap );
1573 $sizes = $uploadThumbnailRenderMap;
1576 foreach ( $sizes as $size ) {
1577 if ( $this->isMultipage() ) {
1580 $pageLimit = min( $this->pageCount(), self::MAX_PAGE_RENDER_JOBS );
1585 'transformParams' => [
'width' => $size,
'page' => 1 ],
1586 'enqueueNextPage' =>
true,
1587 'pageLimit' => $pageLimit
1590 } elseif ( $this->isVectorized() || $this->getWidth() > $size ) {
1593 [
'transformParams' => [
'width' => $size ] ]
1599 MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $jobs );
1610 $fileListDebug = strtr(
1611 var_export( $files,
true ),
1614 wfDebug( __METHOD__ .
": $fileListDebug" );
1616 if ( $this->repo->supportsSha1URLs() ) {
1617 $reference = $this->getSha1();
1619 $reference = $this->getName();
1623 foreach ( $files as $file ) {
1624 # Check that the reference (filename or sha1) is part of the thumb name
1625 # This is a basic check to avoid erasing unrelated directories
1626 if ( str_contains( $file, $reference )
1627 || str_contains( $file,
"-thumbnail" )
1629 $purgeList[] =
"{$dir}/{$file}";
1633 # Delete the thumbnails
1634 $this->repo->quickPurgeBatch( $purgeList );
1635 # Clear out the thumbnail directory if empty
1636 $this->repo->quickCleanDir( $dir );
1650 public function getHistory( $limit =
null, $start =
null, $end =
null, $inc =
true ) {
1651 if ( !$this->exists() ) {
1655 $dbr = $this->repo->getReplicaDB();
1656 $oldFileQuery = FileSelectQueryBuilder::newForOldFile( $dbr )->getQueryInfo();
1658 $tables = $oldFileQuery[
'tables'];
1659 $fields = $oldFileQuery[
'fields'];
1660 $join_conds = $oldFileQuery[
'join_conds'];
1661 $conds = $opts = [];
1662 $eq = $inc ?
'=' :
'';
1663 $conds[] = $dbr->
expr(
'oi_name',
'=', $this->title->getDBkey() );
1666 $conds[] = $dbr->
expr(
'oi_timestamp',
"<$eq", $dbr->
timestamp( $start ) );
1670 $conds[] = $dbr->
expr(
'oi_timestamp',
">$eq", $dbr->
timestamp( $end ) );
1674 $opts[
'LIMIT'] = $limit;
1678 $order = ( !$start && $end !== null ) ?
'ASC' :
'DESC';
1679 $opts[
'ORDER BY'] =
"oi_timestamp $order";
1680 $opts[
'USE INDEX'] = [
'oldimage' =>
'oi_name_timestamp' ];
1682 $this->getHookRunner()->onLocalFile__getHistory( $this, $tables, $fields,
1683 $conds, $opts, $join_conds );
1689 ->caller( __METHOD__ )
1691 ->joinConds( $join_conds )
1695 foreach ( $res as $row ) {
1696 $r[] = $this->repo->newFileFromRow( $row );
1699 if ( $order ==
'ASC' ) {
1700 $r = array_reverse( $r );
1717 if ( !$this->exists() ) {
1721 # Polymorphic function name to distinguish foreign and local fetches
1722 $fname = static::class .
'::' . __FUNCTION__;
1724 $dbr = $this->repo->getReplicaDB();
1726 if ( $this->historyLine == 0 ) {
1727 $queryBuilder = FileSelectQueryBuilder::newForFile( $dbr );
1729 $queryBuilder->fields( [
'oi_archive_name' => $dbr->
addQuotes(
'' ),
'oi_deleted' =>
'0' ] )
1730 ->where( [
'img_name' => $this->title->getDBkey() ] );
1731 $this->historyRes = $queryBuilder->caller( $fname )->fetchResultSet();
1733 if ( $this->historyRes->numRows() == 0 ) {
1734 $this->historyRes =
null;
1738 } elseif ( $this->historyLine == 1 ) {
1739 $queryBuilder = FileSelectQueryBuilder::newForOldFile( $dbr );
1741 $this->historyRes = $queryBuilder->where( [
'oi_name' => $this->title->getDBkey() ] )
1742 ->orderBy(
'oi_timestamp', SelectQueryBuilder::SORT_DESC )
1743 ->caller( $fname )->fetchResultSet();
1745 $this->historyLine++;
1747 return $this->historyRes->fetchObject();
1755 $this->historyLine = 0;
1757 if ( $this->historyRes !==
null ) {
1758 $this->historyRes =
null;
1795 public function upload( $src, $comment, $pageText, $flags = 0, $props =
false,
1796 $timestamp =
false, ?
Authority $uploader =
null, $tags = [],
1797 $createNullRevision =
true, $revert =
false
1799 if ( $this->getRepo()->getReadOnlyReason() !==
false ) {
1800 return $this->readOnlyFatalStatus();
1801 } elseif ( MediaWikiServices::getInstance()->getRevisionStore()->isReadOnly() ) {
1804 return $this->readOnlyFatalStatus();
1807 $srcPath = ( $src instanceof
FSFile ) ? $src->getPath() : $src;
1809 if ( FileRepo::isVirtualUrl( $srcPath )
1810 || FileBackend::isStoragePath( $srcPath )
1812 $props = $this->repo->getFileProps( $srcPath );
1814 $mwProps =
new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
1815 $props = $mwProps->getPropsFromPath( $srcPath,
true );
1820 $handler = MediaHandler::getHandler( $props[
'mime'] );
1822 if ( is_string( $props[
'metadata'] ) ) {
1827 $metadata = @unserialize( $props[
'metadata'] );
1829 $metadata = $props[
'metadata'];
1832 if ( is_array( $metadata ) ) {
1833 $options[
'headers'] = $handler->getContentHeaders( $metadata );
1836 $options[
'headers'] = [];
1840 $comment = trim( $comment );
1842 $status = $this->publish( $src, $flags, $options );
1844 if ( $status->successCount >= 2 ) {
1851 $oldver = $status->value;
1853 $uploadStatus = $this->recordUpload3(
1857 $uploader ?? RequestContext::getMain()->getAuthority(),
1861 $createNullRevision,
1864 if ( !$uploadStatus->isOK() ) {
1865 if ( $uploadStatus->hasMessage(
'filenotfound' ) ) {
1867 $status->fatal(
'filenotfound', $srcPath );
1869 $status->merge( $uploadStatus );
1901 bool $createNullRevision =
true,
1902 bool $revert =
false
1904 $dbw = $this->repo->getPrimaryDB();
1906 # Imports or such might force a certain timestamp; otherwise we generate
1907 # it and can fudge it slightly to keep (name,timestamp) unique on re-upload.
1908 if ( $timestamp ===
false ) {
1909 $timestamp = $dbw->timestamp();
1910 $allowTimeKludge =
true;
1912 $allowTimeKludge =
false;
1915 $props = $props ?: $this->repo->getFileProps( $this->getVirtualUrl() );
1916 $props[
'description'] = $comment;
1917 $props[
'timestamp'] =
wfTimestamp( TS_MW, $timestamp );
1918 $this->setProps( $props );
1920 # Fail now if the file isn't there
1921 if ( !$this->fileExists ) {
1922 wfDebug( __METHOD__ .
": File " . $this->getRel() .
" went missing!" );
1924 return Status::newFatal(
'filenotfound', $this->getRel() );
1927 $mimeAnalyzer = MediaWikiServices::getInstance()->getMimeAnalyzer();
1928 if ( !$mimeAnalyzer->isValidMajorMimeType( $this->major_mime ) ) {
1929 $this->major_mime =
'unknown';
1932 $actorNormalizaton = MediaWikiServices::getInstance()->getActorNormalization();
1934 $dbw->startAtomic( __METHOD__ );
1936 $actorId = $actorNormalizaton->acquireActorId( $performer->
getUser(), $dbw );
1937 $this->user = $performer->
getUser();
1939 # Test to see if the row exists using INSERT IGNORE
1940 # This avoids race conditions by locking the row until the commit, and also
1941 # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
1942 $commentStore = MediaWikiServices::getInstance()->getCommentStore();
1943 $commentFields = $commentStore->insert( $dbw,
'img_description', $comment );
1944 $actorFields = [
'img_actor' => $actorId ];
1945 $dbw->newInsertQueryBuilder()
1946 ->insertInto(
'image' )
1949 'img_name' => $this->getName(),
1950 'img_size' => $this->size,
1951 'img_width' => intval( $this->width ),
1952 'img_height' => intval( $this->height ),
1953 'img_bits' => $this->bits,
1954 'img_media_type' => $this->media_type,
1955 'img_major_mime' => $this->major_mime,
1956 'img_minor_mime' => $this->minor_mime,
1957 'img_timestamp' => $dbw->timestamp( $timestamp ),
1958 'img_metadata' => $this->getMetadataForDb( $dbw ),
1959 'img_sha1' => $this->sha1
1960 ] + $commentFields + $actorFields )
1961 ->caller( __METHOD__ )->execute();
1962 $reupload = ( $dbw->affectedRows() == 0 );
1964 $latestFileRevId =
null;
1967 $latestFileRevId = $dbw->newSelectQueryBuilder()
1969 ->from(
'filerevision' )
1970 ->where( [
'fr_file' => $this->acquireFileIdFromName() ] )
1971 ->orderBy(
'fr_timestamp',
'DESC' )
1972 ->caller( __METHOD__ )
1975 $commentFieldsNew = $commentStore->insert( $dbw,
'fr_description', $comment );
1976 $dbw->newInsertQueryBuilder()
1977 ->insertInto(
'filerevision' )
1979 'fr_file' => $this->acquireFileIdFromName(),
1980 'fr_size' => $this->size,
1981 'fr_width' => intval( $this->width ),
1982 'fr_height' => intval( $this->height ),
1983 'fr_bits' => $this->bits,
1984 'fr_actor' => $actorId,
1986 'fr_timestamp' => $dbw->timestamp( $timestamp ),
1987 'fr_metadata' => $this->getMetadataForDb( $dbw ),
1988 'fr_sha1' => $this->sha1
1989 ] + $commentFieldsNew )
1990 ->caller( __METHOD__ )->execute();
1991 $dbw->newUpdateQueryBuilder()
1993 ->set( [
'file_latest' => $dbw->insertId() ] )
1994 ->where( [
'file_id' => $this->getFileIdFromName() ] )
1995 ->caller( __METHOD__ )->execute();
1999 $row = $dbw->newSelectQueryBuilder()
2000 ->select( [
'img_timestamp',
'img_sha1' ] )
2002 ->where( [
'img_name' => $this->
getName() ] )
2003 ->caller( __METHOD__ )->fetchRow();
2005 if ( $row && $row->img_sha1 === $this->sha1 ) {
2006 $dbw->endAtomic( __METHOD__ );
2007 wfDebug( __METHOD__ .
": File " . $this->getRel() .
" already exists!" );
2009 return Status::newFatal(
'fileexists-no-change', $title->getPrefixedText() );
2012 if ( $allowTimeKludge ) {
2013 # Use LOCK IN SHARE MODE to ignore any transaction snapshotting
2014 $lUnixtime = $row ? (int)
wfTimestamp( TS_UNIX, $row->img_timestamp ) : false;
2015 # Avoid a timestamp that is not newer than the last version
2016 # TODO: the image/oldimage tables should be like page/revision with an ID field
2017 if ( $lUnixtime && (
int)
wfTimestamp( TS_UNIX, $timestamp ) <= $lUnixtime ) {
2019 $timestamp = $dbw->timestamp( $lUnixtime + 1 );
2020 $this->timestamp =
wfTimestamp( TS_MW, $timestamp );
2024 $tables = [
'image' ];
2026 'oi_name' =>
'img_name',
2027 'oi_archive_name' => $dbw->addQuotes( $oldver ),
2028 'oi_size' =>
'img_size',
2029 'oi_width' =>
'img_width',
2030 'oi_height' =>
'img_height',
2031 'oi_bits' =>
'img_bits',
2032 'oi_description_id' =>
'img_description_id',
2033 'oi_timestamp' =>
'img_timestamp',
2034 'oi_metadata' =>
'img_metadata',
2035 'oi_media_type' =>
'img_media_type',
2036 'oi_major_mime' =>
'img_major_mime',
2037 'oi_minor_mime' =>
'img_minor_mime',
2038 'oi_sha1' =>
'img_sha1',
2039 'oi_actor' =>
'img_actor',
2043 $dbw->newUpdateQueryBuilder()
2044 ->update(
'filerevision' )
2045 ->set( [
'fr_archive_name' => $oldver ] )
2046 ->where( [
'fr_id' => $latestFileRevId ] )
2047 ->caller( __METHOD__ )->execute();
2051 # (T36993) Note: $oldver can be empty here, if the previous
2052 # version of the file was broken. Allow registration of the new
2053 # version to continue anyway, because that's better than having
2054 # an image that's not fixable by user operations.
2055 # Collision, this is an update of a file
2056 # Insert previous contents into oldimage
2057 $dbw->insertSelect(
'oldimage', $tables, $fields,
2058 [
'img_name' => $this->
getName() ], __METHOD__, [], [], $joins );
2060 # Update the current image row
2061 $dbw->newUpdateQueryBuilder()
2064 'img_size' => $this->size,
2065 'img_width' => intval( $this->width ),
2066 'img_height' => intval( $this->height ),
2067 'img_bits' => $this->bits,
2068 'img_media_type' => $this->media_type,
2069 'img_major_mime' => $this->major_mime,
2070 'img_minor_mime' => $this->minor_mime,
2071 'img_timestamp' => $dbw->timestamp( $timestamp ),
2072 'img_metadata' => $this->getMetadataForDb( $dbw ),
2073 'img_sha1' => $this->sha1
2074 ] + $commentFields + $actorFields )
2075 ->where( [
'img_name' => $this->
getName() ] )
2076 ->caller( __METHOD__ )->execute();
2080 $descId = $descTitle->getArticleID();
2081 $wikiPage = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $descTitle );
2082 if ( !$wikiPage instanceof WikiFilePage ) {
2083 throw new UnexpectedValueException(
'Cannot obtain instance of WikiFilePage for ' . $this->
getName()
2084 .
', got instance of ' . get_class( $wikiPage ) );
2086 $wikiPage->setFile( $this );
2090 $logAction =
'revert';
2091 } elseif ( $reupload ) {
2092 $logAction =
'overwrite';
2094 $logAction =
'upload';
2097 $logEntry =
new ManualLogEntry(
'upload', $logAction );
2098 $logEntry->setTimestamp( $this->timestamp );
2099 $logEntry->setPerformer( $performer->
getUser() );
2100 $logEntry->setComment( $comment );
2101 $logEntry->setTarget( $descTitle );
2104 $logEntry->setParameters(
2106 'img_sha1' => $this->sha1,
2107 'img_timestamp' => $timestamp,
2116 $logId = $logEntry->insert();
2118 if ( $descTitle->exists() ) {
2119 if ( $createNullRevision ) {
2120 $services = MediaWikiServices::getInstance();
2122 $formatter = $services->getLogFormatterFactory()->newFromEntry( $logEntry );
2123 $formatter->setContext( RequestContext::newExtraneousContext( $descTitle ) );
2124 $editSummary = $formatter->getPlainActionText();
2126 $nullRevRecord = $wikiPage->newPageUpdater( $performer->
getUser() )
2127 ->setCause( PageUpdater::CAUSE_UPLOAD )
2131 $logEntry->setAssociatedRevId( $nullRevRecord->getId() );
2134 $newPageContent =
null;
2137 $newPageContent = ContentHandler::makeContent( $pageText, $descTitle );
2143 $dbw->endAtomic( __METHOD__ );
2144 $fname = __METHOD__;
2146 # Do some cache purges after final commit so that:
2147 # a) Changes are more likely to be seen post-purge
2148 # b) They won't cause rollback of the log publish/update above
2149 $purgeUpdate =
new AutoCommitUpdate(
2153 $reupload, $wikiPage, $newPageContent, $comment, $performer,
2154 $logEntry, $logId, $descId, $tags, $fname
2156 # Update memcache after the commit
2157 $this->invalidateCache();
2159 $updateLogPage =
false;
2160 if ( $newPageContent ) {
2161 # New file page; create the description page.
2162 # There's already a log entry, so don't make a second RC entry
2163 # CDN and file cache for the description page are purged by doUserEditContent.
2164 $revRecord = $wikiPage->newPageUpdater( $performer )
2165 ->setCause( PageUpdater::CAUSE_UPLOAD )
2166 ->setContent( SlotRecord::MAIN, $newPageContent )
2171 $logEntry->setAssociatedRevId( $revRecord->getId() );
2175 $updateLogPage = $revRecord->getPageId();
2178 # Existing file page: invalidate description page cache
2179 $title = $wikiPage->getTitle();
2180 $title->invalidateCache();
2181 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2182 $hcu->purgeTitleUrls( $title, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2183 # Allow the new file version to be patrolled from the page footer
2184 Article::purgePatrolFooterCache( $descId );
2187 # Update associated rev id. This should be done by $logEntry->insert() earlier,
2188 # but setAssociatedRevId() wasn't called at that point yet...
2189 $logParams = $logEntry->getParameters();
2190 $logParams[
'associated_rev_id'] = $logEntry->getAssociatedRevId();
2191 $update = [
'log_params' => LogEntryBase::makeParamBlob( $logParams ) ];
2192 if ( $updateLogPage ) {
2193 # Also log page, in case where we just created it above
2194 $update[
'log_page'] = $updateLogPage;
2196 $this->getRepo()->getPrimaryDB()->newUpdateQueryBuilder()
2197 ->update(
'logging' )
2199 ->where( [
'log_id' => $logId ] )
2200 ->caller( $fname )->execute();
2202 $this->getRepo()->getPrimaryDB()->newInsertQueryBuilder()
2203 ->insertInto(
'log_search' )
2205 'ls_field' =>
'associated_rev_id',
2206 'ls_value' => (
string)$logEntry->getAssociatedRevId(),
2207 'ls_log_id' => $logId,
2209 ->caller( $fname )->execute();
2211 # Add change tags, if any
2213 $logEntry->addTags( $tags );
2216 # Uploads can be patrolled
2217 $logEntry->setIsPatrollable(
true );
2219 # Now that the log entry is up-to-date, make an RC entry.
2220 $logEntry->publish( $logId );
2222 # Run hook for other updates (typically more cache purging)
2223 $this->getHookRunner()->onFileUpload( $this, $reupload, !$newPageContent );
2226 # Delete old thumbnails
2227 $this->purgeThumbnails();
2228 # Remove the old file from the CDN cache
2229 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2230 $hcu->purgeUrls( $this->getUrl(), $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2232 # Update backlink pages pointing to this title if created
2233 $blcFactory = MediaWikiServices::getInstance()->getBacklinkCacheFactory();
2234 LinksUpdate::queueRecursiveJobsForTable(
2238 $performer->
getUser()->getName(),
2239 $blcFactory->getBacklinkCache( $this->getTitle() )
2243 $this->prerenderThumbnails();
2247 # Invalidate cache for all pages using this file
2248 $cacheUpdateJob = HTMLCacheUpdateJob::newForBacklinks(
2251 [
'causeAction' =>
'file-upload',
'causeAgent' => $performer->
getUser()->getName() ]
2259 $dbw->onTransactionCommitOrIdle(
static function () use ( $reupload, $purgeUpdate, $cacheUpdateJob ) {
2260 DeferredUpdates::addUpdate( $purgeUpdate, DeferredUpdates::PRESEND );
2264 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [
'images' => 1 ] ) );
2267 MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $cacheUpdateJob );
2270 return Status::newGood();
2289 public function publish( $src, $flags = 0, array $options = [] ) {
2290 return $this->publishTo( $src, $this->getRel(), $flags, $options );
2309 protected function publishTo( $src, $dstRel, $flags = 0, array $options = [] ) {
2310 $srcPath = ( $src instanceof
FSFile ) ? $src->getPath() : $src;
2312 $repo = $this->getRepo();
2313 if ( $repo->getReadOnlyReason() !==
false ) {
2314 return $this->readOnlyFatalStatus();
2317 $status = $this->acquireFileLock();
2318 if ( !$status->isOK() ) {
2322 if ( $this->isOld() ) {
2323 $archiveRel = $dstRel;
2324 $archiveName = basename( $archiveRel );
2326 $archiveName =
wfTimestamp( TS_MW ) .
'!' . $this->getName();
2327 $archiveRel = $this->getArchiveRel( $archiveName );
2330 if ( $repo->hasSha1Storage() ) {
2331 $sha1 = FileRepo::isVirtualUrl( $srcPath )
2332 ? $repo->getFileSha1( $srcPath )
2333 : FSFile::getSha1Base36FromPath( $srcPath );
2335 $wrapperBackend = $repo->getBackend();
2336 '@phan-var FileBackendDBRepoWrapper $wrapperBackend';
2337 $dst = $wrapperBackend->getPathForSHA1( $sha1 );
2338 $status = $repo->quickImport( $src, $dst );
2339 if ( $flags & File::DELETE_SOURCE ) {
2343 if ( $this->exists() ) {
2344 $status->value = $archiveName;
2347 $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
2348 $status = $repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
2350 if ( $status->value ==
'new' ) {
2351 $status->value =
'';
2353 $status->value = $archiveName;
2357 $this->releaseFileLock();
2380 $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
2381 if ( $this->getRepo()->getReadOnlyReason() !==
false ) {
2382 return $this->readOnlyFatalStatus();
2385 wfDebugLog(
'imagemove',
"Got request to move {$this->name} to " . $target->getText() );
2388 $status = $batch->addCurrent();
2389 if ( !$status->isOK() ) {
2392 $archiveNames = $batch->addOlds();
2393 $status = $batch->execute();
2395 wfDebugLog(
'imagemove',
"Finished moving {$this->name}" );
2398 $oldTitleFile = $localRepo->newFile( $this->title );
2399 $newTitleFile = $localRepo->newFile( $target );
2400 DeferredUpdates::addUpdate(
2402 $this->getRepo()->getPrimaryDB(),
2404 static function () use ( $oldTitleFile, $newTitleFile, $archiveNames ) {
2405 $oldTitleFile->purgeEverything();
2406 foreach ( $archiveNames as $archiveName ) {
2408 '@phan-var OldLocalFile $oldTitleFile';
2409 $oldTitleFile->purgeOldThumbnails( $archiveName );
2411 $newTitleFile->purgeEverything();
2414 DeferredUpdates::PRESEND
2417 if ( $status->isOK() ) {
2419 $this->title = $target;
2422 $this->hashPath =
null;
2445 if ( $this->getRepo()->getReadOnlyReason() !==
false ) {
2446 return $this->readOnlyFatalStatus();
2451 $batch->addCurrent();
2453 $archiveNames = $batch->addOlds();
2454 $status = $batch->execute();
2456 if ( $status->isOK() ) {
2457 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [
'images' => -1 ] ) );
2461 DeferredUpdates::addUpdate(
2463 $this->getRepo()->getPrimaryDB(),
2465 function () use ( $archiveNames ) {
2466 $this->purgeEverything();
2467 foreach ( $archiveNames as $archiveName ) {
2468 $this->purgeOldThumbnails( $archiveName );
2472 DeferredUpdates::PRESEND
2477 foreach ( $archiveNames as $archiveName ) {
2478 $purgeUrls[] = $this->getArchiveUrl( $archiveName );
2481 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2482 $hcu->purgeUrls( $purgeUrls, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2505 if ( $this->getRepo()->getReadOnlyReason() !==
false ) {
2506 return $this->readOnlyFatalStatus();
2511 $batch->addOld( $archiveName );
2512 $status = $batch->execute();
2514 $this->purgeOldThumbnails( $archiveName );
2515 if ( $status->isOK() ) {
2516 $this->purgeDescription();
2519 $url = $this->getArchiveUrl( $archiveName );
2520 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2521 $hcu->purgeUrls(
$url, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2538 public function restore( $versions = [], $unsuppress =
false ) {
2539 if ( $this->getRepo()->getReadOnlyReason() !==
false ) {
2540 return $this->readOnlyFatalStatus();
2548 $batch->addIds( $versions );
2550 $status = $batch->execute();
2551 if ( $status->isGood() ) {
2552 $cleanupStatus = $batch->cleanup();
2553 $cleanupStatus->successCount = 0;
2554 $cleanupStatus->failCount = 0;
2555 $status->merge( $cleanupStatus );
2573 return $this->title ? $this->title->getLocalURL() :
false;
2586 if ( !$this->title ) {
2590 $services = MediaWikiServices::getInstance();
2591 $page = $services->getPageStore()->getPageByReference( $this->
getTitle() );
2597 $parserOptions = ParserOptions::newFromUserAndLang(
2598 RequestContext::getMain()->getUser(),
2602 $parserOptions = ParserOptions::newFromContext( RequestContext::getMain() );
2605 $parseStatus = $services->getParserOutputAccess()
2606 ->getParserOutput( $page, $parserOptions );
2608 if ( !$parseStatus->isGood() ) {
2612 return $parseStatus->getValue()->getText();
2624 if ( $audience === self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
2626 } elseif ( $audience === self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $performer ) ) {
2641 if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
2643 } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $performer ) ) {
2646 return $this->description;
2657 return $this->timestamp;
2665 if ( !$this->exists() ) {
2672 if ( $this->descriptionTouched ===
null ) {
2673 $touched = $this->repo->getReplicaDB()->newSelectQueryBuilder()
2674 ->select(
'page_touched' )
2676 ->where( [
'page_namespace' => $this->title->getNamespace() ] )
2677 ->andWhere( [
'page_title' => $this->title->getDBkey() ] )
2678 ->caller( __METHOD__ )->fetchField();
2679 $this->descriptionTouched = $touched ?
wfTimestamp( TS_MW, $touched ) :
false;
2682 return $this->descriptionTouched;
2701 return $this->extraDataLoaded
2702 && strlen( serialize( $this->metadataArray ) ) <= self::CACHE_FIELD_MAX_LEN;
2714 return Status::wrap( $this->getRepo()->getBackend()->lockFiles(
2715 [ $this->getPath() ], LockManager::LOCK_EX, $timeout
2726 return Status::wrap( $this->getRepo()->getBackend()->unlockFiles(
2727 [ $this->getPath() ], LockManager::LOCK_EX
2742 if ( !$this->locked ) {
2743 $logger = LoggerFactory::getInstance(
'LocalFile' );
2745 $dbw = $this->repo->getPrimaryDB();
2746 $makesTransaction = !$dbw->trxLevel();
2747 $dbw->startAtomic( self::ATOMIC_SECTION_LOCK );
2751 $status = $this->acquireFileLock( 10 );
2752 if ( !$status->isGood() ) {
2753 $dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
2754 $logger->warning(
"Failed to lock '{file}'", [
'file' => $this->name ] );
2760 $dbw->onTransactionResolution(
2761 function () use ( $logger ) {
2762 $status = $this->releaseFileLock();
2763 if ( !$status->isGood() ) {
2764 $logger->error(
"Failed to unlock '{file}'", [
'file' => $this->name ] );
2770 $this->lockedOwnTrx = $makesTransaction;
2775 return $this->lockedOwnTrx;
2789 if ( $this->locked ) {
2791 if ( !$this->locked ) {
2792 $dbw = $this->repo->getPrimaryDB();
2793 $dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
2794 $this->lockedOwnTrx =
false;
2803 return $this->getRepo()->newFatal(
'filereadonlyerror', $this->getName(),
2804 $this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
2816class_alias( LocalFile::class,
'LocalFile' );
const SCHEMA_COMPAT_WRITE_NEW
const EDIT_SILENT
Do not notify other users (e.g.
const EDIT_NEW
Article is assumed to be non-existent, fail if it exists.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Resource locking handling.
MimeMagic helper wrapper.
Base class for content handling.
Group all the pieces relevant to the context of a request into one instance.
Extends the LogEntry Interface with some basic functionality.
Class for creating new log entries and inserting them into the database.
A class containing constants representing the names of configuration variables.
const FileSchemaMigrationStage
Name constant for the FileSchemaMigrationStage setting, for use with Config::get()
Legacy class representing an editable page and handling UI for some page actions.
Special handling for representing file pages.
Controller-like object for creating and updating pages by creating new revisions.