28use Wikimedia\AtEase\AtEase;
101 protected $repoClass = LocalRepo::class;
179 $file->loadFromRow( $row );
195 public static function newFromKey( $sha1, $repo, $timestamp =
false ) {
198 $conds = [
'img_sha1' => $sha1 ];
200 $conds[
'img_timestamp'] =
$dbr->timestamp( $timestamp );
203 $fileQuery = static::getQueryInfo();
204 $row =
$dbr->selectRow(
205 $fileQuery[
'tables'], $fileQuery[
'fields'], $conds, __METHOD__, [], $fileQuery[
'joins']
208 return static::newFromRow( $row,
$repo );
228 $commentQuery = MediaWikiServices::getInstance()->getCommentStore()->getJoin(
'img_description' );
229 $actorQuery = ActorMigration::newMigration()->getJoin(
'img_user' );
231 'tables' => [
'image' ] + $commentQuery[
'tables'] + $actorQuery[
'tables'],
244 ] + $commentQuery[
'fields'] + $actorQuery[
'fields'],
245 'joins' => $commentQuery[
'joins'] + $actorQuery[
'joins'],
248 if ( in_array(
'omit-nonlazy', $options,
true ) ) {
252 if ( !in_array(
'omit-lazy', $options,
true ) ) {
254 $ret[
'fields'][] =
'img_metadata';
270 $this->metadata =
'';
271 $this->historyLine = 0;
272 $this->historyRes =
null;
273 $this->dataLoaded =
false;
274 $this->extraDataLoaded =
false;
294 return $this->repo->getSharedCacheKey(
'file', sha1( $this->
getName() ) );
310 $this->dataLoaded =
false;
311 $this->extraDataLoaded =
false;
320 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
321 $cachedValues =
$cache->getWithSetCallback(
324 function ( $oldValue, &$ttl, array &$setOpts ) use (
$cache ) {
325 $setOpts += Database::getCacheSetOptions( $this->repo->getReplicaDB() );
331 $cacheVal[
'fileExists'] = $this->fileExists;
332 if ( $this->fileExists ) {
333 foreach ( $fields as $field ) {
334 $cacheVal[$field] = $this->$field;
337 $cacheVal[
'user'] = $this->user ? $this->user->getId() : 0;
338 $cacheVal[
'user_text'] = $this->user ? $this->user->getName() :
'';
339 $cacheVal[
'actor'] = $this->user ? $this->user->getActorId() :
null;
345 if ( isset( $cacheVal[$field] )
346 && strlen( $cacheVal[$field] ) > 100 * 1024
348 unset( $cacheVal[$field] );
352 if ( $this->fileExists ) {
355 $ttl = $cache::TTL_DAY;
360 [
'version' => self::VERSION ]
363 $this->fileExists = $cachedValues[
'fileExists'];
364 if ( $this->fileExists ) {
368 $this->dataLoaded =
true;
369 $this->extraDataLoaded =
true;
371 $this->extraDataLoaded = $this->extraDataLoaded && isset( $cachedValues[$field] );
384 $this->repo->getMasterDB()->onTransactionPreCommitOrIdle(
385 function () use ( $key ) {
386 MediaWikiServices::getInstance()->getMainWANObjectCache()->delete( $key );
396 $props = $this->repo->getFileProps( $this->
getVirtualUrl() );
408 if ( $prefix !==
'' ) {
409 throw new InvalidArgumentException(
410 __METHOD__ .
' with a non-empty prefix is no longer supported.'
418 return [
'size',
'width',
'height',
'bits',
'media_type',
419 'major_mime',
'minor_mime',
'metadata',
'timestamp',
'sha1',
'description' ];
430 if ( $prefix !==
'' ) {
431 throw new InvalidArgumentException(
432 __METHOD__ .
' with a non-empty prefix is no longer supported.'
437 return [
'metadata' ];
446 $fname = static::class .
'::' . __FUNCTION__;
448 # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
449 $this->dataLoaded =
true;
450 $this->extraDataLoaded =
true;
452 $dbr = ( $flags & self::READ_LATEST )
453 ? $this->repo->getMasterDB()
454 : $this->repo->getReplicaDB();
456 $fileQuery = static::getQueryInfo();
457 $row =
$dbr->selectRow(
458 $fileQuery[
'tables'],
459 $fileQuery[
'fields'],
460 [
'img_name' => $this->
getName() ],
469 $this->fileExists =
false;
479 if ( !$this->title ) {
483 $fname = static::class .
'::' . __FUNCTION__;
485 # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
486 $this->extraDataLoaded =
true;
494 foreach ( $fieldMap as
$name => $value ) {
495 $this->
$name = $value;
498 throw new MWException(
"Could not find data for image '{$this->getName()}'." );
510 $fileQuery = self::getQueryInfo( [
'omit-nonlazy' ] );
511 $row =
$dbr->selectRow(
512 $fileQuery[
'tables'],
513 $fileQuery[
'fields'],
515 'img_name' => $this->
getName(),
516 'img_timestamp' =>
$dbr->timestamp( $this->getTimestamp() ),
525 # File may have been uploaded over in the meantime; check the old versions
527 $row =
$dbr->selectRow(
528 $fileQuery[
'tables'],
529 $fileQuery[
'fields'],
532 'oi_timestamp' =>
$dbr->timestamp( $this->getTimestamp() ),
543 if ( isset( $fieldMap[
'metadata'] ) ) {
544 $fieldMap[
'metadata'] = $this->repo->getReplicaDB()->decodeBlob( $fieldMap[
'metadata'] );
557 $array = (array)$row;
558 $prefixLength = strlen( $prefix );
561 if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
562 throw new MWException( __METHOD__ .
': incorrect $prefix parameter' );
566 foreach ( $array as
$name => $value ) {
567 $decoded[substr(
$name, $prefixLength )] = $value;
584 $decoded[
'description'] = MediaWikiServices::getInstance()->getCommentStore()
585 ->getComment(
'description', (
object)$decoded )->text;
587 $decoded[
'user'] = User::newFromAnyId(
588 $decoded[
'user'] ??
null,
589 $decoded[
'user_text'] ??
null,
590 $decoded[
'actor'] ??
null
592 unset( $decoded[
'user_text'], $decoded[
'actor'] );
594 $decoded[
'timestamp'] =
wfTimestamp( TS_MW, $decoded[
'timestamp'] );
596 $decoded[
'metadata'] = $this->repo->getReplicaDB()->decodeBlob( $decoded[
'metadata'] );
598 if ( empty( $decoded[
'major_mime'] ) ) {
599 $decoded[
'mime'] =
'unknown/unknown';
601 if ( !$decoded[
'minor_mime'] ) {
602 $decoded[
'minor_mime'] =
'unknown';
604 $decoded[
'mime'] = $decoded[
'major_mime'] .
'/' . $decoded[
'minor_mime'];
608 $decoded[
'sha1'] = rtrim( $decoded[
'sha1'],
"\0" );
614 foreach ( [
'size',
'width',
'height',
'bits' ] as $field ) {
615 $decoded[$field] = +$decoded[$field];
629 $this->dataLoaded =
true;
630 $this->extraDataLoaded =
true;
632 $array = $this->
decodeRow( $row, $prefix );
634 foreach ( $array as
$name => $value ) {
635 $this->
$name = $value;
638 $this->fileExists =
true;
646 public function load( $flags = 0 ) {
647 if ( !$this->dataLoaded ) {
648 if ( $flags & self::READ_LATEST ) {
655 if ( ( $flags & self::LOAD_ALL ) && !$this->extraDataLoaded ) {
672 if ( $this->media_type ===
null || $this->mime ==
'image/svg' ) {
687 $this->upgrading =
true;
689 DeferredUpdates::addCallableUpdate(
function () {
690 $this->upgrading =
false;
704 return $this->upgraded;
716 # Don't destroy file info of missing files
717 if ( !$this->fileExists ) {
719 wfDebug( __METHOD__ .
": file does not exist, aborting" );
724 $dbw = $this->repo->getMasterDB();
725 list( $major, $minor ) = self::splitMime( $this->mime );
732 wfDebug( __METHOD__ .
': upgrading ' . $this->
getName() .
" to the current schema" );
734 $dbw->update(
'image',
736 'img_size' => $this->size,
737 'img_width' => $this->width,
738 'img_height' => $this->height,
739 'img_bits' => $this->bits,
740 'img_media_type' => $this->media_type,
741 'img_major_mime' => $major,
742 'img_minor_mime' => $minor,
743 'img_metadata' => $dbw->encodeBlob( $this->metadata ),
744 'img_sha1' => $this->sha1,
746 [
'img_name' => $this->getName() ],
753 $this->upgraded =
true;
768 $this->dataLoaded =
true;
770 $fields[] =
'fileExists';
772 foreach ( $fields as $field ) {
773 if ( isset( $info[$field] ) ) {
774 $this->$field = $info[$field];
778 if ( isset( $info[
'user'] ) || isset( $info[
'user_text'] ) || isset( $info[
'actor'] ) ) {
779 $this->user = User::newFromAnyId(
780 $info[
'user'] ??
null,
781 $info[
'user_text'] ??
null,
782 $info[
'actor'] ??
null
787 if ( isset( $info[
'major_mime'] ) ) {
788 $this->mime =
"{$info['major_mime']}/{$info['minor_mime']}";
789 } elseif ( isset( $info[
'mime'] ) ) {
790 $this->mime = $info[
'mime'];
791 list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
811 if ( $this->missing ===
null ) {
812 $fileExists = $this->repo->fileExists( $this->
getVirtualUrl() );
813 $this->missing = !$fileExists;
816 return $this->missing;
841 return $dim[
'width'];
874 return $dim[
'height'];
881 return $this->height;
896 if ( !$this->user ) {
899 if (
$type ===
'object' ) {
900 return User::newFromName(
'Unknown user',
false );
901 } elseif (
$type ===
'text' ) {
902 return 'Unknown user';
903 } elseif (
$type ===
'id' ) {
907 if (
$type ===
'object' ) {
909 } elseif (
$type ===
'text' ) {
910 return $this->user->getName();
911 } elseif (
$type ===
'id' ) {
912 return $this->user->getId();
927 if ( !$this->title ) {
931 $pageId = $this->title->getArticleID();
934 $url = $this->repo->makeUrl( [
'curid' => $pageId ] );
935 if (
$url !==
false ) {
948 $this->
load( self::LOAD_ALL );
949 return $this->metadata;
959 return (
int)$this->bits;
993 return $this->media_type;
1010 return $this->fileExists;
1030 if ( $archiveName ) {
1036 $backend = $this->repo->getBackend();
1039 $iterator = $backend->getFileList( [
'dir' => $dir ] );
1040 if ( $iterator !==
null ) {
1041 foreach ( $iterator as
$file ) {
1075 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1078 !empty( $options[
'forThumbRefresh'] )
1079 ? $hcu::PURGE_PRESEND
1080 : $hcu::PURGE_INTENT_TXROUND_REFLECTED
1094 $dir = array_shift( $thumbs );
1098 foreach ( $thumbs as $thumb ) {
1103 $this->getHookRunner()->onLocalFilePurgeThumbnails( $this, $archiveName, $urls );
1106 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1107 $hcu->purgeUrls( $urls, $hcu::PURGE_PRESEND );
1120 $dir = array_shift( $thumbs );
1125 foreach ( $thumbs as $thumb ) {
1130 if ( !empty( $options[
'forThumbRefresh'] ) ) {
1138 $this->getHookRunner()->onLocalFilePurgeThumbnails( $this,
false, $urls );
1141 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1144 !empty( $options[
'forThumbRefresh'] )
1145 ? $hcu::PURGE_PRESEND
1146 : $hcu::PURGE_INTENT_TXROUND_REFLECTED
1164 foreach ( $sizes as $size ) {
1168 [
'transformParams' => [
'width' => $size ] ]
1174 JobQueueGroup::singleton()->lazyPush( $jobs );
1185 $fileListDebug = strtr(
1186 var_export( $files,
true ),
1189 wfDebug( __METHOD__ .
": $fileListDebug" );
1191 if ( $this->repo->supportsSha1URLs() ) {
1192 $reference = $this->
getSha1();
1194 $reference = $this->
getName();
1198 foreach ( $files as
$file ) {
1199 # Check that the reference (filename or sha1) is part of the thumb name
1200 # This is a basic sanity check to avoid erasing unrelated directories
1201 if ( strpos(
$file, $reference ) !==
false
1202 || strpos(
$file,
"-thumbnail" ) !==
false
1204 $purgeList[] =
"{$dir}/{$file}";
1208 # Delete the thumbnails
1209 $this->repo->quickPurgeBatch( $purgeList );
1210 # Clear out the thumbnail directory if empty
1211 $this->repo->quickCleanDir( $dir );
1225 public function getHistory( $limit =
null, $start =
null, $end =
null, $inc =
true ) {
1226 if ( !$this->
exists() ) {
1230 $dbr = $this->repo->getReplicaDB();
1233 $tables = $oldFileQuery[
'tables'];
1234 $fields = $oldFileQuery[
'fields'];
1235 $join_conds = $oldFileQuery[
'joins'];
1236 $conds = $opts = [];
1237 $eq = $inc ?
'=' :
'';
1238 $conds[] =
"oi_name = " .
$dbr->addQuotes( $this->title->getDBkey() );
1241 $conds[] =
"oi_timestamp <$eq " .
$dbr->addQuotes(
$dbr->timestamp( $start ) );
1245 $conds[] =
"oi_timestamp >$eq " .
$dbr->addQuotes(
$dbr->timestamp( $end ) );
1249 $opts[
'LIMIT'] = $limit;
1253 $order = ( !$start && $end !== null ) ?
'ASC' :
'DESC';
1254 $opts[
'ORDER BY'] =
"oi_timestamp $order";
1255 $opts[
'USE INDEX'] = [
'oldimage' =>
'oi_name_timestamp' ];
1257 $this->getHookRunner()->onLocalFile__getHistory( $this, $tables, $fields,
1258 $conds, $opts, $join_conds );
1260 $res =
$dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
1263 foreach (
$res as $row ) {
1264 $r[] = $this->repo->newFileFromRow( $row );
1267 if ( $order ==
'ASC' ) {
1268 $r = array_reverse( $r );
1285 if ( !$this->
exists() ) {
1289 # Polymorphic function name to distinguish foreign and local fetches
1290 $fname = static::class .
'::' . __FUNCTION__;
1292 $dbr = $this->repo->getReplicaDB();
1294 if ( $this->historyLine == 0 ) {
1295 $fileQuery = self::getQueryInfo();
1296 $this->historyRes =
$dbr->select( $fileQuery[
'tables'],
1297 $fileQuery[
'fields'] + [
1298 'oi_archive_name' =>
$dbr->addQuotes(
'' ),
1301 [
'img_name' => $this->title->getDBkey() ],
1307 if (
$dbr->numRows( $this->historyRes ) == 0 ) {
1308 $this->historyRes =
null;
1312 } elseif ( $this->historyLine == 1 ) {
1314 $this->historyRes =
$dbr->select(
1315 $fileQuery[
'tables'],
1316 $fileQuery[
'fields'],
1317 [
'oi_name' => $this->title->getDBkey() ],
1319 [
'ORDER BY' =>
'oi_timestamp DESC' ],
1323 $this->historyLine++;
1325 return $dbr->fetchObject( $this->historyRes );
1333 $this->historyLine = 0;
1335 if ( $this->historyRes !==
null ) {
1336 $this->historyRes =
null;
1373 public function upload( $src, $comment, $pageText, $flags = 0, $props =
false,
1374 $timestamp =
false, $user =
null, $tags = [],
1375 $createNullRevision =
true, $revert =
false
1377 if ( $this->
getRepo()->getReadOnlyReason() !==
false ) {
1379 } elseif ( MediaWikiServices::getInstance()->getRevisionStore()->isReadOnly() ) {
1385 $srcPath = ( $src instanceof
FSFile ) ? $src->getPath() : $src;
1390 $props = $this->repo->getFileProps( $srcPath );
1392 $mwProps =
new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
1393 $props = $mwProps->getPropsFromPath( $srcPath,
true );
1400 $metadata = AtEase::quietCall(
'unserialize', $props[
'metadata'] );
1402 if ( !is_array( $metadata ) ) {
1408 $options[
'headers'] = [];
1412 $comment = trim( $comment );
1415 $status = $this->
publish( $src, $flags, $options );
1417 if ( $status->successCount >= 2 ) {
1424 $oldver = $status->value;
1433 $createNullRevision,
1436 if ( !$uploadStatus->isOK() ) {
1437 if ( $uploadStatus->hasMessage(
'filenotfound' ) ) {
1439 $status->fatal(
'filenotfound', $srcPath );
1441 $status->merge( $uploadStatus );
1464 $watch =
false, $timestamp =
false,
User $user =
null ) {
1471 $pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus,
$source );
1473 if ( !$this->
recordUpload2( $oldver, $desc, $pageText,
false, $timestamp, $user )->isOK() ) {
1500 $oldver, $comment, $pageText, $props =
false, $timestamp =
false, $user =
null, $tags = [],
1501 $createNullRevision =
true, $revert =
false
1504 if ( $user ===
null ) {
1509 $oldver, $comment, $pageText,
1510 $user, $props, $timestamp, $tags,
1511 $createNullRevision, $revert
1539 bool $createNullRevision =
true,
1540 bool $revert =
false
1542 $dbw = $this->repo->getMasterDB();
1544 # Imports or such might force a certain timestamp; otherwise we generate
1545 # it and can fudge it slightly to keep (name,timestamp) unique on re-upload.
1546 if ( $timestamp ===
false ) {
1547 $timestamp = $dbw->timestamp();
1548 $allowTimeKludge =
true;
1550 $allowTimeKludge =
false;
1553 $props = $props ?: $this->repo->getFileProps( $this->
getVirtualUrl() );
1554 $props[
'description'] = $comment;
1555 $props[
'user'] = $user->
getId();
1556 $props[
'user_text'] = $user->
getName();
1558 $props[
'timestamp'] =
wfTimestamp( TS_MW, $timestamp );
1561 # Fail now if the file isn't there
1562 if ( !$this->fileExists ) {
1563 wfDebug( __METHOD__ .
": File " . $this->
getRel() .
" went missing!" );
1568 $dbw->startAtomic( __METHOD__ );
1570 # Test to see if the row exists using INSERT IGNORE
1571 # This avoids race conditions by locking the row until the commit, and also
1572 # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
1573 $commentStore = MediaWikiServices::getInstance()->getCommentStore();
1574 $commentFields = $commentStore->insert( $dbw,
'img_description', $comment );
1576 $actorFields = $actorMigration->getInsertValues( $dbw,
'img_user', $user );
1577 $dbw->insert(
'image',
1579 'img_name' => $this->
getName(),
1580 'img_size' => $this->size,
1581 'img_width' => intval( $this->width ),
1582 'img_height' => intval( $this->height ),
1583 'img_bits' => $this->bits,
1584 'img_media_type' => $this->media_type,
1585 'img_major_mime' => $this->major_mime,
1586 'img_minor_mime' => $this->minor_mime,
1587 'img_timestamp' => $timestamp,
1588 'img_metadata' => $dbw->encodeBlob( $this->metadata ),
1589 'img_sha1' => $this->sha1
1590 ] + $commentFields + $actorFields,
1594 $reupload = ( $dbw->affectedRows() == 0 );
1597 $row = $dbw->selectRow(
1599 [
'img_timestamp',
'img_sha1' ],
1600 [
'img_name' => $this->
getName() ],
1602 [
'LOCK IN SHARE MODE' ]
1605 if ( $row && $row->img_sha1 === $this->sha1 ) {
1606 $dbw->endAtomic( __METHOD__ );
1607 wfDebug( __METHOD__ .
": File " . $this->
getRel() .
" already exists!" );
1612 if ( $allowTimeKludge ) {
1613 # Use LOCK IN SHARE MODE to ignore any transaction snapshotting
1614 $lUnixtime = $row ?
wfTimestamp( TS_UNIX, $row->img_timestamp ) : false;
1615 # Avoid a timestamp that is not newer than the last version
1616 # TODO: the image/oldimage tables should be like page/revision with an ID field
1617 if ( $lUnixtime &&
wfTimestamp( TS_UNIX, $timestamp ) <= $lUnixtime ) {
1619 $timestamp = $dbw->timestamp( $lUnixtime + 1 );
1620 $this->timestamp =
wfTimestamp( TS_MW, $timestamp );
1624 $tables = [
'image' ];
1626 'oi_name' =>
'img_name',
1627 'oi_archive_name' => $dbw->addQuotes( $oldver ),
1628 'oi_size' =>
'img_size',
1629 'oi_width' =>
'img_width',
1630 'oi_height' =>
'img_height',
1631 'oi_bits' =>
'img_bits',
1632 'oi_description_id' =>
'img_description_id',
1633 'oi_timestamp' =>
'img_timestamp',
1634 'oi_metadata' =>
'img_metadata',
1635 'oi_media_type' =>
'img_media_type',
1636 'oi_major_mime' =>
'img_major_mime',
1637 'oi_minor_mime' =>
'img_minor_mime',
1638 'oi_sha1' =>
'img_sha1',
1639 'oi_actor' =>
'img_actor',
1643 # (T36993) Note: $oldver can be empty here, if the previous
1644 # version of the file was broken. Allow registration of the new
1645 # version to continue anyway, because that's better than having
1646 # an image that's not fixable by user operations.
1647 # Collision, this is an update of a file
1648 # Insert previous contents into oldimage
1649 $dbw->insertSelect(
'oldimage', $tables, $fields,
1650 [
'img_name' => $this->
getName() ], __METHOD__, [], [], $joins );
1652 # Update the current image row
1653 $dbw->update(
'image',
1655 'img_size' => $this->size,
1656 'img_width' => intval( $this->width ),
1657 'img_height' => intval( $this->height ),
1658 'img_bits' => $this->bits,
1659 'img_media_type' => $this->media_type,
1660 'img_major_mime' => $this->major_mime,
1661 'img_minor_mime' => $this->minor_mime,
1662 'img_timestamp' => $timestamp,
1663 'img_metadata' => $dbw->encodeBlob( $this->metadata ),
1664 'img_sha1' => $this->sha1
1665 ] + $commentFields + $actorFields,
1666 [
'img_name' => $this->getName() ],
1672 $descId = $descTitle->getArticleID();
1674 $wikiPage->setFile( $this );
1677 if ( $revert ===
true ) {
1678 $logAction =
'revert';
1679 } elseif ( $reupload ===
true ) {
1680 $logAction =
'overwrite';
1682 $logAction =
'upload';
1686 $logEntry->setTimestamp( $this->timestamp );
1687 $logEntry->setPerformer( $user );
1688 $logEntry->setComment( $comment );
1689 $logEntry->setTarget( $descTitle );
1692 $logEntry->setParameters(
1694 'img_sha1' => $this->sha1,
1695 'img_timestamp' => $timestamp,
1704 $logId = $logEntry->insert();
1706 if ( $descTitle->exists() ) {
1707 if ( $createNullRevision !==
false ) {
1708 $revStore = MediaWikiServices::getInstance()->getRevisionStore();
1711 $formatter->setContext( RequestContext::newExtraneousContext( $descTitle ) );
1712 $editSummary = $formatter->getPlainActionText();
1714 $nullRevRecord =
$revStore->newNullRevision(
1722 if ( $nullRevRecord ) {
1723 $inserted =
$revStore->insertRevisionOn( $nullRevRecord, $dbw );
1725 $this->getHookRunner()->onRevisionFromEditComplete(
1728 $inserted->getParentId(),
1734 if ( $this->getHookContainer()->isRegistered(
'NewRevisionFromEditComplete' ) ) {
1736 $nullRevision =
new Revision( $inserted );
1737 $this->getHookRunner()->onNewRevisionFromEditComplete(
1740 $inserted->getParentId(),
1746 $wikiPage->updateRevisionOn( $dbw, $inserted );
1748 $logEntry->setAssociatedRevId( $inserted->getId() );
1752 $newPageContent =
null;
1758 # Defer purges, page creation, and link updates in case they error out.
1759 # The most important thing is that files and the DB registry stay synced.
1760 $dbw->endAtomic( __METHOD__ );
1761 $fname = __METHOD__;
1763 # Do some cache purges after final commit so that:
1764 # a) Changes are more likely to be seen post-purge
1765 # b) They won't cause rollback of the log publish/update above
1772 $reupload, $wikiPage, $newPageContent, $comment, $user,
1773 $logEntry, $logId, $descId, $tags, $fname
1775 # Update memcache after the commit
1778 $updateLogPage =
false;
1779 if ( $newPageContent ) {
1780 # New file page; create the description page.
1781 # There's already a log entry, so don't make a second RC entry
1782 # CDN and file cache for the description page are purged by doEditContent.
1783 $status = $wikiPage->doEditContent(
1791 if ( isset( $status->value[
'revision-record'] ) ) {
1793 $revRecord = $status->value[
'revision-record'];
1795 $logEntry->setAssociatedRevId( $revRecord->getId() );
1799 if ( isset( $status->value[
'revision-record'] ) ) {
1801 $revRecord = $status->value[
'revision-record'];
1802 $updateLogPage = $revRecord->getPageId();
1805 # Existing file page: invalidate description page cache
1806 $title = $wikiPage->getTitle();
1807 $title->invalidateCache();
1808 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1809 $hcu->purgeTitleUrls(
$title, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
1810 # Allow the new file version to be patrolled from the page footer
1814 # Update associated rev id. This should be done by $logEntry->insert() earlier,
1815 # but setAssociatedRevId() wasn't called at that point yet...
1816 $logParams = $logEntry->getParameters();
1817 $logParams[
'associated_rev_id'] = $logEntry->getAssociatedRevId();
1819 if ( $updateLogPage ) {
1820 # Also log page, in case where we just created it above
1821 $update[
'log_page'] = $updateLogPage;
1823 $this->
getRepo()->getMasterDB()->update(
1826 [
'log_id' => $logId ],
1829 $this->
getRepo()->getMasterDB()->insert(
1832 'ls_field' =>
'associated_rev_id',
1833 'ls_value' => (
string)$logEntry->getAssociatedRevId(),
1834 'ls_log_id' => $logId,
1839 # Add change tags, if any
1841 $logEntry->addTags( $tags );
1844 # Uploads can be patrolled
1845 $logEntry->setIsPatrollable(
true );
1847 # Now that the log entry is up-to-date, make an RC entry.
1848 $logEntry->publish( $logId );
1850 # Run hook for other updates (typically more cache purging)
1851 $this->getHookRunner()->onFileUpload( $this, $reupload, !$newPageContent );
1854 # Delete old thumbnails
1856 # Remove the old file from the CDN cache
1857 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1858 $hcu->purgeUrls( $this->
getUrl(), $hcu::PURGE_INTENT_TXROUND_REFLECTED );
1860 # Update backlink pages pointing to this title if created
1872 DeferredUpdates::PRESEND
1876 # This is a new file, so update the image count
1880 # Invalidate cache for all pages using this file
1884 [
'causeAction' =>
'file-upload',
'causeAgent' => $user->
getName() ]
1907 public function publish( $src, $flags = 0, array $options = [] ) {
1908 return $this->publishTo( $src, $this->getRel(), $flags, $options );
1927 protected function publishTo( $src, $dstRel, $flags = 0, array $options = [] ) {
1928 $srcPath = ( $src instanceof
FSFile ) ? $src->getPath() : $src;
1930 $repo = $this->getRepo();
1931 if ( $repo->getReadOnlyReason() !==
false ) {
1932 return $this->readOnlyFatalStatus();
1937 if ( $this->isOld() ) {
1938 $archiveRel = $dstRel;
1939 $archiveName = basename( $archiveRel );
1941 $archiveName =
wfTimestamp( TS_MW ) .
'!' . $this->getName();
1942 $archiveRel = $this->getArchiveRel( $archiveName );
1945 if ( $repo->hasSha1Storage() ) {
1947 ? $repo->getFileSha1( $srcPath )
1950 $wrapperBackend = $repo->getBackend();
1951 '@phan-var FileBackendDBRepoWrapper $wrapperBackend';
1952 $dst = $wrapperBackend->getPathForSHA1( $sha1 );
1953 $status = $repo->quickImport( $src, $dst );
1958 if ( $this->exists() ) {
1959 $status->value = $archiveName;
1963 $status = $repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
1965 if ( $status->value ==
'new' ) {
1966 $status->value =
'';
1968 $status->value = $archiveName;
1995 $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
1996 if ( $this->getRepo()->getReadOnlyReason() !==
false ) {
1997 return $this->readOnlyFatalStatus();
2000 wfDebugLog(
'imagemove',
"Got request to move {$this->name} to " . $target->getText() );
2004 $batch->addCurrent();
2005 $archiveNames = $batch->addOlds();
2006 $status = $batch->execute();
2009 wfDebugLog(
'imagemove',
"Finished moving {$this->name}" );
2012 $oldTitleFile = $localRepo->newFile( $this->title );
2013 $newTitleFile = $localRepo->newFile( $target );
2014 DeferredUpdates::addUpdate(
2016 $this->getRepo()->getMasterDB(),
2018 function () use ( $oldTitleFile, $newTitleFile, $archiveNames ) {
2019 $oldTitleFile->purgeEverything();
2020 foreach ( $archiveNames as $archiveName ) {
2022 '@phan-var OldLocalFile $oldTitleFile';
2023 $oldTitleFile->purgeOldThumbnails( $archiveName );
2025 $newTitleFile->purgeEverything();
2028 DeferredUpdates::PRESEND
2031 if ( $status->isOK() ) {
2033 $this->title = $target;
2036 $this->hashPath =
null;
2057 public function delete( $reason, $suppress =
false, $user = null ) {
2059 if ( $user ===
null ) {
2063 return $this->deleteFile( $reason, $user, $suppress );
2083 if ( $this->getRepo()->getReadOnlyReason() !==
false ) {
2084 return $this->readOnlyFatalStatus();
2090 $batch->addCurrent();
2092 $archiveNames = $batch->addOlds();
2093 $status = $batch->execute();
2096 if ( $status->isOK() ) {
2097 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [
'images' => -1 ] ) );
2101 DeferredUpdates::addUpdate(
2103 $this->getRepo()->getMasterDB(),
2105 function () use ( $archiveNames ) {
2106 $this->purgeEverything();
2107 foreach ( $archiveNames as $archiveName ) {
2108 $this->purgeOldThumbnails( $archiveName );
2112 DeferredUpdates::PRESEND
2117 foreach ( $archiveNames as $archiveName ) {
2118 $purgeUrls[] = $this->getArchiveUrl( $archiveName );
2121 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2122 $hcu->purgeUrls( $purgeUrls, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2144 public function deleteOld( $archiveName, $reason, $suppress =
false, $user =
null ) {
2146 if ( $user ===
null ) {
2150 return $this->deleteOldFile( $archiveName, $reason, $user, $suppress );
2172 if ( $this->getRepo()->getReadOnlyReason() !==
false ) {
2173 return $this->readOnlyFatalStatus();
2179 $batch->addOld( $archiveName );
2180 $status = $batch->execute();
2183 $this->purgeOldThumbnails( $archiveName );
2184 if ( $status->isOK() ) {
2185 $this->purgeDescription();
2188 $url = $this->getArchiveUrl( $archiveName );
2189 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2190 $hcu->purgeUrls( $url, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2207 public function restore( $versions = [], $unsuppress =
false ) {
2208 if ( $this->getRepo()->getReadOnlyReason() !==
false ) {
2209 return $this->readOnlyFatalStatus();
2218 $batch->addIds( $versions );
2220 $status = $batch->execute();
2221 if ( $status->isGood() ) {
2222 $cleanupStatus = $batch->cleanup();
2223 $cleanupStatus->successCount = 0;
2224 $cleanupStatus->failCount = 0;
2225 $status->merge( $cleanupStatus );
2243 if ( !$this->title ) {
2247 return $this->title->getLocalURL();
2260 if ( !$this->title ) {
2264 $store = MediaWikiServices::getInstance()->getRevisionStore();
2265 $revision = $store->getRevisionByTitle( $this->title, 0, RevisionStore::READ_NORMAL );
2270 $renderer = MediaWikiServices::getInstance()->getRevisionRenderer();
2271 $rendered = $renderer->getRenderedRevision( $revision,
new ParserOptions(
null,
$lang ) );
2278 $pout = $rendered->getRevisionParserOutput();
2279 return $pout->getText();
2290 if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
2292 } elseif ( $audience == self::FOR_THIS_USER
2293 && !$this->userCan( self::DELETED_COMMENT, $user )
2297 return $this->description;
2308 return $this->timestamp;
2316 if ( !$this->exists() ) {
2323 if ( $this->descriptionTouched ===
null ) {
2325 'page_namespace' => $this->title->getNamespace(),
2326 'page_title' => $this->title->getDBkey()
2328 $touched = $this->repo->getReplicaDB()->selectField(
'page',
'page_touched', $cond, __METHOD__ );
2329 $this->descriptionTouched = $touched ?
wfTimestamp( TS_MW, $touched ) :
false;
2332 return $this->descriptionTouched;
2342 if ( $this->sha1 ==
'' && $this->fileExists ) {
2345 $this->sha1 = $this->repo->getFileSha1( $this->getPath() );
2346 if ( !
wfReadOnly() && strval( $this->sha1 ) !=
'' ) {
2347 $dbw = $this->repo->getMasterDB();
2348 $dbw->update(
'image',
2349 [
'img_sha1' => $this->sha1 ],
2350 [
'img_name' => $this->getName() ],
2352 $this->invalidateCache();
2368 return $this->extraDataLoaded
2369 && strlen(
serialize( $this->metadata ) ) <= self::CACHE_FIELD_MAX_LEN;
2377 return Status::wrap( $this->getRepo()->getBackend()->lockFiles(
2378 [ $this->getPath() ], LockManager::LOCK_EX, 10
2387 return Status::wrap( $this->getRepo()->getBackend()->unlockFiles(
2388 [ $this->getPath() ], LockManager::LOCK_EX
2402 if ( !$this->locked ) {
2403 $logger = LoggerFactory::getInstance(
'LocalFile' );
2405 $dbw = $this->repo->getMasterDB();
2406 $makesTransaction = !$dbw->trxLevel();
2407 $dbw->startAtomic( self::ATOMIC_SECTION_LOCK );
2411 $status = $this->acquireFileLock();
2412 if ( !$status->isGood() ) {
2413 $dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
2414 $logger->warning(
"Failed to lock '{file}'", [
'file' => $this->name ] );
2420 $dbw->onTransactionResolution(
2421 function () use ( $logger ) {
2422 $status = $this->releaseFileLock();
2423 if ( !$status->isGood() ) {
2424 $logger->error(
"Failed to unlock '{file}'", [
'file' => $this->name ] );
2430 $this->lockedOwnTrx = $makesTransaction;
2435 return $this->lockedOwnTrx;
2447 if ( $this->locked ) {
2449 if ( !$this->locked ) {
2450 $dbw = $this->repo->getMasterDB();
2451 $dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
2452 $this->lockedOwnTrx =
false;
2461 return $this->getRepo()->newFatal(
'filereadonlyerror', $this->getName(),
2462 $this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
$wgUpdateCompatibleMetadata
If to automatically update the img_metadata field if the metadata field is outdated but compatible wi...
$wgUploadThumbnailRenderMap
When defined, is an array of thumbnail widths to be rendered at upload time.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfReadOnly()
Check whether the wiki is in read-only mode.
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 $function is deprecated.
static newMigration()
Static constructor.
static purgePatrolFooterCache( $articleID)
Purge the cache used to check if it is worth showing the patrol footer For example,...
Deferrable Update for closure/callback updates that should use auto-commit mode.
static makeContent( $text, Title $title=null, $modelId=null, $format=null)
Convenience function for creating a Content object from a given textual representation.
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the deferred update queue for execution at the appropriate time.
Class representing a non-directory file on the file system.
static getSha1Base36FromPath( $path)
Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case encoding,...
File backend exception for checked exceptions (e.g.
static isStoragePath( $path)
Check if a given path is a "mwstore://" path.
static isVirtualUrl( $url)
Determine if a string is an mwrepo:// URL.
Implements some public methods and some protected utility functions which are required by multiple ch...
string $url
The URL corresponding to one of the four basic zones.
isVectorized()
Return true if the file is vectorized.
getThumbPath( $suffix=false)
Get the path of the thumbnail directory, or a particular file if $suffix is specified Stable to overr...
getRel()
Get the path of the file relative to the public zone root.
assertRepoDefined()
Assert that $this->repo is set to a valid FileRepo instance.
getName()
Return the name of this file.
getThumbUrl( $suffix=false)
Get the URL of the thumbnail directory, or a particular file if $suffix is specified Stable to overri...
getVirtualUrl( $suffix=false)
Get the public zone virtual URL for a current version source file Stable to override.
assertTitleDefined()
Assert that $this->title is set to a Title.
getArchiveThumbPath( $archiveName, $suffix=false)
Get the path of an archived file's thumbs, or a particular thumb if $suffix is specified.
isMultipage()
Returns 'true' if this file is a type which supports multiple pages, e.g.
string $name
The name of a file from its title object.
FileRepo LocalRepo ForeignAPIRepo bool $repo
Some member variables can be lazy-initialised using __get().
getUrl()
Return the URL of the file Stable to override.
getHandler()
Get a MediaHandler instance for this file.
getArchiveThumbUrl( $archiveName, $suffix=false)
Get the URL of the archived file's thumbs, or a particular thumb if $suffix is specified Stable to ov...
static newForBacklinks(Title $title, $table, $params=[])
static singleton( $domain=false)
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
static queueRecursiveJobsForTable(Title $title, $table, $action='unknown', $userName='unknown')
Queue a RefreshLinks job for any table.
Helper class for file deletion.
@newable Stable to extend
Helper class for file movement.
Helper class for file undeletion.
Class to represent a local file in the wiki's own database.
exists()
canRender inherited
setProps( $info)
Set properties in this object to be equal to those given in the associative array $info.
string $major_mime
Major MIME type.
maybeUpgradeRow()
Upgrade a row if it needs it.
static newFromKey( $sha1, $repo, $timestamp=false)
Create a LocalFile from a SHA-1 key Do not call this except from inside a repo class.
getMediaType()
Returns the type of the media in the file.
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.
move( $target)
getLinksTo inherited
lock()
Start an atomic DB section and lock the image for update or increments a reference counter if the loc...
recordUpload2( $oldver, $comment, $pageText, $props=false, $timestamp=false, $user=null, $tags=[], $createNullRevision=true, $revert=false)
Record a file upload in the upload log and the image table.
loadFromRow( $row, $prefix='img_')
Load file metadata from a DB result row Stable to override.
getCacheKey()
Get the memcached key for the main data for this file, or false if there is no access to the shared c...
getWidth( $page=1)
Return the width of the image Stable to override.
__destruct()
Clean up any dangling locks.
string $mime
MIME type, determined by MimeAnalyzer::guessMimeType.
isMissing()
splitMime inherited
deleteOld( $archiveName, $reason, $suppress=false, $user=null)
Delete an old version of the file.
getDescriptionUrl()
isMultipage inherited
deleteFile( $reason, User $user, $suppress=false)
Delete all versions of the file.
getHistory( $limit=null, $start=null, $end=null, $inc=true)
purgeDescription inherited
getMutableCacheKeys(WANObjectCache $cache)
static getQueryInfo(array $options=[])
Return the tables, fields, and join conditions to be selected to create a new localfile object.
getDescription( $audience=self::FOR_PUBLIC, User $user=null)
Stable to override.
getSha1()
Stable to override.
loadFromDB( $flags=0)
Load file metadata from the DB Stable to override.
load( $flags=0)
Load file metadata from cache or DB, unless already loaded Stable to override.
bool $upgrading
Whether the row was scheduled to upgrade on load.
string $media_type
MEDIATYPE_xxx (bitmap, drawing, audio...)
loadFromFile()
Load metadata from the file itself.
purgeCache( $options=[])
Delete all previously generated thumbnails, refresh metadata in memcached and purge the CDN.
getDescriptionTouched()
Stable to override.
getBitDepth()
Stable to override.
int $size
Size in bytes (loadFromXxx)
string $minor_mime
Minor MIME type.
getDescriptionShortUrl()
Get short description URL for a file based on the page ID.
getThumbnails( $archiveName=false)
getTransformScript inherited
bool $upgraded
Whether the row was upgraded on load.
static newFromTitle( $title, $repo, $unused=null)
Create a LocalFile from a title Do not call this except from inside a repo class.
purgeMetadataCache()
Refresh metadata in memcached, but don't touch thumbnails or CDN.
string $timestamp
Upload timestamp.
const ATOMIC_SECTION_LOCK
purgeOldThumbnails( $archiveName)
Delete cached transformed files for an archived version only.
publishTo( $src, $dstRel, $flags=0, array $options=[])
Move or copy a file to a specified location.
purgeThumbList( $dir, $files)
Delete a list of thumbnails visible at urls Stable to override.
getUser( $type='text')
Returns user who uploaded the file Stable to override.
decodeRow( $row, $prefix='img_')
Decode a row from the database (either object or array) to an array with timestamps and MIME types de...
string $description
Description of current revision of the file.
const CACHE_FIELD_MAX_LEN
unlock()
Decrement the lock reference count and end the atomic section if it reaches zero.
IResultWrapper null $historyRes
Result of the query for the file's history (nextHistoryLine)
getLazyCacheFields( $prefix='img_')
Returns the list of object properties that are included as-is in the cache, only when they're not too...
getSize()
Returns the size of the image file, in bytes Stable to override.
loadExtraFieldsWithTimestamp( $dbr, $fname)
invalidateCache()
Purge the file object/metadata cache.
getMimeType()
Returns the MIME type of the file.
upload( $src, $comment, $pageText, $flags=0, $props=false, $timestamp=false, $user=null, $tags=[], $createNullRevision=true, $revert=false)
getHashPath inherited
bool $extraDataLoaded
Whether or not lazy-loaded data has been loaded from the database.
int $historyLine
Number of line to return by nextHistoryLine() (constructor)
string $sha1
SHA-1 base 36 content hash.
deleteOldFile( $archiveName, $reason, User $user, $suppress=false)
Delete an old version of the file.
getHeight( $page=1)
Return the height of the image Stable to override.
bool $locked
True if the image row is locked.
prerenderThumbnails()
Prerenders a configurable set of thumbnails Stable to override.
bool $lockedOwnTrx
True if the image row is locked with a lock initiated transaction.
resetHistory()
Reset the history pointer to the first element of the history Stable to override.
unprefixRow( $row, $prefix='img_')
static newFromRow( $row, $repo)
Create a LocalFile from a title Do not call this except from inside a repo class.
publish( $src, $flags=0, array $options=[])
Move or copy a file to its public location.
string $metadata
Handler-specific metadata.
restore( $versions=[], $unsuppress=false)
Restore all or specified deleted revisions to the given file.
getCacheFields( $prefix='img_')
Returns the list of object properties that are included as-is in the cache.
int $bits
Returned by getimagesize (loadFromXxx)
getTimestamp()
Stable to override.
bool $missing
True if file is not present in file system.
getDescriptionText(Language $lang=null)
Get the HTML text of the description page This is not used by ImagePage for local files,...
purgeThumbnails( $options=[])
Delete cached transformed files for the current version only.
loadExtraFromDB()
Load lazy file metadata from the DB.
nextHistoryLine()
Returns the history of this file, line by line.
upgradeRow()
Fix assorted version-related problems with the image row by reloading it from the file Stable to over...
int $deleted
Bitfield akin to rev_deleted.
loadFromCache()
Try to load file metadata from memcached, falling back to the database.
string $descriptionTouched
TS_MW timestamp of the last change of the file description.
getMetadata()
Get handler-specific metadata Stable to override.
__construct( $title, $repo)
Do not call this except from inside a repo class.
bool $dataLoaded
Whether or not core data has been loaded from the database (loadFromXxx)
bool $fileExists
Does the file exist on disk? (loadFromXxx)
recordUpload3(string $oldver, string $comment, string $pageText, User $user, $props=false, $timestamp=false, $tags=[], bool $createNullRevision=true, bool $revert=false)
Record a file upload in the upload log and the image table (version 3)
static makeParamBlob( $params)
Create a blob from a parameter array.
MimeMagic helper wrapper.
Class for creating new log entries and inserting them into the database.
static getQueryInfo(array $options=[])
Return the tables, fields, and join conditions to be selected to create a new oldlocalfile object.
Set options of the Parser.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
static newGood( $value=null)
Factory function for good results.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Job for asynchronous rendering of thumbnails.
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
getName()
Get the user name, or the IP of an anonymous user.
getId()
Get the user's ID.
getActorId(IDatabase $dbw=null)
Get the user's actor ID.
addWatch( $title, $checkRights=self::CHECK_USER_RIGHTS, ?string $expiry=null)
Watch an article.
Multi-datacenter aware caching interface.
Special handling for file pages.
if(count( $args)< 1) $job
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
if(!isset( $args[0])) $lang