Файловый менеджер - Редактировать - /var/www/html/Revision.zip
Ðазад
PK ! ��5r�� �� RevisionStore.phpnu �Iw�� <?php /** * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * Attribution notice: when this file was created, much of its content was taken * from the Revision.php file as present in release 1.30. Refer to the history * of that file for original authorship (that file was removed entirely in 1.37, * but its history can still be found in prior versions of MediaWiki). * * @file */ namespace MediaWiki\Revision; use InvalidArgumentException; use LogicException; use MediaWiki\CommentStore\CommentStore; use MediaWiki\CommentStore\CommentStoreComment; use MediaWiki\Content\Content; use MediaWiki\Content\FallbackContent; use MediaWiki\Content\IContentHandlerFactory; use MediaWiki\DAO\WikiAwareEntity; use MediaWiki\HookContainer\HookContainer; use MediaWiki\HookContainer\HookRunner; use MediaWiki\Linker\LinkTarget; use MediaWiki\MainConfigNames; use MediaWiki\MediaWikiServices; use MediaWiki\Page\LegacyArticleIdAccess; use MediaWiki\Page\PageIdentity; use MediaWiki\Page\PageIdentityValue; use MediaWiki\Page\PageStore; use MediaWiki\Permissions\Authority; use MediaWiki\Storage\BadBlobException; use MediaWiki\Storage\BlobAccessException; use MediaWiki\Storage\BlobStore; use MediaWiki\Storage\NameTableAccessException; use MediaWiki\Storage\NameTableStore; use MediaWiki\Storage\RevisionSlotsUpdate; use MediaWiki\Storage\SqlBlobStore; use MediaWiki\Title\Title; use MediaWiki\Title\TitleFactory; use MediaWiki\User\ActorStore; use MediaWiki\User\UserIdentity; use MediaWiki\Utils\MWTimestamp; use MWException; use MWUnknownContentModelException; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use RecentChange; use RuntimeException; use StatusValue; use stdClass; use Traversable; use Wikimedia\Assert\Assert; use Wikimedia\IPUtils; use Wikimedia\ObjectCache\BagOStuff; use Wikimedia\ObjectCache\WANObjectCache; use Wikimedia\Rdbms\Database; use Wikimedia\Rdbms\DBAccessObjectUtils; use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\IDBAccessObject; use Wikimedia\Rdbms\ILoadBalancer; use Wikimedia\Rdbms\IReadableDatabase; use Wikimedia\Rdbms\IResultWrapper; use Wikimedia\Rdbms\Platform\ISQLPlatform; use Wikimedia\Rdbms\SelectQueryBuilder; /** * Service for looking up page revisions. * * @since 1.31 * @since 1.32 Renamed from MediaWiki\Storage\RevisionStore * * @note This was written to act as a drop-in replacement for the corresponding * static methods in the old Revision class (which was later removed in 1.37). */ class RevisionStore implements RevisionFactory, RevisionLookup, LoggerAwareInterface { use LegacyArticleIdAccess; public const ROW_CACHE_KEY = 'revision-row-1.29'; public const ORDER_OLDEST_TO_NEWEST = 'ASC'; public const ORDER_NEWEST_TO_OLDEST = 'DESC'; // Constants for get(...)Between methods public const INCLUDE_OLD = 'include_old'; public const INCLUDE_NEW = 'include_new'; public const INCLUDE_BOTH = 'include_both'; /** * @var SqlBlobStore */ private $blobStore; /** * @var false|string */ private $wikiId; /** * @var ILoadBalancer */ private $loadBalancer; /** * @var WANObjectCache */ private $cache; /** * @var BagOStuff */ private $localCache; /** * @var CommentStore */ private $commentStore; /** @var ActorStore */ private $actorStore; /** * @var LoggerInterface */ private $logger; /** * @var NameTableStore */ private $contentModelStore; /** * @var NameTableStore */ private $slotRoleStore; /** @var SlotRoleRegistry */ private $slotRoleRegistry; /** @var IContentHandlerFactory */ private $contentHandlerFactory; /** @var HookRunner */ private $hookRunner; /** @var PageStore */ private $pageStore; /** @var TitleFactory */ private $titleFactory; /** * @param ILoadBalancer $loadBalancer * @param SqlBlobStore $blobStore * @param WANObjectCache $cache A cache for caching revision rows. This can be the local * wiki's default instance even if $wikiId refers to a different wiki, since * makeGlobalKey() is used to constructed a key that allows cached revision rows from * the same database to be re-used between wikis. For example, enwiki and frwiki will * use the same cache keys for revision rows from the wikidatawiki database, regardless * of the cache's default key space. * @param BagOStuff $localCache Another layer of cache, best to use APCu here. * @param CommentStore $commentStore * @param NameTableStore $contentModelStore * @param NameTableStore $slotRoleStore * @param SlotRoleRegistry $slotRoleRegistry * @param ActorStore $actorStore * @param IContentHandlerFactory $contentHandlerFactory * @param PageStore $pageStore * @param TitleFactory $titleFactory * @param HookContainer $hookContainer * @param false|string $wikiId Relevant wiki id or WikiAwareEntity::LOCAL for the current one * * @todo $blobStore should be allowed to be any BlobStore! * */ public function __construct( ILoadBalancer $loadBalancer, SqlBlobStore $blobStore, WANObjectCache $cache, BagOStuff $localCache, CommentStore $commentStore, NameTableStore $contentModelStore, NameTableStore $slotRoleStore, SlotRoleRegistry $slotRoleRegistry, ActorStore $actorStore, IContentHandlerFactory $contentHandlerFactory, PageStore $pageStore, TitleFactory $titleFactory, HookContainer $hookContainer, $wikiId = WikiAwareEntity::LOCAL ) { Assert::parameterType( [ 'string', 'false' ], $wikiId, '$wikiId' ); $this->loadBalancer = $loadBalancer; $this->blobStore = $blobStore; $this->cache = $cache; $this->localCache = $localCache; $this->commentStore = $commentStore; $this->contentModelStore = $contentModelStore; $this->slotRoleStore = $slotRoleStore; $this->slotRoleRegistry = $slotRoleRegistry; $this->actorStore = $actorStore; $this->wikiId = $wikiId; $this->logger = new NullLogger(); $this->contentHandlerFactory = $contentHandlerFactory; $this->pageStore = $pageStore; $this->titleFactory = $titleFactory; $this->hookRunner = new HookRunner( $hookContainer ); } public function setLogger( LoggerInterface $logger ) { $this->logger = $logger; } /** * @return bool Whether the store is read-only */ public function isReadOnly() { return $this->blobStore->isReadOnly(); } /** * Get the ID of the wiki this revision belongs to. * * @return string|false The wiki's logical name, of false to indicate the local wiki. */ public function getWikiId() { return $this->wikiId; } /** * @param int $queryFlags a bit field composed of READ_XXX flags * * @return IReadableDatabase */ private function getDBConnectionRefForQueryFlags( $queryFlags ) { if ( ( $queryFlags & IDBAccessObject::READ_LATEST ) == IDBAccessObject::READ_LATEST ) { return $this->getPrimaryConnection(); } else { return $this->getReplicaConnection(); } } /** * @param string|array $groups * @return IReadableDatabase */ private function getReplicaConnection( $groups = [] ) { // TODO: Replace with ICP return $this->loadBalancer->getConnection( DB_REPLICA, $groups, $this->wikiId ); } private function getPrimaryConnection(): IDatabase { // TODO: Replace with ICP return $this->loadBalancer->getConnection( DB_PRIMARY, [], $this->wikiId ); } /** * Determines the page Title based on the available information. * * MCR migration note: this corresponded to Revision::getTitle * * @deprecated since 1.36, Use RevisionRecord::getPage() instead. * @note The resulting Title object will be misleading if the RevisionStore is not * for the local wiki. * * @param int|null $pageId * @param int|null $revId * @param int $queryFlags * * @return Title * @throws RevisionAccessException */ public function getTitle( $pageId, $revId, $queryFlags = IDBAccessObject::READ_NORMAL ) { // TODO: Hard-deprecate this once getPage() returns a PageRecord. T195069 if ( $this->wikiId !== WikiAwareEntity::LOCAL ) { wfDeprecatedMsg( 'Using a Title object to refer to a page on another site.', '1.36' ); } $page = $this->getPage( $pageId, $revId, $queryFlags ); return $this->titleFactory->newFromPageIdentity( $page ); } /** * Determines the page based on the available information. * * @param int|null $pageId * @param int|null $revId * @param int $queryFlags * * @return PageIdentity * @throws RevisionAccessException */ private function getPage( ?int $pageId, ?int $revId, int $queryFlags = IDBAccessObject::READ_NORMAL ) { if ( !$pageId && !$revId ) { throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' ); } // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method if ( DBAccessObjectUtils::hasFlags( $queryFlags, IDBAccessObject::READ_LATEST_IMMUTABLE ) ) { $queryFlags = IDBAccessObject::READ_NORMAL; } // Loading by ID is best if ( $pageId !== null && $pageId > 0 ) { $page = $this->pageStore->getPageById( $pageId, $queryFlags ); if ( $page ) { return $this->wrapPage( $page ); } } // rev_id is defined as NOT NULL, but this revision may not yet have been inserted. if ( $revId !== null && $revId > 0 ) { $pageQuery = $this->pageStore->newSelectQueryBuilder( $queryFlags ) ->join( 'revision', null, 'page_id=rev_page' ) ->conds( [ 'rev_id' => $revId ] ) ->caller( __METHOD__ ); $page = $pageQuery->fetchPageRecord(); if ( $page ) { return $this->wrapPage( $page ); } } // If we still don't have a title, fallback to primary DB if that wasn't already happening. if ( $queryFlags === IDBAccessObject::READ_NORMAL ) { $title = $this->getPage( $pageId, $revId, IDBAccessObject::READ_LATEST ); if ( $title ) { $this->logger->info( __METHOD__ . ' fell back to READ_LATEST and got a Title.', [ 'exception' => new RuntimeException() ] ); return $title; } } throw new RevisionAccessException( 'Could not determine title for page ID {page_id} and revision ID {rev_id}', [ 'page_id' => $pageId, 'rev_id' => $revId, ] ); } /** * @param PageIdentity $page * * @return PageIdentity */ private function wrapPage( PageIdentity $page ): PageIdentity { if ( $this->wikiId === WikiAwareEntity::LOCAL ) { // NOTE: since there is still a lot of code that needs a full Title, // and uses Title::castFromPageIdentity() to get one, it's beneficial // to create a Title right away if we can, so we don't have to convert // over and over later on. // When there is less need to convert to Title, this special case can // be removed. return $this->titleFactory->newFromPageIdentity( $page ); } else { return $page; } } /** * @param mixed $value * @param string $name * * @throws IncompleteRevisionException if $value is null * @return mixed $value, if $value is not null */ private function failOnNull( $value, $name ) { if ( $value === null ) { throw new IncompleteRevisionException( "$name must not be " . var_export( $value, true ) . "!" ); } return $value; } /** * @param mixed $value * @param string $name * * @throws IncompleteRevisionException if $value is empty * @return mixed $value, if $value is not null */ private function failOnEmpty( $value, $name ) { if ( $value === null || $value === 0 || $value === '' ) { throw new IncompleteRevisionException( "$name must not be " . var_export( $value, true ) . "!" ); } return $value; } /** * Insert a new revision into the database, returning the new revision record * on success and dies horribly on failure. * * This should be followed up by a WikiPage::updateRevisionOn on call to update * page_latest on the page the revision is added to. * * MCR migration note: this replaced Revision::insertOn * * @param RevisionRecord $rev * @param IDatabase $dbw (primary connection) * * @return RevisionRecord the new revision record. */ public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) { // TODO: pass in a DBTransactionContext instead of a database connection. $this->checkDatabaseDomain( $dbw ); $slotRoles = $rev->getSlotRoles(); // Make sure the main slot is always provided throughout migration if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) { throw new IncompleteRevisionException( 'main slot must be provided' ); } // Checks $this->failOnNull( $rev->getSize(), 'size field' ); $this->failOnEmpty( $rev->getSha1(), 'sha1 field' ); $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' ); $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' ); $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' ); $this->failOnNull( $user->getId(), 'user field' ); $this->failOnEmpty( $user->getName(), 'user_text field' ); if ( !$rev->isReadyForInsertion() ) { // This is here for future-proofing. At the time this check being added, it // was redundant to the individual checks above. throw new IncompleteRevisionException( 'Revision is incomplete' ); } if ( $slotRoles == [ SlotRecord::MAIN ] ) { // T239717: If the main slot is the only slot, make sure the revision's nominal size // and hash match the main slot's nominal size and hash. $mainSlot = $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW ); Assert::precondition( $mainSlot->getSize() === $rev->getSize(), 'The revisions\'s size must match the main slot\'s size (see T239717)' ); Assert::precondition( $mainSlot->getSha1() === $rev->getSha1(), 'The revisions\'s SHA1 hash must match the main slot\'s SHA1 hash (see T239717)' ); } $pageId = $this->failOnEmpty( $rev->getPageId( $this->wikiId ), 'rev_page field' ); // check this early $parentId = $rev->getParentId() ?? $this->getPreviousRevisionId( $dbw, $rev ); /** @var RevisionRecord $rev */ $rev = $dbw->doAtomicSection( __METHOD__, function ( IDatabase $dbw, $fname ) use ( $rev, $user, $comment, $pageId, $parentId ) { return $this->insertRevisionInternal( $rev, $dbw, $user, $comment, $rev->getPage(), $pageId, $parentId ); } ); Assert::postcondition( $rev->getId( $this->wikiId ) > 0, 'revision must have an ID' ); Assert::postcondition( $rev->getPageId( $this->wikiId ) > 0, 'revision must have a page ID' ); Assert::postcondition( $rev->getComment( RevisionRecord::RAW ) !== null, 'revision must have a comment' ); Assert::postcondition( $rev->getUser( RevisionRecord::RAW ) !== null, 'revision must have a user' ); // Trigger exception if the main slot is missing. // Technically, this could go away after MCR migration: while // calling code may require a main slot to exist, RevisionStore // really should not know or care about that requirement. $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW ); foreach ( $slotRoles as $role ) { $slot = $rev->getSlot( $role, RevisionRecord::RAW ); Assert::postcondition( $slot->getContent() !== null, $role . ' slot must have content' ); Assert::postcondition( $slot->hasRevision(), $role . ' slot must have a revision associated' ); } $this->hookRunner->onRevisionRecordInserted( $rev ); return $rev; } /** * Update derived slots in an existing revision into the database, returning the modified * slots on success. * * @param RevisionRecord $revision After this method returns, the $revision object will be * obsolete in that it does not have the new slots. * @param RevisionSlotsUpdate $revisionSlotsUpdate * @param IDatabase $dbw (primary connection) * * @return SlotRecord[] the new slot records. * @internal */ public function updateSlotsOn( RevisionRecord $revision, RevisionSlotsUpdate $revisionSlotsUpdate, IDatabase $dbw ): array { $this->checkDatabaseDomain( $dbw ); // Make sure all modified and removed slots are derived slots foreach ( $revisionSlotsUpdate->getModifiedRoles() as $role ) { Assert::precondition( $this->slotRoleRegistry->getRoleHandler( $role )->isDerived(), 'Trying to modify a slot that is not derived' ); } foreach ( $revisionSlotsUpdate->getRemovedRoles() as $role ) { $isDerived = $this->slotRoleRegistry->getRoleHandler( $role )->isDerived(); Assert::precondition( $isDerived, 'Trying to remove a slot that is not derived' ); throw new LogicException( 'Removing derived slots is not yet implemented. See T277394.' ); } /** @var SlotRecord[] $slotRecords */ $slotRecords = $dbw->doAtomicSection( __METHOD__, function ( IDatabase $dbw, $fname ) use ( $revision, $revisionSlotsUpdate ) { return $this->updateSlotsInternal( $revision, $revisionSlotsUpdate, $dbw ); } ); foreach ( $slotRecords as $role => $slot ) { Assert::postcondition( $slot->getContent() !== null, $role . ' slot must have content' ); Assert::postcondition( $slot->hasRevision(), $role . ' slot must have a revision associated' ); } return $slotRecords; } /** * @param RevisionRecord $revision * @param RevisionSlotsUpdate $revisionSlotsUpdate * @param IDatabase $dbw * @return SlotRecord[] */ private function updateSlotsInternal( RevisionRecord $revision, RevisionSlotsUpdate $revisionSlotsUpdate, IDatabase $dbw ): array { $page = $revision->getPage(); $revId = $revision->getId( $this->wikiId ); $blobHints = [ BlobStore::PAGE_HINT => $page->getId( $this->wikiId ), BlobStore::REVISION_HINT => $revId, BlobStore::PARENT_HINT => $revision->getParentId( $this->wikiId ), ]; $newSlots = []; foreach ( $revisionSlotsUpdate->getModifiedRoles() as $role ) { $slot = $revisionSlotsUpdate->getModifiedSlot( $role ); $newSlots[$role] = $this->insertSlotOn( $dbw, $revId, $slot, $page, $blobHints ); } return $newSlots; } private function insertRevisionInternal( RevisionRecord $rev, IDatabase $dbw, UserIdentity $user, CommentStoreComment $comment, PageIdentity $page, $pageId, $parentId ) { $slotRoles = $rev->getSlotRoles(); $revisionRow = $this->insertRevisionRowOn( $dbw, $rev, $parentId ); $revisionId = $revisionRow['rev_id']; $blobHints = [ BlobStore::PAGE_HINT => $pageId, BlobStore::REVISION_HINT => $revisionId, BlobStore::PARENT_HINT => $parentId, ]; $newSlots = []; foreach ( $slotRoles as $role ) { $slot = $rev->getSlot( $role, RevisionRecord::RAW ); // If the SlotRecord already has a revision ID set, this means it already exists // in the database, and should already belong to the current revision. // However, a slot may already have a revision, but no content ID, if the slot // is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD // mode, and the respective archive row was not yet migrated to the new schema. // In that case, a new slot row (and content row) must be inserted even during // undeletion. if ( $slot->hasRevision() && $slot->hasContentId() ) { // TODO: properly abort transaction if the assertion fails! Assert::parameter( $slot->getRevision() === $revisionId, 'slot role ' . $slot->getRole(), 'Existing slot should belong to revision ' . $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!' ); // Slot exists, nothing to do, move along. // This happens when restoring archived revisions. $newSlots[$role] = $slot; } else { $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $page, $blobHints ); } } $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId ); $rev = new RevisionStoreRecord( $page, $user, $comment, (object)$revisionRow, new RevisionSlots( $newSlots ), $this->wikiId ); return $rev; } /** * @param IDatabase $dbw * @param int $revisionId * @param SlotRecord $protoSlot * @param PageIdentity $page * @param array $blobHints See the BlobStore::XXX_HINT constants * @return SlotRecord */ private function insertSlotOn( IDatabase $dbw, $revisionId, SlotRecord $protoSlot, PageIdentity $page, array $blobHints = [] ) { if ( $protoSlot->hasAddress() ) { $blobAddress = $protoSlot->getAddress(); } else { $blobAddress = $this->storeContentBlob( $protoSlot, $page, $blobHints ); } if ( $protoSlot->hasContentId() ) { $contentId = $protoSlot->getContentId(); } else { $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress ); } $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId ); return SlotRecord::newSaved( $revisionId, $contentId, $blobAddress, $protoSlot ); } /** * Insert IP revision into ip_changes for use when querying for a range. * @param IDatabase $dbw * @param UserIdentity $user * @param RevisionRecord $rev * @param int $revisionId */ private function insertIpChangesRow( IDatabase $dbw, UserIdentity $user, RevisionRecord $rev, $revisionId ) { if ( !$user->isRegistered() && IPUtils::isValid( $user->getName() ) ) { $dbw->newInsertQueryBuilder() ->insertInto( 'ip_changes' ) ->row( [ 'ipc_rev_id' => $revisionId, 'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ), 'ipc_hex' => IPUtils::toHex( $user->getName() ), ] ) ->caller( __METHOD__ )->execute(); } } /** * @param IDatabase $dbw * @param RevisionRecord $rev * @param int $parentId * * @return array a revision table row * * @throws MWException * @throws MWUnknownContentModelException */ private function insertRevisionRowOn( IDatabase $dbw, RevisionRecord $rev, $parentId ) { $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $parentId ); $revisionRow += $this->commentStore->insert( $dbw, 'rev_comment', $rev->getComment( RevisionRecord::RAW ) ); $dbw->newInsertQueryBuilder() ->insertInto( 'revision' ) ->row( $revisionRow ) ->caller( __METHOD__ )->execute(); if ( !isset( $revisionRow['rev_id'] ) ) { // only if auto-increment was used $revisionRow['rev_id'] = intval( $dbw->insertId() ); if ( $dbw->getType() === 'mysql' ) { // (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the // auto-increment value to disk, so on server restart it might reuse IDs from deleted // revisions. We can fix that with an insert with an explicit rev_id value, if necessary. $maxRevId = intval( $dbw->newSelectQueryBuilder() ->select( 'MAX(ar_rev_id)' ) ->from( 'archive' ) ->caller( __METHOD__ ) ->fetchField() ); $table = 'archive'; $maxRevId2 = intval( $dbw->newSelectQueryBuilder() ->select( 'MAX(slot_revision_id)' ) ->from( 'slots' ) ->caller( __METHOD__ ) ->fetchField() ); if ( $maxRevId2 >= $maxRevId ) { $maxRevId = $maxRevId2; $table = 'slots'; } if ( $maxRevId >= $revisionRow['rev_id'] ) { $this->logger->debug( '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.' . ' Trying to fix it.', [ 'revid' => $revisionRow['rev_id'], 'table' => $table, 'maxrevid' => $maxRevId, ] ); if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) { throw new MWException( 'Failed to get database lock for T202032' ); } $fname = __METHOD__; $dbw->onTransactionResolution( static function ( $trigger, IDatabase $dbw ) use ( $fname ) { $dbw->unlock( 'fix-for-T202032', $fname ); }, __METHOD__ ); $dbw->newDeleteQueryBuilder() ->deleteFrom( 'revision' ) ->where( [ 'rev_id' => $revisionRow['rev_id'] ] ) ->caller( __METHOD__ )->execute(); // The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction // isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing // inserts too, though, at least on MariaDB 10.1.29. // // Don't try to lock `revision` in this way, it'll deadlock if there are concurrent // transactions in this code path thanks to the row lock from the original ->insert() above. // // And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning // that's for non-MySQL DBs. $row1 = $dbw->query( $dbw->selectSQLText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE', __METHOD__ )->fetchObject(); $row2 = $dbw->query( $dbw->selectSQLText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ ) . ' FOR UPDATE', __METHOD__ )->fetchObject(); $maxRevId = max( $maxRevId, $row1 ? intval( $row1->v ) : 0, $row2 ? intval( $row2->v ) : 0 ); // If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent // transactions will throw a duplicate key error here. It doesn't seem worth trying // to avoid that. $revisionRow['rev_id'] = $maxRevId + 1; $dbw->newInsertQueryBuilder() ->insertInto( 'revision' ) ->row( $revisionRow ) ->caller( __METHOD__ )->execute(); } } } return $revisionRow; } /** * @param IDatabase $dbw * @param RevisionRecord $rev * @param int $parentId * * @return array a revision table row */ private function getBaseRevisionRow( IDatabase $dbw, RevisionRecord $rev, $parentId ) { // Record the edit in revisions $revisionRow = [ 'rev_page' => $rev->getPageId( $this->wikiId ), 'rev_parent_id' => $parentId, 'rev_actor' => $this->actorStore->acquireActorId( $rev->getUser( RevisionRecord::RAW ), $dbw ), 'rev_minor_edit' => $rev->isMinor() ? 1 : 0, 'rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ), 'rev_deleted' => $rev->getVisibility(), 'rev_len' => $rev->getSize(), 'rev_sha1' => $rev->getSha1(), ]; if ( $rev->getId( $this->wikiId ) !== null ) { // Needed to restore revisions with their original ID $revisionRow['rev_id'] = $rev->getId( $this->wikiId ); } return $revisionRow; } /** * @param SlotRecord $slot * @param PageIdentity $page * @param array $blobHints See the BlobStore::XXX_HINT constants * * @throws MWException * @return string the blob address */ private function storeContentBlob( SlotRecord $slot, PageIdentity $page, array $blobHints = [] ) { $content = $slot->getContent(); $format = $content->getDefaultFormat(); $model = $content->getModel(); $this->checkContent( $content, $page, $slot->getRole() ); return $this->blobStore->storeBlob( $content->serialize( $format ), // These hints "leak" some information from the higher abstraction layer to // low level storage to allow for optimization. array_merge( $blobHints, [ BlobStore::DESIGNATION_HINT => 'page-content', BlobStore::ROLE_HINT => $slot->getRole(), BlobStore::SHA1_HINT => $slot->getSha1(), BlobStore::MODEL_HINT => $model, BlobStore::FORMAT_HINT => $format, ] ) ); } /** * @param SlotRecord $slot * @param IDatabase $dbw * @param int $revisionId * @param int $contentId */ private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) { $dbw->newInsertQueryBuilder() ->insertInto( 'slots' ) ->row( [ 'slot_revision_id' => $revisionId, 'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ), 'slot_content_id' => $contentId, // If the slot has a specific origin use that ID, otherwise use the ID of the revision // that we just inserted. 'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId, ] ) ->caller( __METHOD__ )->execute(); } /** * @param SlotRecord $slot * @param IDatabase $dbw * @param string $blobAddress * @return int content row ID */ private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) { $dbw->newInsertQueryBuilder() ->insertInto( 'content' ) ->row( [ 'content_size' => $slot->getSize(), 'content_sha1' => $slot->getSha1(), 'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ), 'content_address' => $blobAddress, ] ) ->caller( __METHOD__ )->execute(); return intval( $dbw->insertId() ); } /** * MCR migration note: this corresponded to Revision::checkContentModel * * @param Content $content * @param PageIdentity $page * @param string $role * * @throws MWException * @throws MWUnknownContentModelException */ private function checkContent( Content $content, PageIdentity $page, string $role ) { // Note: may return null for revisions that have not yet been inserted $model = $content->getModel(); $format = $content->getDefaultFormat(); $handler = $content->getContentHandler(); if ( !$handler->isSupportedFormat( $format ) ) { throw new MWException( "Can't use format $format with content model $model on $page role $role" ); } if ( !$content->isValid() ) { throw new MWException( "New content for $page role $role is not valid! Content model is $model" ); } } /** * Create a new null-revision for insertion into a page's * history. This will not re-save the text, but simply refer * to the text from the previous version. * * Such revisions can for instance identify page rename * operations and other such meta-modifications. * * @note This method grabs a FOR UPDATE lock on the relevant row of the page table, * to prevent a new revision from being inserted before the null revision has been written * to the database. * * MCR migration note: this replaced Revision::newNullRevision * * @todo Introduce newFromParentRevision(). newNullRevision can then be based on that * (or go away). * * @param IDatabase $dbw used for obtaining the lock on the page table row * @param PageIdentity $page the page to read from * @param CommentStoreComment $comment RevisionRecord's summary * @param bool $minor Whether the revision should be considered as minor * @param UserIdentity $user The user to attribute the revision to * * @return RevisionRecord|null RevisionRecord or null on error */ public function newNullRevision( IDatabase $dbw, PageIdentity $page, CommentStoreComment $comment, $minor, UserIdentity $user ) { $this->checkDatabaseDomain( $dbw ); $pageId = $this->getArticleId( $page ); // T51581: Lock the page table row to ensure no other process // is adding a revision to the page at the same time. // Avoid locking extra tables, compare T191892. $pageLatest = $dbw->newSelectQueryBuilder() ->select( 'page_latest' ) ->forUpdate() ->from( 'page' ) ->where( [ 'page_id' => $pageId ] ) ->caller( __METHOD__ )->fetchField(); if ( !$pageLatest ) { $msg = 'T235589: Failed to select table row during null revision creation' . " Page id '$pageId' does not exist."; $this->logger->error( $msg, [ 'exception' => new RuntimeException( $msg ) ] ); return null; } // Fetch the actual revision row from primary DB, without locking all extra tables. $oldRevision = $this->loadRevisionFromConds( $dbw, [ 'rev_id' => intval( $pageLatest ) ], IDBAccessObject::READ_LATEST, $page ); if ( !$oldRevision ) { $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId."; $this->logger->error( $msg, [ 'exception' => new RuntimeException( $msg ) ] ); return null; } // Construct the new revision $timestamp = MWTimestamp::now( TS_MW ); $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision ); $newRevision->setComment( $comment ); $newRevision->setUser( $user ); $newRevision->setTimestamp( $timestamp ); $newRevision->setMinorEdit( $minor ); return $newRevision; } /** * MCR migration note: this replaced Revision::isUnpatrolled * * @todo This is overly specific, so move or kill this method. * * @param RevisionRecord $rev * * @return int Rcid of the unpatrolled row, zero if there isn't one */ public function getRcIdIfUnpatrolled( RevisionRecord $rev ) { $rc = $this->getRecentChange( $rev ); if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) { return $rc->getAttribute( 'rc_id' ); } else { return 0; } } /** * Get the RC object belonging to the current revision, if there's one * * MCR migration note: this replaced Revision::getRecentChange * * @todo move this somewhere else? * * @param RevisionRecord $rev * @param int $flags (optional) $flags include: * IDBAccessObject::READ_LATEST: Select the data from the primary DB * * @return null|RecentChange */ public function getRecentChange( RevisionRecord $rev, $flags = 0 ) { if ( ( $flags & IDBAccessObject::READ_LATEST ) == IDBAccessObject::READ_LATEST ) { $dbType = DB_PRIMARY; } else { $dbType = DB_REPLICA; } $rc = RecentChange::newFromConds( [ 'rc_this_oldid' => $rev->getId( $this->wikiId ), // rc_this_oldid does not have to be unique, // in particular, it is shared with categorization // changes. Prefer the original change because callers // often expect a change for patrolling. 'rc_type' => [ RC_EDIT, RC_NEW, RC_LOG ], ], __METHOD__, $dbType ); // XXX: cache this locally? Glue it to the RevisionRecord? return $rc; } /** * Loads a Content object based on a slot row. * * This method does not call $slot->getContent(), and may be used as a callback * called by $slot->getContent(). * * MCR migration note: this roughly corresponded to Revision::getContentInternal * * @param SlotRecord $slot The SlotRecord to load content for * @param string|null $blobData The content blob, in the form indicated by $blobFlags * @param string|null $blobFlags Flags indicating how $blobData needs to be processed. * Use null if no processing should happen. That is in contrast to the empty string, * which causes the blob to be decoded according to the configured legacy encoding. * @param string|null $blobFormat MIME type indicating how $dataBlob is encoded * @param int $queryFlags * * @throws RevisionAccessException * @return Content */ private function loadSlotContent( SlotRecord $slot, ?string $blobData = null, ?string $blobFlags = null, ?string $blobFormat = null, int $queryFlags = 0 ) { if ( $blobData !== null ) { $blobAddress = $slot->hasAddress() ? $slot->getAddress() : null; if ( $blobFlags === null ) { // No blob flags, so use the blob verbatim. $data = $blobData; } else { try { $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $blobAddress ); } catch ( BadBlobException $e ) { throw new BadRevisionException( $e->getMessage(), [], 0, $e ); } if ( $data === false ) { throw new RevisionAccessException( 'Failed to expand blob data using flags {flags} (key: {cache_key})', [ 'flags' => $blobFlags, 'cache_key' => $blobAddress, ] ); } } } else { $address = $slot->getAddress(); try { $data = $this->blobStore->getBlob( $address, $queryFlags ); } catch ( BadBlobException $e ) { throw new BadRevisionException( $e->getMessage(), [], 0, $e ); } catch ( BlobAccessException $e ) { throw new RevisionAccessException( 'Failed to load data blob from {address} for revision {revision}. ' . 'If this problem persist, use the findBadBlobs maintenance script ' . 'to investigate the issue and mark bad blobs.', [ 'address' => $e->getMessage(), 'revision' => $slot->getRevision() ], 0, $e ); } } $model = $slot->getModel(); // If the content model is not known, don't fail here (T220594, T220793, T228921) if ( !$this->contentHandlerFactory->isDefinedModel( $model ) ) { $this->logger->warning( "Undefined content model '$model', falling back to FallbackContent", [ 'content_address' => $slot->getAddress(), 'rev_id' => $slot->getRevision(), 'role_name' => $slot->getRole(), 'model_name' => $model, 'exception' => new RuntimeException() ] ); return new FallbackContent( $data, $model ); } return $this->contentHandlerFactory ->getContentHandler( $model ) ->unserializeContent( $data, $blobFormat ); } /** * Load a page revision from a given revision ID number. * Returns null if no such revision can be found. * * MCR migration note: this replaced Revision::newFromId * * $flags include: * IDBAccessObject::READ_LATEST: Select the data from the primary DB * IDBAccessObject::READ_LOCKING : Select & lock the data from the primary DB * * @param int $id * @param int $flags (optional) * @param PageIdentity|null $page The page the revision belongs to. * Providing the page may improve performance. * * @return RevisionRecord|null */ public function getRevisionById( $id, $flags = 0, ?PageIdentity $page = null ) { return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags, $page ); } /** * Load either the current, or a specified, revision * that's attached to a given link target. If not attached * to that link target, will return null. * * MCR migration note: this replaced Revision::newFromTitle * * $flags include: * IDBAccessObject::READ_LATEST: Select the data from the primary DB * IDBAccessObject::READ_LOCKING : Select & lock the data from the primary DB * * @param LinkTarget|PageIdentity $page Calling with LinkTarget is deprecated since 1.36 * @param int $revId (optional) * @param int $flags Bitfield (optional) * @return RevisionRecord|null */ public function getRevisionByTitle( $page, $revId = 0, $flags = 0 ) { $conds = [ 'page_namespace' => $page->getNamespace(), 'page_title' => $page->getDBkey() ]; if ( $page instanceof LinkTarget ) { // Only resolve LinkTarget to a Title when operating in the context of the local wiki (T248756) $page = $this->wikiId === WikiAwareEntity::LOCAL ? Title::castFromLinkTarget( $page ) : null; } if ( $revId ) { // Use the specified revision ID. // Note that we use newRevisionFromConds here because we want to retry // and fall back to primary DB if the page is not found on a replica. // Since the caller supplied a revision ID, we are pretty sure the revision is // supposed to exist, so we should try hard to find it. $conds['rev_id'] = $revId; return $this->newRevisionFromConds( $conds, $flags, $page ); } else { // Use a join to get the latest revision. // Note that we don't use newRevisionFromConds here because we don't want to retry // and fall back to primary DB. The assumption is that we only want to force the fallback // if we are quite sure the revision exists because the caller supplied a revision ID. // If the page isn't found at all on a replica, it probably simply does not exist. $db = $this->getDBConnectionRefForQueryFlags( $flags ); $conds[] = 'rev_id=page_latest'; return $this->loadRevisionFromConds( $db, $conds, $flags, $page ); } } /** * Load either the current, or a specified, revision * that's attached to a given page ID. * Returns null if no such revision can be found. * * MCR migration note: this replaced Revision::newFromPageId * * $flags include: * IDBAccessObject::READ_LATEST: Select the data from the primary DB (since 1.20) * IDBAccessObject::READ_LOCKING : Select & lock the data from the primary DB * * @param int $pageId * @param int $revId (optional) * @param int $flags Bitfield (optional) * @return RevisionRecord|null */ public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) { $conds = [ 'page_id' => $pageId ]; if ( $revId ) { // Use the specified revision ID. // Note that we use newRevisionFromConds here because we want to retry // and fall back to primary DB if the page is not found on a replica. // Since the caller supplied a revision ID, we are pretty sure the revision is // supposed to exist, so we should try hard to find it. $conds['rev_id'] = $revId; return $this->newRevisionFromConds( $conds, $flags ); } else { // Use a join to get the latest revision. // Note that we don't use newRevisionFromConds here because we don't want to retry // and fall back to primary DB. The assumption is that we only want to force the fallback // if we are quite sure the revision exists because the caller supplied a revision ID. // If the page isn't found at all on a replica, it probably simply does not exist. $db = $this->getDBConnectionRefForQueryFlags( $flags ); $conds[] = 'rev_id=page_latest'; return $this->loadRevisionFromConds( $db, $conds, $flags ); } } /** * Load the revision for the given title with the given timestamp. * WARNING: Timestamps may in some circumstances not be unique, * so this isn't the best key to use. * * MCR migration note: this replaced Revision::loadFromTimestamp * * @param LinkTarget|PageIdentity $page Calling with LinkTarget is deprecated since 1.36 * @param string $timestamp * @param int $flags Bitfield (optional) include: * IDBAccessObject::READ_LATEST: Select the data from the primary DB * IDBAccessObject::READ_LOCKING: Select & lock the data from the primary DB * Default: IDBAccessObject::READ_NORMAL * @return RevisionRecord|null */ public function getRevisionByTimestamp( $page, string $timestamp, int $flags = IDBAccessObject::READ_NORMAL ): ?RevisionRecord { if ( $page instanceof LinkTarget ) { // Only resolve LinkTarget to a Title when operating in the context of the local wiki (T248756) $page = $this->wikiId === WikiAwareEntity::LOCAL ? Title::castFromLinkTarget( $page ) : null; } $db = $this->getDBConnectionRefForQueryFlags( $flags ); return $this->newRevisionFromConds( [ 'rev_timestamp' => $db->timestamp( $timestamp ), 'page_namespace' => $page->getNamespace(), 'page_title' => $page->getDBkey() ], $flags, $page ); } /** * @param int $revId The revision to load slots for. * @param int $queryFlags * @param PageIdentity $page * * @return SlotRecord[] */ private function loadSlotRecords( $revId, $queryFlags, PageIdentity $page ) { // TODO: Find a way to add NS_MODULE from Scribunto here if ( $page->getNamespace() !== NS_TEMPLATE ) { $res = $this->loadSlotRecordsFromDb( $revId, $queryFlags, $page ); return $this->constructSlotRecords( $revId, $res, $queryFlags, $page ); } $ttl = MediaWikiServices::getInstance() ->getMainConfig() ->get( MainConfigNames::RevisionSlotsCacheExpiry ); // TODO: These caches should not be needed. See T297147#7563670 $res = $this->localCache->getWithSetCallback( $this->localCache->makeKey( 'revision-slots', $page->getWikiId(), $page->getId( $page->getWikiId() ), $revId ), $ttl['local'] ?? $this->localCache::TTL_UNCACHEABLE, function () use ( $revId, $queryFlags, $page, $ttl ) { return $this->cache->getWithSetCallback( $this->cache->makeKey( 'revision-slots', $page->getWikiId(), $page->getId( $page->getWikiId() ), $revId ), $ttl['WAN'] ?? WANObjectCache::TTL_UNCACHEABLE, function () use ( $revId, $queryFlags, $page ) { $res = $this->loadSlotRecordsFromDb( $revId, $queryFlags, $page ); if ( !$res ) { // Avoid caching return false; } return $res; } ); } ); if ( !$res ) { $res = []; } return $this->constructSlotRecords( $revId, $res, $queryFlags, $page ); } private function loadSlotRecordsFromDb( $revId, $queryFlags, PageIdentity $page ): array { $revQuery = $this->getSlotsQueryInfo( [ 'content' ] ); $db = $this->getDBConnectionRefForQueryFlags( $queryFlags ); $res = $db->newSelectQueryBuilder() ->queryInfo( $revQuery ) ->where( [ 'slot_revision_id' => $revId ] ) ->recency( $queryFlags ) ->caller( __METHOD__ )->fetchResultSet(); if ( !$res->numRows() && !( $queryFlags & IDBAccessObject::READ_LATEST ) ) { // If we found no slots, try looking on the primary database (T212428, T252156) $this->logger->info( __METHOD__ . ' falling back to READ_LATEST.', [ 'revid' => $revId, 'exception' => new RuntimeException(), ] ); return $this->loadSlotRecordsFromDb( $revId, $queryFlags | IDBAccessObject::READ_LATEST, $page ); } return iterator_to_array( $res ); } /** * Factory method for SlotRecords based on known slot rows. * * @param int $revId The revision to load slots for. * @param \stdClass[]|IResultWrapper $slotRows * @param int $queryFlags * @param PageIdentity $page * @param array|null $slotContents a map from blobAddress to slot * content blob or Content object. * * @return SlotRecord[] */ private function constructSlotRecords( $revId, $slotRows, $queryFlags, PageIdentity $page, $slotContents = null ) { $slots = []; foreach ( $slotRows as $row ) { // Resolve role names and model names from in-memory cache, if they were not joined in. if ( !isset( $row->role_name ) ) { $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id ); } if ( !isset( $row->model_name ) ) { if ( isset( $row->content_model ) ) { $row->model_name = $this->contentModelStore->getName( (int)$row->content_model ); } else { // We may get here if $row->model_name is set but null, perhaps because it // came from rev_content_model, which is NULL for the default model. $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( $row->role_name ); $row->model_name = $slotRoleHandler->getDefaultModel( $page ); } } // We may have a fake blob_data field from getSlotRowsForBatch(), use it! if ( isset( $row->blob_data ) ) { $slotContents[$row->content_address] = $row->blob_data; } $contentCallback = function ( SlotRecord $slot ) use ( $slotContents, $queryFlags ) { $blob = null; if ( isset( $slotContents[$slot->getAddress()] ) ) { $blob = $slotContents[$slot->getAddress()]; if ( $blob instanceof Content ) { return $blob; } } return $this->loadSlotContent( $slot, $blob, null, null, $queryFlags ); }; $slots[$row->role_name] = new SlotRecord( $row, $contentCallback ); } if ( !isset( $slots[SlotRecord::MAIN] ) ) { $this->logger->error( __METHOD__ . ': Main slot of revision not found in database. See T212428.', [ 'revid' => $revId, 'queryFlags' => $queryFlags, 'exception' => new RuntimeException(), ] ); throw new RevisionAccessException( 'Main slot of revision not found in database. See T212428.' ); } return $slots; } /** * Factory method for RevisionSlots based on a revision ID. * * @note If other code has a need to construct RevisionSlots objects, this should be made * public, since RevisionSlots instances should not be constructed directly. * * @param int $revId * @param \stdClass[]|null $slotRows * @param int $queryFlags * @param PageIdentity $page * * @return RevisionSlots */ private function newRevisionSlots( $revId, $slotRows, $queryFlags, PageIdentity $page ) { if ( $slotRows ) { $slots = new RevisionSlots( $this->constructSlotRecords( $revId, $slotRows, $queryFlags, $page ) ); } else { $slots = new RevisionSlots( function () use( $revId, $queryFlags, $page ) { return $this->loadSlotRecords( $revId, $queryFlags, $page ); } ); } return $slots; } /** * Make a fake RevisionRecord object from an archive table row. This is queried * for permissions or even inserted (as in Special:Undelete) * * The user ID and user name may optionally be supplied using the aliases * ar_user and ar_user_text (the names of fields which existed before * MW 1.34). * * MCR migration note: this replaced Revision::newFromArchiveRow * * @param \stdClass $row * @param int $queryFlags * @param PageIdentity|null $page * @param array $overrides associative array with fields of $row to override. This may be * used e.g. to force the parent revision ID or page ID. Keys in the array are fields * names from the archive table without the 'ar_' prefix, i.e. use 'parent_id' to * override ar_parent_id. * * @return RevisionRecord */ public function newRevisionFromArchiveRow( $row, $queryFlags = 0, ?PageIdentity $page = null, array $overrides = [] ) { return $this->newRevisionFromArchiveRowAndSlots( $row, null, $queryFlags, $page, $overrides ); } /** * @see RevisionFactory::newRevisionFromRow * * MCR migration note: this replaced Revision::newFromRow * * @param \stdClass $row A database row generated from a query based on RevisionSelectQueryBuilder * @param int $queryFlags * @param PageIdentity|null $page Preloaded page object * @param bool $fromCache if true, the returned RevisionRecord will ensure that no stale * data is returned from getters, by querying the database as needed * @return RevisionRecord */ public function newRevisionFromRow( $row, $queryFlags = 0, ?PageIdentity $page = null, $fromCache = false ) { return $this->newRevisionFromRowAndSlots( $row, null, $queryFlags, $page, $fromCache ); } /** * @see newRevisionFromArchiveRow() * @since 1.35 * * @param stdClass $row * @param null|stdClass[]|RevisionSlots $slots * - Database rows generated from a query based on getSlotsQueryInfo * with the 'content' flag set. Or * - RevisionSlots instance * @param int $queryFlags * @param PageIdentity|null $page * @param array $overrides associative array with fields of $row to override. This may be * used e.g. to force the parent revision ID or page ID. Keys in the array are fields * names from the archive table without the 'ar_' prefix, i.e. use 'parent_id' to * override ar_parent_id. * * @return RevisionRecord */ public function newRevisionFromArchiveRowAndSlots( stdClass $row, $slots, int $queryFlags = 0, ?PageIdentity $page = null, array $overrides = [] ) { if ( !$page && isset( $overrides['title'] ) ) { if ( !( $overrides['title'] instanceof PageIdentity ) ) { throw new InvalidArgumentException( 'title field override must contain a PageIdentity object.' ); } $page = $overrides['title']; } if ( !isset( $page ) ) { if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) { $page = Title::makeTitle( $row->ar_namespace, $row->ar_title ); } else { throw new InvalidArgumentException( 'A Title or ar_namespace and ar_title must be given' ); } } foreach ( $overrides as $key => $value ) { $field = "ar_$key"; $row->$field = $value; } try { $user = $this->actorStore->newActorFromRowFields( $row->ar_user ?? null, $row->ar_user_text ?? null, $row->ar_actor ?? null ); } catch ( InvalidArgumentException $ex ) { $this->logger->warning( 'Could not load user for archive revision {rev_id}', [ 'ar_rev_id' => $row->ar_rev_id, 'ar_actor' => $row->ar_actor ?? 'null', 'ar_user_text' => $row->ar_user_text ?? 'null', 'ar_user' => $row->ar_user ?? 'null', 'exception' => $ex ] ); $user = $this->actorStore->getUnknownActor(); } $db = $this->getDBConnectionRefForQueryFlags( $queryFlags ); // Legacy because $row may have come from self::selectFields() $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true ); if ( !( $slots instanceof RevisionSlots ) ) { $slots = $this->newRevisionSlots( (int)$row->ar_rev_id, $slots, $queryFlags, $page ); } return new RevisionArchiveRecord( $page, $user, $comment, $row, $slots, $this->wikiId ); } /** * @see newFromRevisionRow() * * @param stdClass $row A database row generated from a query based on RevisionSelectQueryBuilder * @param null|stdClass[]|RevisionSlots $slots * - Database rows generated from a query based on getSlotsQueryInfo * with the 'content' flag set. Or * - RevisionSlots instance * @param int $queryFlags * @param PageIdentity|null $page * @param bool $fromCache if true, the returned RevisionRecord will ensure that no stale * data is returned from getters, by querying the database as needed * * @return RevisionRecord * @throws RevisionAccessException * @see RevisionFactory::newRevisionFromRow */ public function newRevisionFromRowAndSlots( stdClass $row, $slots, int $queryFlags = 0, ?PageIdentity $page = null, bool $fromCache = false ) { if ( !$page ) { if ( isset( $row->page_id ) && isset( $row->page_namespace ) && isset( $row->page_title ) ) { $page = new PageIdentityValue( (int)$row->page_id, (int)$row->page_namespace, $row->page_title, $this->wikiId ); $page = $this->wrapPage( $page ); } else { $pageId = (int)( $row->rev_page ?? 0 ); $revId = (int)( $row->rev_id ?? 0 ); $page = $this->getPage( $pageId, $revId, $queryFlags ); } } else { $page = $this->ensureRevisionRowMatchesPage( $row, $page ); } if ( !$page ) { // This should already have been caught about, but apparently // it not always is, see T286877. throw new RevisionAccessException( "Failed to determine page associated with revision {$row->rev_id}" ); } try { $user = $this->actorStore->newActorFromRowFields( $row->rev_user ?? null, $row->rev_user_text ?? null, $row->rev_actor ?? null ); } catch ( InvalidArgumentException $ex ) { $this->logger->warning( 'Could not load user for revision {rev_id}', [ 'rev_id' => $row->rev_id, 'rev_actor' => $row->rev_actor ?? 'null', 'rev_user_text' => $row->rev_user_text ?? 'null', 'rev_user' => $row->rev_user ?? 'null', 'exception' => $ex ] ); $user = $this->actorStore->getUnknownActor(); } $db = $this->getDBConnectionRefForQueryFlags( $queryFlags ); // Legacy because $row may have come from self::selectFields() $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true ); if ( !( $slots instanceof RevisionSlots ) ) { $slots = $this->newRevisionSlots( (int)$row->rev_id, $slots, $queryFlags, $page ); } // If this is a cached row, instantiate a cache-aware RevisionRecord to avoid stale data. if ( $fromCache ) { $rev = new RevisionStoreCacheRecord( function ( $revId ) use ( $queryFlags ) { $db = $this->getDBConnectionRefForQueryFlags( $queryFlags ); $row = $this->fetchRevisionRowFromConds( $db, [ 'rev_id' => intval( $revId ) ] ); if ( !$row && !( $queryFlags & IDBAccessObject::READ_LATEST ) ) { // If we found no slots, try looking on the primary database (T259738) $this->logger->info( 'RevisionStoreCacheRecord refresh callback falling back to READ_LATEST.', [ 'revid' => $revId, 'exception' => new RuntimeException(), ] ); $dbw = $this->getDBConnectionRefForQueryFlags( IDBAccessObject::READ_LATEST ); $row = $this->fetchRevisionRowFromConds( $dbw, [ 'rev_id' => intval( $revId ) ] ); } if ( !$row ) { return [ null, null ]; } return [ $row->rev_deleted, $this->actorStore->newActorFromRowFields( $row->rev_user ?? null, $row->rev_user_text ?? null, $row->rev_actor ?? null ) ]; }, $page, $user, $comment, $row, $slots, $this->wikiId ); } else { $rev = new RevisionStoreRecord( $page, $user, $comment, $row, $slots, $this->wikiId ); } return $rev; } /** * Check that the given row matches the given Title object. * When a mismatch is detected, this tries to re-load the title from primary DB, * to avoid spurious errors during page moves. * * @param \stdClass $row * @param PageIdentity $page * @param array $context * * @return Pageidentity */ private function ensureRevisionRowMatchesPage( $row, PageIdentity $page, $context = [] ) { $revId = (int)( $row->rev_id ?? 0 ); $revPageId = (int)( $row->rev_page ?? 0 ); // XXX: also check $row->page_id? $expectedPageId = $page->getId( $this->wikiId ); // Avoid fatal error when the Title's ID changed, T246720 if ( $revPageId && $expectedPageId && $revPageId !== $expectedPageId ) { // NOTE: PageStore::getPageByReference may use the page ID, which we don't want here. $pageRec = $this->pageStore->getPageByName( $page->getNamespace(), $page->getDBkey(), IDBAccessObject::READ_LATEST ); $masterPageId = $pageRec->getId( $this->wikiId ); $masterLatest = $pageRec->getLatest( $this->wikiId ); if ( $revPageId === $masterPageId ) { if ( $page instanceof Title ) { // If we were using a Title object, keep using it, but update the page ID. // This way, we don't unexpectedly mix Titles with immutable value objects. $page->resetArticleID( $masterPageId ); } else { $page = $pageRec; } $this->logger->info( "Encountered stale Title object", [ 'page_id_stale' => $expectedPageId, 'page_id_reloaded' => $masterPageId, 'page_latest' => $masterLatest, 'rev_id' => $revId, 'exception' => new RuntimeException(), ] + $context ); } else { $expectedTitle = (string)$page; if ( $page instanceof Title ) { // If we started with a Title, keep using a Title. $page = $this->titleFactory->newFromID( $revPageId ); } else { $page = $pageRec; } // This could happen if a caller to e.g. getRevisionById supplied a Title that is // plain wrong. In this case, we should ideally throw an IllegalArgumentException. // However, it is more likely that we encountered a race condition during a page // move (T268910, T279832) or database corruption (T263340). That situation // should not be ignored, but we can allow the request to continue in a reasonable // manner without breaking things for the user. $this->logger->error( "Encountered mismatching Title object (see T259022, T268910, T279832, T263340)", [ 'expected_page_id' => $masterPageId, 'expected_page_title' => $expectedTitle, 'rev_page' => $revPageId, 'rev_page_title' => (string)$page, 'page_latest' => $masterLatest, 'rev_id' => $revId, 'exception' => new RuntimeException(), ] + $context ); } } // @phan-suppress-next-line PhanTypeMismatchReturnNullable getPageByName/newFromID should not return null return $page; } /** * Construct a RevisionRecord instance for each row in $rows, * and return them as an associative array indexed by revision ID. * Use RevisionSelectQueryBuilder or getArchiveQueryInfo() to construct the * query that produces the rows. * * @param IResultWrapper|\stdClass[] $rows the rows to construct revision records from * @param array $options Supports the following options: * 'slots' - whether metadata about revision slots should be * loaded immediately. Supports falsy or truthy value as well * as an explicit list of slot role names. The main slot will * always be loaded. * 'content' - whether the actual content of the slots should be * preloaded. * 'archive' - whether the rows where generated using getArchiveQueryInfo(), * rather than getQueryInfo. * @param int $queryFlags * @param PageIdentity|null $page The page to which all the revision rows belong, if there * is such a page and the caller has it handy, so we don't have to look it up again. * If this parameter is given and any of the rows has a rev_page_id that is different * from Article Id associated with the page, an InvalidArgumentException is thrown. * * @return StatusValue a status with a RevisionRecord[] of successfully fetched revisions * and an array of errors for the revisions failed to fetch. */ public function newRevisionsFromBatch( $rows, array $options = [], $queryFlags = 0, ?PageIdentity $page = null ) { $result = new StatusValue(); $archiveMode = $options['archive'] ?? false; if ( $archiveMode ) { $revIdField = 'ar_rev_id'; } else { $revIdField = 'rev_id'; } $rowsByRevId = []; $pageIdsToFetchTitles = []; $titlesByPageKey = []; foreach ( $rows as $row ) { if ( isset( $rowsByRevId[$row->$revIdField] ) ) { $result->warning( 'internalerror_info', "Duplicate rows in newRevisionsFromBatch, $revIdField {$row->$revIdField}" ); } // Attach a page key to the row, so we can find and reuse Title objects easily. $row->_page_key = $archiveMode ? $row->ar_namespace . ':' . $row->ar_title : $row->rev_page; if ( $page ) { if ( !$archiveMode && $row->rev_page != $this->getArticleId( $page ) ) { throw new InvalidArgumentException( "Revision {$row->$revIdField} doesn't belong to page " . $this->getArticleId( $page ) ); } if ( $archiveMode && ( $row->ar_namespace != $page->getNamespace() || $row->ar_title !== $page->getDBkey() ) ) { throw new InvalidArgumentException( "Revision {$row->$revIdField} doesn't belong to page " . $page ); } } elseif ( !isset( $titlesByPageKey[ $row->_page_key ] ) ) { if ( isset( $row->page_namespace ) && isset( $row->page_title ) // This should always be true, but just in case we don't have a page_id // set or it doesn't match rev_page, let's fetch the title again. && isset( $row->page_id ) && isset( $row->rev_page ) && $row->rev_page === $row->page_id ) { $titlesByPageKey[ $row->_page_key ] = Title::newFromRow( $row ); } elseif ( $archiveMode ) { // Can't look up deleted pages by ID, but we have namespace and title $titlesByPageKey[ $row->_page_key ] = Title::makeTitle( $row->ar_namespace, $row->ar_title ); } else { $pageIdsToFetchTitles[] = $row->rev_page; } } $rowsByRevId[$row->$revIdField] = $row; } if ( !$rowsByRevId ) { $result->setResult( true, [] ); return $result; } // If the page is not supplied, batch-fetch Title objects. if ( $page ) { // same logic as for $row->_page_key above $pageKey = $archiveMode ? $page->getNamespace() . ':' . $page->getDBkey() : $this->getArticleId( $page ); $titlesByPageKey[$pageKey] = $page; } elseif ( $pageIdsToFetchTitles ) { // Note: when we fetch titles by ID, the page key is also the ID. // We should never get here if $archiveMode is true. Assert::invariant( !$archiveMode, 'Titles are not loaded by ID in archive mode.' ); $pageIdsToFetchTitles = array_unique( $pageIdsToFetchTitles ); $pageRecords = $this->pageStore ->newSelectQueryBuilder() ->wherePageIds( $pageIdsToFetchTitles ) ->caller( __METHOD__ ) ->fetchPageRecordArray(); // Cannot array_merge because it re-indexes entries $titlesByPageKey = $pageRecords + $titlesByPageKey; } // which method to use for creating RevisionRecords $newRevisionRecord = $archiveMode ? [ $this, 'newRevisionFromArchiveRowAndSlots' ] : [ $this, 'newRevisionFromRowAndSlots' ]; if ( !isset( $options['slots'] ) ) { $result->setResult( true, array_map( static function ( $row ) use ( $queryFlags, $titlesByPageKey, $result, $newRevisionRecord, $revIdField ) { try { if ( !isset( $titlesByPageKey[$row->_page_key] ) ) { $result->warning( 'internalerror_info', "Couldn't find title for rev {$row->$revIdField} " . "(page key {$row->_page_key})" ); return null; } return $newRevisionRecord( $row, null, $queryFlags, $titlesByPageKey[ $row->_page_key ] ); } catch ( MWException $e ) { $result->warning( 'internalerror_info', $e->getMessage() ); return null; } }, $rowsByRevId ) ); return $result; } $slotRowOptions = [ 'slots' => $options['slots'] ?? true, 'blobs' => $options['content'] ?? false, ]; if ( is_array( $slotRowOptions['slots'] ) && !in_array( SlotRecord::MAIN, $slotRowOptions['slots'] ) ) { // Make sure the main slot is always loaded, RevisionRecord requires this. $slotRowOptions['slots'][] = SlotRecord::MAIN; } $slotRowsStatus = $this->getSlotRowsForBatch( $rowsByRevId, $slotRowOptions, $queryFlags ); $result->merge( $slotRowsStatus ); $slotRowsByRevId = $slotRowsStatus->getValue(); $result->setResult( true, array_map( function ( $row ) use ( $slotRowsByRevId, $queryFlags, $titlesByPageKey, $result, $revIdField, $newRevisionRecord ) { if ( !isset( $slotRowsByRevId[$row->$revIdField] ) ) { $result->warning( 'internalerror_info', "Couldn't find slots for rev {$row->$revIdField}" ); return null; } if ( !isset( $titlesByPageKey[$row->_page_key] ) ) { $result->warning( 'internalerror_info', "Couldn't find title for rev {$row->$revIdField} " . "(page key {$row->_page_key})" ); return null; } try { return $newRevisionRecord( $row, new RevisionSlots( $this->constructSlotRecords( $row->$revIdField, $slotRowsByRevId[$row->$revIdField], $queryFlags, $titlesByPageKey[$row->_page_key] ) ), $queryFlags, $titlesByPageKey[$row->_page_key] ); } catch ( MWException $e ) { $result->warning( 'internalerror_info', $e->getMessage() ); return null; } }, $rowsByRevId ) ); return $result; } /** * Gets the slot rows associated with a batch of revisions. * The serialized content of each slot can be included by setting the 'blobs' option. * Callers are responsible for unserializing and interpreting the content blobs * based on the model_name and role_name fields. * * @param Traversable|array $rowsOrIds list of revision ids, or revision or archive rows * from a db query. * @param array $options Supports the following options: * 'slots' - a list of slot role names to fetch. If omitted or true or null, * all slots are fetched * 'blobs' - whether the serialized content of each slot should be loaded. * If true, the serialized content will be present in the slot row * in the blob_data field. * @param int $queryFlags * * @return StatusValue a status containing, if isOK() returns true, a two-level nested * associative array, mapping from revision ID to an associative array that maps from * role name to a database row object. The database row object will contain the fields * defined by getSlotQueryInfo() with the 'content' flag set, plus the blob_data field * if the 'blobs' is set in $options. The model_name and role_name fields will also be * set. */ private function getSlotRowsForBatch( $rowsOrIds, array $options = [], $queryFlags = 0 ) { $result = new StatusValue(); $revIds = []; foreach ( $rowsOrIds as $row ) { if ( is_object( $row ) ) { $revIds[] = isset( $row->ar_rev_id ) ? (int)$row->ar_rev_id : (int)$row->rev_id; } else { $revIds[] = (int)$row; } } // Nothing to do. // Note that $rowsOrIds may not be "empty" even if $revIds is, e.g. if it's a ResultWrapper. if ( !$revIds ) { $result->setResult( true, [] ); return $result; } // We need to set the `content` flag to join in content meta-data $slotQueryInfo = $this->getSlotsQueryInfo( [ 'content' ] ); $revIdField = $slotQueryInfo['keys']['rev_id']; $slotQueryConds = [ $revIdField => $revIds ]; if ( isset( $options['slots'] ) && is_array( $options['slots'] ) ) { $slotIds = []; foreach ( $options['slots'] as $slot ) { try { $slotIds[] = $this->slotRoleStore->getId( $slot ); } catch ( NameTableAccessException $exception ) { // Do not fail when slot has no id (unused slot) // This also means for this slot are never data in the database } } if ( $slotIds === [] ) { // Degenerate case: return no slots for each revision. $result->setResult( true, array_fill_keys( $revIds, [] ) ); return $result; } $roleIdField = $slotQueryInfo['keys']['role_id']; $slotQueryConds[$roleIdField] = $slotIds; } $db = $this->getDBConnectionRefForQueryFlags( $queryFlags ); $slotRows = $db->newSelectQueryBuilder() ->queryInfo( $slotQueryInfo ) ->where( $slotQueryConds ) ->caller( __METHOD__ ) ->fetchResultSet(); $slotContents = null; if ( $options['blobs'] ?? false ) { $blobAddresses = []; foreach ( $slotRows as $slotRow ) { $blobAddresses[] = $slotRow->content_address; } $slotContentFetchStatus = $this->blobStore ->getBlobBatch( $blobAddresses, $queryFlags ); foreach ( $slotContentFetchStatus->getMessages() as $msg ) { $result->warning( $msg ); } $slotContents = $slotContentFetchStatus->getValue(); } $slotRowsByRevId = []; foreach ( $slotRows as $slotRow ) { if ( $slotContents === null ) { // nothing to do } elseif ( isset( $slotContents[$slotRow->content_address] ) ) { $slotRow->blob_data = $slotContents[$slotRow->content_address]; } else { $result->warning( 'internalerror_info', "Couldn't find blob data for rev {$slotRow->slot_revision_id}" ); $slotRow->blob_data = null; } // conditional needed for SCHEMA_COMPAT_READ_OLD if ( !isset( $slotRow->role_name ) && isset( $slotRow->slot_role_id ) ) { $slotRow->role_name = $this->slotRoleStore->getName( (int)$slotRow->slot_role_id ); } // conditional needed for SCHEMA_COMPAT_READ_OLD if ( !isset( $slotRow->model_name ) && isset( $slotRow->content_model ) ) { $slotRow->model_name = $this->contentModelStore->getName( (int)$slotRow->content_model ); } $slotRowsByRevId[$slotRow->slot_revision_id][$slotRow->role_name] = $slotRow; } $result->setResult( true, $slotRowsByRevId ); return $result; } /** * Gets raw (serialized) content blobs for the given set of revisions. * Callers are responsible for unserializing and interpreting the content blobs * based on the model_name field and the slot role. * * This method is intended for bulk operations in maintenance scripts. * It may be chosen over newRevisionsFromBatch by code that are only interested * in raw content, as opposed to meta data. Code that needs to access meta data of revisions, * slots, or content objects should use newRevisionsFromBatch() instead. * * @param Traversable|array $rowsOrIds list of revision ids, or revision rows from a db query. * @param array|null $slots the role names for which to get slots. * @param int $queryFlags * * @return StatusValue a status containing, if isOK() returns true, a two-level nested * associative array, mapping from revision ID to an associative array that maps from * role name to an anonymous object containing two fields: * - model_name: the name of the content's model * - blob_data: serialized content data */ public function getContentBlobsForBatch( $rowsOrIds, $slots = null, $queryFlags = 0 ) { $result = $this->getSlotRowsForBatch( $rowsOrIds, [ 'slots' => $slots, 'blobs' => true ], $queryFlags ); if ( $result->isOK() ) { // strip out all internal meta data that we don't want to expose foreach ( $result->value as $revId => $rowsByRole ) { foreach ( $rowsByRole as $role => $slotRow ) { if ( is_array( $slots ) && !in_array( $role, $slots ) ) { // In SCHEMA_COMPAT_READ_OLD mode we may get the main slot even // if we didn't ask for it. unset( $result->value[$revId][$role] ); continue; } $result->value[$revId][$role] = (object)[ 'blob_data' => $slotRow->blob_data, 'model_name' => $slotRow->model_name, ]; } } } return $result; } /** * Given a set of conditions, fetch a revision * * This method should be used if we are pretty sure the revision exists. * Unless $flags has READ_LATEST set, this method will first try to find the revision * on a replica before hitting the primary database. * * MCR migration note: this corresponded to Revision::newFromConds * * @param array $conditions * @param int $flags (optional) * @param PageIdentity|null $page (optional) * @param array $options (optional) additional query options * * @return RevisionRecord|null */ private function newRevisionFromConds( array $conditions, int $flags = IDBAccessObject::READ_NORMAL, ?PageIdentity $page = null, array $options = [] ) { $db = $this->getDBConnectionRefForQueryFlags( $flags ); $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $page, $options ); // Make sure new pending/committed revision are visible later on // within web requests to certain avoid bugs like T93866 and T94407. if ( !$rev && !( $flags & IDBAccessObject::READ_LATEST ) && $this->loadBalancer->hasStreamingReplicaServers() && $this->loadBalancer->hasOrMadeRecentPrimaryChanges() ) { $flags = IDBAccessObject::READ_LATEST; $dbw = $this->getPrimaryConnection(); $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $page, $options ); } return $rev; } /** * Given a set of conditions, fetch a revision from * the given database connection. * * MCR migration note: this corresponded to Revision::loadFromConds * * @param IReadableDatabase $db * @param array $conditions * @param int $flags (optional) * @param PageIdentity|null $page (optional) additional query options * @param array $options (optional) additional query options * * @return RevisionRecord|null */ private function loadRevisionFromConds( IReadableDatabase $db, array $conditions, int $flags = IDBAccessObject::READ_NORMAL, ?PageIdentity $page = null, array $options = [] ) { $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags, $options ); if ( $row ) { return $this->newRevisionFromRow( $row, $flags, $page ); } return null; } /** * Throws an exception if the given database connection does not belong to the wiki this * RevisionStore is bound to. * * @param IReadableDatabase $db */ private function checkDatabaseDomain( IReadableDatabase $db ) { $dbDomain = $db->getDomainID(); $storeDomain = $this->loadBalancer->resolveDomainID( $this->wikiId ); if ( $dbDomain === $storeDomain ) { return; } throw new RuntimeException( "DB connection domain '$dbDomain' does not match '$storeDomain'" ); } /** * Given a set of conditions, return a row with the * fields necessary to build RevisionRecord objects. * * MCR migration note: this corresponded to Revision::fetchFromConds * * @param IReadableDatabase $db * @param array $conditions * @param int $flags (optional) * @param array $options (optional) additional query options * * @return \stdClass|false data row as a raw object */ private function fetchRevisionRowFromConds( IReadableDatabase $db, array $conditions, int $flags = IDBAccessObject::READ_NORMAL, array $options = [] ) { $this->checkDatabaseDomain( $db ); $queryBuilder = $this->newSelectQueryBuilder( $db ) ->joinComment() ->joinPage() ->joinUser() ->where( $conditions ) ->options( $options ); if ( ( $flags & IDBAccessObject::READ_LOCKING ) == IDBAccessObject::READ_LOCKING ) { $queryBuilder->forUpdate(); } return $queryBuilder->caller( __METHOD__ )->fetchRow(); } /** * Return the tables, fields, and join conditions to be selected to create * a new RevisionStoreRecord object. * * MCR migration note: this replaced Revision::getQueryInfo * * If the format of fields returned changes in any way then the cache key provided by * self::getRevisionRowCacheKey should be updated. * * @since 1.31 * @deprecated since 1.41 use RevisionStore::newSelectQueryBuilder() instead. * * @param array $options Any combination of the following strings * - 'page': Join with the page table, and select fields to identify the page * - 'user': Join with the user table, and select the user name * * @return array[] With three keys: * - tables: (string[]) to include in the `$table` to `IDatabase->select()` or `SelectQueryBuilder::tables` * - fields: (string[]) to include in the `$vars` to `IDatabase->select()` or `SelectQueryBuilder::fields` * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` or `SelectQueryBuilder::joinConds` * @phan-return array{tables:string[],fields:string[],joins:array} */ public function getQueryInfo( $options = [] ) { $ret = [ 'tables' => [], 'fields' => [], 'joins' => [], ]; $ret['tables'] = array_merge( $ret['tables'], [ 'revision', 'actor_rev_user' => 'actor', ] ); $ret['fields'] = array_merge( $ret['fields'], [ 'rev_id', 'rev_page', 'rev_actor' => 'rev_actor', 'rev_user' => 'actor_rev_user.actor_user', 'rev_user_text' => 'actor_rev_user.actor_name', 'rev_timestamp', 'rev_minor_edit', 'rev_deleted', 'rev_len', 'rev_parent_id', 'rev_sha1', ] ); $ret['joins']['actor_rev_user'] = [ 'JOIN', "actor_rev_user.actor_id = rev_actor" ]; $commentQuery = $this->commentStore->getJoin( 'rev_comment' ); $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] ); $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] ); $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] ); if ( in_array( 'page', $options, true ) ) { $ret['tables'][] = 'page'; $ret['fields'] = array_merge( $ret['fields'], [ 'page_namespace', 'page_title', 'page_id', 'page_latest', 'page_is_redirect', 'page_len', ] ); $ret['joins']['page'] = [ 'JOIN', [ 'page_id = rev_page' ] ]; } if ( in_array( 'user', $options, true ) ) { $ret['tables'][] = 'user'; $ret['fields'] = array_merge( $ret['fields'], [ 'user_name', ] ); $ret['joins']['user'] = [ 'LEFT JOIN', [ 'actor_rev_user.actor_user != 0', 'user_id = actor_rev_user.actor_user' ] ]; } if ( in_array( 'text', $options, true ) ) { throw new InvalidArgumentException( 'The `text` option is no longer supported in MediaWiki 1.35 and later.' ); } return $ret; } /** * @inheritDoc */ public function newSelectQueryBuilder( IReadableDatabase $dbr ): RevisionSelectQueryBuilder { return new RevisionSelectQueryBuilder( $dbr ); } /** * @inheritDoc */ public function newArchiveSelectQueryBuilder( IReadableDatabase $dbr ): ArchiveSelectQueryBuilder { return new ArchiveSelectQueryBuilder( $dbr ); } /** * Return the tables, fields, and join conditions to be selected to create * a new SlotRecord. * * @since 1.32 * * @param array $options Any combination of the following strings * - 'content': Join with the content table, and select content meta-data fields * - 'model': Join with the content_models table, and select the model_name field. * Only applicable if 'content' is also set. * - 'role': Join with the slot_roles table, and select the role_name field * * @return array[] With three keys: * - tables: (string[]) to include in the `$table` to `IDatabase->select()` or `SelectQueryBuilder::tables` * - fields: (string[]) to include in the `$vars` to `IDatabase->select()` or `SelectQueryBuilder::fields` * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` or `SelectQueryBuilder::joinConds` * - keys: (associative array) to look up fields to match against. * In particular, the field that can be used to find slots by rev_id * can be found in ['keys']['rev_id']. * @phan-return array{tables:string[],fields:string[],joins:array,keys:array} */ public function getSlotsQueryInfo( $options = [] ) { $ret = [ 'tables' => [], 'fields' => [], 'joins' => [], 'keys' => [], ]; $ret['keys']['rev_id'] = 'slot_revision_id'; $ret['keys']['role_id'] = 'slot_role_id'; $ret['tables'][] = 'slots'; $ret['fields'] = array_merge( $ret['fields'], [ 'slot_revision_id', 'slot_content_id', 'slot_origin', 'slot_role_id', ] ); if ( in_array( 'role', $options, true ) ) { // Use left join to attach role name, so we still find the revision row even // if the role name is missing. This triggers a more obvious failure mode. $ret['tables'][] = 'slot_roles'; $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ]; $ret['fields'][] = 'role_name'; } if ( in_array( 'content', $options, true ) ) { $ret['keys']['model_id'] = 'content_model'; $ret['tables'][] = 'content'; $ret['fields'] = array_merge( $ret['fields'], [ 'content_size', 'content_sha1', 'content_address', 'content_model', ] ); $ret['joins']['content'] = [ 'JOIN', [ 'slot_content_id = content_id' ] ]; if ( in_array( 'model', $options, true ) ) { // Use left join to attach model name, so we still find the revision row even // if the model name is missing. This triggers a more obvious failure mode. $ret['tables'][] = 'content_models'; $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ]; $ret['fields'][] = 'model_name'; } } return $ret; } /** * Determine whether the parameter is a row containing all the fields * that RevisionStore needs to create a RevisionRecord from the row. * * @param mixed $row * @param string $table 'archive' or empty * @return bool */ public function isRevisionRow( $row, string $table = '' ) { if ( !( $row instanceof stdClass ) ) { return false; } $queryInfo = $table === 'archive' ? $this->getArchiveQueryInfo() : $this->getQueryInfo(); foreach ( $queryInfo['fields'] as $alias => $field ) { $name = is_numeric( $alias ) ? $field : $alias; if ( !property_exists( $row, $name ) ) { return false; } } return true; } /** * Return the tables, fields, and join conditions to be selected to create * a new RevisionArchiveRecord object. * * Since 1.34, ar_user and ar_user_text have not been present in the * database, but they continue to be available in query results as * aliases. * * MCR migration note: this replaced Revision::getArchiveQueryInfo * * @since 1.31 * @deprecated since 1.41 use RevisionStore::newArchiveSelectQueryBuilder() instead. * * @return array[] With three keys: * - tables: (string[]) to include in the `$table` to `IDatabase->select()` or `SelectQueryBuilder::tables` * - fields: (string[]) to include in the `$vars` to `IDatabase->select()` or `SelectQueryBuilder::fields` * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` or `SelectQueryBuilder::joinConds` * @phan-return array{tables:string[],fields:string[],joins:array} */ public function getArchiveQueryInfo() { $commentQuery = $this->commentStore->getJoin( 'ar_comment' ); $ret = [ 'tables' => [ 'archive', 'archive_actor' => 'actor' ] + $commentQuery['tables'], 'fields' => [ 'ar_id', 'ar_page_id', 'ar_namespace', 'ar_title', 'ar_rev_id', 'ar_timestamp', 'ar_minor_edit', 'ar_deleted', 'ar_len', 'ar_parent_id', 'ar_sha1', 'ar_actor', 'ar_user' => 'archive_actor.actor_user', 'ar_user_text' => 'archive_actor.actor_name', ] + $commentQuery['fields'], 'joins' => [ 'archive_actor' => [ 'JOIN', 'actor_id=ar_actor' ] ] + $commentQuery['joins'], ]; return $ret; } /** * Do a batched query for the sizes of a set of revisions. * * MCR migration note: this replaced Revision::getParentLengths * * @param int[] $revIds * @return int[] associative array mapping revision IDs from $revIds to the nominal size * of the corresponding revision. */ public function getRevisionSizes( array $revIds ) { $dbr = $this->getReplicaConnection(); $revLens = []; if ( !$revIds ) { return $revLens; // empty } $res = $dbr->newSelectQueryBuilder() ->select( [ 'rev_id', 'rev_len' ] ) ->from( 'revision' ) ->where( [ 'rev_id' => $revIds ] ) ->caller( __METHOD__ )->fetchResultSet(); foreach ( $res as $row ) { $revLens[$row->rev_id] = intval( $row->rev_len ); } return $revLens; } /** * Implementation of getPreviousRevision and getNextRevision. * * @param RevisionRecord $rev * @param int $flags * @param string $dir 'next' or 'prev' * @return RevisionRecord|null */ private function getRelativeRevision( RevisionRecord $rev, $flags, $dir ) { $op = $dir === 'next' ? '>' : '<'; $sort = $dir === 'next' ? 'ASC' : 'DESC'; $revisionIdValue = $rev->getId( $this->wikiId ); if ( !$revisionIdValue || !$rev->getPageId( $this->wikiId ) ) { // revision is unsaved or otherwise incomplete return null; } if ( $rev instanceof RevisionArchiveRecord ) { // revision is deleted, so it's not part of the page history return null; } $db = $this->getDBConnectionRefForQueryFlags( $flags ); $ts = $rev->getTimestamp() ?? $this->getTimestampFromId( $revisionIdValue, $flags ); if ( $ts === false ) { // XXX Should this be moved into getTimestampFromId? $ts = $db->newSelectQueryBuilder() ->select( 'ar_timestamp' ) ->from( 'archive' ) ->where( [ 'ar_rev_id' => $revisionIdValue ] ) ->caller( __METHOD__ )->fetchField(); if ( $ts === false ) { // XXX Is this reachable? How can we have a page id but no timestamp? return null; } } $revId = $db->newSelectQueryBuilder() ->select( 'rev_id' ) ->from( 'revision' ) ->where( [ 'rev_page' => $rev->getPageId( $this->wikiId ), $db->buildComparison( $op, [ 'rev_timestamp' => $db->timestamp( $ts ), 'rev_id' => $revisionIdValue, ] ), ] ) ->orderBy( [ 'rev_timestamp', 'rev_id' ], $sort ) ->ignoreIndex( 'rev_timestamp' ) // Probably needed for T159319 ->caller( __METHOD__ ) ->fetchField(); if ( $revId === false ) { return null; } return $this->getRevisionById( intval( $revId ), $flags ); } /** * Get the revision before $rev in the page's history, if any. * Will return null for the first revision but also for deleted or unsaved revisions. * * MCR migration note: this replaced Revision::getPrevious * * @see PageArchive::getPreviousRevisionRecord * * @param RevisionRecord $rev * @param int $flags (optional) $flags include: * IDBAccessObject::READ_LATEST: Select the data from the primary DB * * @return RevisionRecord|null */ public function getPreviousRevision( RevisionRecord $rev, $flags = IDBAccessObject::READ_NORMAL ) { return $this->getRelativeRevision( $rev, $flags, 'prev' ); } /** * Get the revision after $rev in the page's history, if any. * Will return null for the latest revision but also for deleted or unsaved revisions. * * MCR migration note: this replaced Revision::getNext * * @param RevisionRecord $rev * @param int $flags (optional) $flags include: * IDBAccessObject::READ_LATEST: Select the data from the primary DB * @return RevisionRecord|null */ public function getNextRevision( RevisionRecord $rev, $flags = IDBAccessObject::READ_NORMAL ) { return $this->getRelativeRevision( $rev, $flags, 'next' ); } /** * Get previous revision Id for this page_id * This is used to populate rev_parent_id on save * * MCR migration note: this corresponded to Revision::getPreviousRevisionId * * @param IReadableDatabase $db * @param RevisionRecord $rev * * @return int */ private function getPreviousRevisionId( IReadableDatabase $db, RevisionRecord $rev ) { $this->checkDatabaseDomain( $db ); if ( $rev->getPageId( $this->wikiId ) === null ) { return 0; } # Use page_latest if ID is not given if ( !$rev->getId( $this->wikiId ) ) { $prevId = $db->newSelectQueryBuilder() ->select( 'page_latest' ) ->from( 'page' ) ->where( [ 'page_id' => $rev->getPageId( $this->wikiId ) ] ) ->caller( __METHOD__ )->fetchField(); } else { $prevId = $db->newSelectQueryBuilder() ->select( 'rev_id' ) ->from( 'revision' ) ->where( [ 'rev_page' => $rev->getPageId( $this->wikiId ) ] ) ->andWhere( $db->expr( 'rev_id', '<', $rev->getId( $this->wikiId ) ) ) ->orderBy( 'rev_id DESC' ) ->caller( __METHOD__ )->fetchField(); } return intval( $prevId ); } /** * Get rev_timestamp from rev_id, without loading the rest of the row. * * Historically, there was an extra Title parameter that was passed before $id. This is no * longer needed and is deprecated in 1.34. * * MCR migration note: this replaced Revision::getTimestampFromId * * @param int $id * @param int $flags * @return string|false False if not found */ public function getTimestampFromId( $id, $flags = 0 ) { if ( $id instanceof Title ) { // Old deprecated calling convention supported for backwards compatibility $id = $flags; $flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0; } // T270149: Bail out if we know the query will definitely return false. Some callers are // passing RevisionRecord::getId() call directly as $id which can possibly return null. // Null $id or $id <= 0 will lead to useless query with WHERE clause of 'rev_id IS NULL' // or 'rev_id = 0', but 'rev_id' is always greater than zero and cannot be null. // @todo typehint $id and remove the null check if ( $id === null || $id <= 0 ) { return false; } $db = $this->getDBConnectionRefForQueryFlags( $flags ); $timestamp = $db->newSelectQueryBuilder() ->select( 'rev_timestamp' ) ->from( 'revision' ) ->where( [ 'rev_id' => $id ] ) ->caller( __METHOD__ )->fetchField(); return ( $timestamp !== false ) ? MWTimestamp::convert( TS_MW, $timestamp ) : false; } /** * Get count of revisions per page...not very efficient * * MCR migration note: this replaced Revision::countByPageId * * @param IReadableDatabase $db * @param int $id Page id * @return int */ public function countRevisionsByPageId( IReadableDatabase $db, $id ) { $this->checkDatabaseDomain( $db ); $row = $db->newSelectQueryBuilder() ->select( [ 'revCount' => 'COUNT(*)' ] ) ->from( 'revision' ) ->where( [ 'rev_page' => $id ] ) ->caller( __METHOD__ )->fetchRow(); if ( $row ) { return intval( $row->revCount ); } return 0; } /** * Get count of revisions per page...not very efficient * * MCR migration note: this replaced Revision::countByTitle * * @param IReadableDatabase $db * @param PageIdentity $page * @return int */ public function countRevisionsByTitle( IReadableDatabase $db, PageIdentity $page ) { $id = $this->getArticleId( $page ); if ( $id ) { return $this->countRevisionsByPageId( $db, $id ); } return 0; } /** * Check if no edits were made by other users since * the time a user started editing the page. Limit to * 50 revisions for the sake of performance. * * MCR migration note: this replaced Revision::userWasLastToEdit * * @deprecated since 1.31; Can possibly be removed, since the self-conflict suppression * logic in EditPage that uses this seems conceptually dubious. Revision::userWasLastToEdit * had been deprecated since 1.24 (the Revision class was removed entirely in 1.37). * * @param IReadableDatabase $db The Database to perform the check on. * @param int $pageId The ID of the page in question * @param int $userId The ID of the user in question * @param string $since Look at edits since this time * * @return bool True if the given user was the only one to edit since the given timestamp */ public function userWasLastToEdit( IReadableDatabase $db, $pageId, $userId, $since ) { $this->checkDatabaseDomain( $db ); if ( !$userId ) { return false; } $queryBuilder = $this->newSelectQueryBuilder( $db ) ->where( [ 'rev_page' => $pageId, $db->expr( 'rev_timestamp', '>', $db->timestamp( $since ) ) ] ) ->orderBy( 'rev_timestamp', SelectQueryBuilder::SORT_ASC ) ->limit( 50 ); $res = $queryBuilder->caller( __METHOD__ )->fetchResultSet(); foreach ( $res as $row ) { if ( $row->rev_user != $userId ) { return false; } } return true; } /** * Load a revision based on a known page ID and current revision ID from the DB * * This method allows for the use of caching, though accessing anything that normally * requires permission checks (aside from the text) will trigger a small DB lookup. * * MCR migration note: this replaced Revision::newKnownCurrent * * @param PageIdentity $page the associated page * @param int $revId current revision of this page. Defaults to $title->getLatestRevID(). * * @return RevisionRecord|false Returns false if missing */ public function getKnownCurrentRevision( PageIdentity $page, $revId = 0 ) { $db = $this->getReplicaConnection(); $revIdPassed = $revId; $pageId = $this->getArticleId( $page ); if ( !$pageId ) { return false; } if ( !$revId ) { if ( $page instanceof Title ) { $revId = $page->getLatestRevID(); } else { $pageRecord = $this->pageStore->getPageByReference( $page ); if ( $pageRecord ) { $revId = $pageRecord->getLatest( $this->getWikiId() ); } } } if ( !$revId ) { $this->logger->warning( 'No latest revision known for page {page} even though it exists with page ID {page_id}', [ 'page' => $page->__toString(), 'page_id' => $pageId, 'wiki_id' => $this->getWikiId() ?: 'local', ] ); return false; } // Load the row from cache if possible. If not possible, populate the cache. // As a minor optimization, remember if this was a cache hit or miss. // We can sometimes avoid a database query later if this is a cache miss. $fromCache = true; $row = $this->cache->getWithSetCallback( // Page/rev IDs passed in from DB to reflect history merges $this->getRevisionRowCacheKey( $db, $pageId, $revId ), WANObjectCache::TTL_WEEK, function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $revId, &$fromCache ) { $setOpts += Database::getCacheSetOptions( $db ); $row = $this->fetchRevisionRowFromConds( $db, [ 'rev_id' => intval( $revId ) ] ); if ( $row ) { $fromCache = false; } return $row; // don't cache negatives } ); // Reflect revision deletion and user renames. if ( $row ) { $title = $this->ensureRevisionRowMatchesPage( $row, $page, [ 'from_cache_flag' => $fromCache, 'page_id_initial' => $pageId, 'rev_id_used' => $revId, 'rev_id_requested' => $revIdPassed, ] ); return $this->newRevisionFromRow( $row, 0, $title, $fromCache ); } else { return false; } } /** * Get the first revision of a given page. * * @since 1.35 * @param LinkTarget|PageIdentity $page Calling with LinkTarget is deprecated since 1.36 * @param int $flags * @return RevisionRecord|null */ public function getFirstRevision( $page, int $flags = IDBAccessObject::READ_NORMAL ): ?RevisionRecord { if ( $page instanceof LinkTarget ) { // Only resolve LinkTarget to a Title when operating in the context of the local wiki (T248756) $page = $this->wikiId === WikiAwareEntity::LOCAL ? Title::castFromLinkTarget( $page ) : null; } return $this->newRevisionFromConds( [ 'page_namespace' => $page->getNamespace(), 'page_title' => $page->getDBkey() ], $flags, $page, [ 'ORDER BY' => [ 'rev_timestamp ASC', 'rev_id ASC' ], 'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319 ] ); } /** * Get a cache key for use with a row as selected with getQueryInfo( [ 'page', 'user' ] ) * Caching rows without 'page' or 'user' could lead to issues. * If the format of the rows returned by the query provided by getQueryInfo changes the * cache key should be updated to avoid conflicts. * * @param IReadableDatabase $db * @param int $pageId * @param int $revId * @return string */ private function getRevisionRowCacheKey( IReadableDatabase $db, $pageId, $revId ) { return $this->cache->makeGlobalKey( self::ROW_CACHE_KEY, $db->getDomainID(), $pageId, $revId ); } /** * Asserts that if revision is provided, it's saved and belongs to the page with provided pageId. * @param string $paramName * @param int $pageId * @param RevisionRecord|null $rev */ private function assertRevisionParameter( $paramName, $pageId, ?RevisionRecord $rev = null ) { if ( $rev ) { if ( $rev->getId( $this->wikiId ) === null ) { throw new InvalidArgumentException( "Unsaved {$paramName} revision passed" ); } if ( $rev->getPageId( $this->wikiId ) !== $pageId ) { throw new InvalidArgumentException( "Revision {$rev->getId( $this->wikiId )} doesn't belong to page {$pageId}" ); } } } /** * Converts revision limits to query conditions. * * @param ISQLPlatform $dbr * @param RevisionRecord|null $old Old revision. * If null is provided, count starting from the first revision (inclusive). * @param RevisionRecord|null $new New revision. * If null is provided, count until the last revision (inclusive). * @param string|array $options Single option, or an array of options: * RevisionStore::INCLUDE_OLD Include $old in the range; $new is excluded. * RevisionStore::INCLUDE_NEW Include $new in the range; $old is excluded. * RevisionStore::INCLUDE_BOTH Include both $old and $new in the range. * @return array */ private function getRevisionLimitConditions( ISQLPlatform $dbr, ?RevisionRecord $old = null, ?RevisionRecord $new = null, $options = [] ) { $options = (array)$options; if ( in_array( self::INCLUDE_OLD, $options ) || in_array( self::INCLUDE_BOTH, $options ) ) { $oldCmp = '>='; } else { $oldCmp = '>'; } if ( in_array( self::INCLUDE_NEW, $options ) || in_array( self::INCLUDE_BOTH, $options ) ) { $newCmp = '<='; } else { $newCmp = '<'; } $conds = []; if ( $old ) { $conds[] = $dbr->buildComparison( $oldCmp, [ 'rev_timestamp' => $dbr->timestamp( $old->getTimestamp() ), 'rev_id' => $old->getId( $this->wikiId ), ] ); } if ( $new ) { $conds[] = $dbr->buildComparison( $newCmp, [ 'rev_timestamp' => $dbr->timestamp( $new->getTimestamp() ), 'rev_id' => $new->getId( $this->wikiId ), ] ); } return $conds; } /** * Get IDs of revisions between the given revisions. * * @since 1.36 * * @param int $pageId The id of the page * @param RevisionRecord|null $old Old revision. * If null is provided, count starting from the first revision (inclusive). * @param RevisionRecord|null $new New revision. * If null is provided, count until the last revision (inclusive). * @param int|null $max Limit of Revisions to count, will be incremented by * one to detect truncations. * @param string|array $options Single option, or an array of options: * RevisionStore::INCLUDE_OLD Include $old in the range; $new is excluded. * RevisionStore::INCLUDE_NEW Include $new in the range; $old is excluded. * RevisionStore::INCLUDE_BOTH Include both $old and $new in the range. * @param string|null $order The direction in which the revisions should be sorted. * Possible values: * - RevisionStore::ORDER_OLDEST_TO_NEWEST * - RevisionStore::ORDER_NEWEST_TO_OLDEST * - null for no specific ordering (default value) * @param int $flags * @throws InvalidArgumentException in case either revision is unsaved or * the revisions do not belong to the same page or unknown option is passed. * @return int[] */ public function getRevisionIdsBetween( int $pageId, ?RevisionRecord $old = null, ?RevisionRecord $new = null, ?int $max = null, $options = [], ?string $order = null, int $flags = IDBAccessObject::READ_NORMAL ): array { $this->assertRevisionParameter( 'old', $pageId, $old ); $this->assertRevisionParameter( 'new', $pageId, $new ); $options = (array)$options; $includeOld = in_array( self::INCLUDE_OLD, $options ) || in_array( self::INCLUDE_BOTH, $options ); $includeNew = in_array( self::INCLUDE_NEW, $options ) || in_array( self::INCLUDE_BOTH, $options ); // No DB query needed if old and new are the same revision. // Can't check for consecutive revisions with 'getParentId' for a similar // optimization as edge cases exist when there are revisions between // a revision and it's parent. See T185167 for more details. if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) { return $includeOld || $includeNew ? [ $new->getId( $this->wikiId ) ] : []; } $db = $this->getDBConnectionRefForQueryFlags( $flags ); $queryBuilder = $db->newSelectQueryBuilder() ->select( 'rev_id' ) ->from( 'revision' ) ->where( [ 'rev_page' => $pageId, $db->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . ' = 0' ] ) ->andWhere( $this->getRevisionLimitConditions( $db, $old, $new, $options ) ); if ( $order !== null ) { $queryBuilder->orderBy( [ 'rev_timestamp', 'rev_id' ], $order ); } if ( $max !== null ) { // extra to detect truncation $queryBuilder->limit( $max + 1 ); } $values = $queryBuilder->caller( __METHOD__ )->fetchFieldValues(); return array_map( 'intval', $values ); } /** * Get the authors between the given revisions or revisions. * Used for diffs and other things that really need it. * * @since 1.35 * * @param int $pageId The id of the page * @param RevisionRecord|null $old Old revision. * If null is provided, count starting from the first revision (inclusive). * @param RevisionRecord|null $new New revision. * If null is provided, count until the last revision (inclusive). * @param Authority|null $performer the user whose access rights to apply * @param int|null $max Limit of Revisions to count, will be incremented to detect truncations. * @param string|array $options Single option, or an array of options: * RevisionStore::INCLUDE_OLD Include $old in the range; $new is excluded. * RevisionStore::INCLUDE_NEW Include $new in the range; $old is excluded. * RevisionStore::INCLUDE_BOTH Include both $old and $new in the range. * @throws InvalidArgumentException in case either revision is unsaved or * the revisions do not belong to the same page or unknown option is passed. * @return UserIdentity[] Names of revision authors in the range */ public function getAuthorsBetween( $pageId, ?RevisionRecord $old = null, ?RevisionRecord $new = null, ?Authority $performer = null, $max = null, $options = [] ) { $this->assertRevisionParameter( 'old', $pageId, $old ); $this->assertRevisionParameter( 'new', $pageId, $new ); $options = (array)$options; // No DB query needed if old and new are the same revision. // Can't check for consecutive revisions with 'getParentId' for a similar // optimization as edge cases exist when there are revisions between //a revision and it's parent. See T185167 for more details. if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) { if ( !$options ) { return []; } elseif ( $performer ) { return [ $new->getUser( RevisionRecord::FOR_THIS_USER, $performer ) ]; } else { return [ $new->getUser() ]; } } $dbr = $this->getReplicaConnection(); $queryBuilder = $dbr->newSelectQueryBuilder() ->select( [ 'rev_actor', 'rev_user' => 'revision_actor.actor_user', 'rev_user_text' => 'revision_actor.actor_name', ] ) ->distinct() ->from( 'revision' ) ->join( 'actor', 'revision_actor', 'revision_actor.actor_id = rev_actor' ) ->where( [ 'rev_page' => $pageId, $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) . " = 0" ] ) ->andWhere( $this->getRevisionLimitConditions( $dbr, $old, $new, $options ) ) ->caller( __METHOD__ ); if ( $max !== null ) { $queryBuilder->limit( $max + 1 ); } return array_map( function ( $row ) { return $this->actorStore->newActorFromRowFields( $row->rev_user, $row->rev_user_text, $row->rev_actor ); }, iterator_to_array( $queryBuilder->fetchResultSet() ) ); } /** * Get the number of authors between the given revisions. * Used for diffs and other things that really need it. * * @since 1.35 * * @param int $pageId The id of the page * @param RevisionRecord|null $old Old revision . * If null is provided, count starting from the first revision (inclusive). * @param RevisionRecord|null $new New revision. * If null is provided, count until the last revision (inclusive). * @param Authority|null $performer the user whose access rights to apply * @param int|null $max Limit of Revisions to count, will be incremented to detect truncations. * @param string|array $options Single option, or an array of options: * RevisionStore::INCLUDE_OLD Include $old in the range; $new is excluded. * RevisionStore::INCLUDE_NEW Include $new in the range; $old is excluded. * RevisionStore::INCLUDE_BOTH Include both $old and $new in the range. * @throws InvalidArgumentException in case either revision is unsaved or * the revisions do not belong to the same page or unknown option is passed. * @return int Number of revisions authors in the range. */ public function countAuthorsBetween( $pageId, ?RevisionRecord $old = null, ?RevisionRecord $new = null, ?Authority $performer = null, $max = null, $options = [] ) { // TODO: Implement with a separate query to avoid cost of selecting unneeded fields // and creation of UserIdentity stuff. return count( $this->getAuthorsBetween( $pageId, $old, $new, $performer, $max, $options ) ); } /** * Get the number of revisions between the given revisions. * Used for diffs and other things that really need it. * * @since 1.35 * * @param int $pageId The id of the page * @param RevisionRecord|null $old Old revision. * If null is provided, count starting from the first revision (inclusive). * @param RevisionRecord|null $new New revision. * If null is provided, count until the last revision (inclusive). * @param int|null $max Limit of Revisions to count, will be incremented to detect truncations. * @param string|array $options Single option, or an array of options: * RevisionStore::INCLUDE_OLD Include $old in the range; $new is excluded. * RevisionStore::INCLUDE_NEW Include $new in the range; $old is excluded. * RevisionStore::INCLUDE_BOTH Include both $old and $new in the range. * @throws InvalidArgumentException in case either revision is unsaved or * the revisions do not belong to the same page. * @return int Number of revisions between these revisions. */ public function countRevisionsBetween( $pageId, ?RevisionRecord $old = null, ?RevisionRecord $new = null, $max = null, $options = [] ) { $this->assertRevisionParameter( 'old', $pageId, $old ); $this->assertRevisionParameter( 'new', $pageId, $new ); // No DB query needed if old and new are the same revision. // Can't check for consecutive revisions with 'getParentId' for a similar // optimization as edge cases exist when there are revisions between //a revision and it's parent. See T185167 for more details. if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) { return 0; } $dbr = $this->getReplicaConnection(); $conds = array_merge( [ 'rev_page' => $pageId, $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . " = 0" ], $this->getRevisionLimitConditions( $dbr, $old, $new, $options ) ); if ( $max !== null ) { return $dbr->newSelectQueryBuilder() ->select( '1' ) ->from( 'revision' ) ->where( $conds ) ->caller( __METHOD__ ) ->limit( $max + 1 ) // extra to detect truncation ->fetchRowCount(); } else { return (int)$dbr->newSelectQueryBuilder() ->select( 'count(*)' ) ->from( 'revision' ) ->where( $conds ) ->caller( __METHOD__ )->fetchField(); } } /** * Tries to find a revision identical to $revision in $searchLimit most recent revisions * of this page. The comparison is based on SHA1s of these revisions. * * @since 1.37 * * @param RevisionRecord $revision which revision to compare to * @param int $searchLimit How many recent revisions should be checked * * @return RevisionRecord|null */ public function findIdenticalRevision( RevisionRecord $revision, int $searchLimit ): ?RevisionRecord { $revision->assertWiki( $this->wikiId ); $db = $this->getReplicaConnection(); $subquery = $this->newSelectQueryBuilder( $db ) ->joinComment() ->where( [ 'rev_page' => $revision->getPageId( $this->wikiId ) ] ) // Include 'rev_id' in the ordering in case there are multiple revs with same timestamp ->orderBy( [ 'rev_timestamp', 'rev_id' ], SelectQueryBuilder::SORT_DESC ) // T354015 ->useIndex( [ 'revision' => 'rev_page_timestamp' ] ) ->limit( $searchLimit ) // skip the most recent edit, we can't revert to it anyway ->offset( 1 ) ->caller( __METHOD__ ); // fetchRow effectively uses LIMIT 1 clause, returning only the first result $revisionRow = $db->newSelectQueryBuilder() ->select( '*' ) ->from( $subquery, 'recent_revs' ) ->where( [ 'rev_sha1' => $revision->getSha1() ] ) ->caller( __METHOD__ )->fetchRow(); return $revisionRow ? $this->newRevisionFromRow( $revisionRow ) : null; } // TODO: move relevant methods from Title here, e.g. isBigDeletion, etc. } PK ! ��M �M RevisionRecord.phpnu �Iw�� <?php /** * Page revision base class. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Revision; use InvalidArgumentException; use MediaWiki\CommentStore\CommentStoreComment; use MediaWiki\Content\Content; use MediaWiki\DAO\WikiAwareEntity; use MediaWiki\DAO\WikiAwareEntityTrait; use MediaWiki\Linker\LinkTarget; use MediaWiki\Page\LegacyArticleIdAccess; use MediaWiki\Page\PageIdentity; use MediaWiki\Permissions\Authority; use MediaWiki\Title\Title; use MediaWiki\User\UserIdentity; use Wikimedia\NonSerializable\NonSerializableTrait; /** * Page revision base class. * * RevisionRecords are considered value objects, but they may use callbacks for lazy loading. * Note that while the base class has no setters, subclasses may offer a mutable interface. * * @since 1.31 * @since 1.32 Renamed from MediaWiki\Storage\RevisionRecord */ abstract class RevisionRecord implements WikiAwareEntity { use LegacyArticleIdAccess; use NonSerializableTrait; use WikiAwareEntityTrait; // RevisionRecord deletion constants public const DELETED_TEXT = 1; public const DELETED_COMMENT = 2; public const DELETED_USER = 4; public const DELETED_RESTRICTED = 8; public const SUPPRESSED_USER = self::DELETED_USER | self::DELETED_RESTRICTED; // convenience public const SUPPRESSED_ALL = self::DELETED_TEXT | self::DELETED_COMMENT | self::DELETED_USER | self::DELETED_RESTRICTED; // convenience // Audience options for accessors public const FOR_PUBLIC = 1; public const FOR_THIS_USER = 2; public const RAW = 3; /** @var string|false Wiki ID; false means the current wiki */ protected $wikiId = false; /** @var int|null */ protected $mId; /** @var int */ protected $mPageId; /** @var UserIdentity|null */ protected $mUser; /** @var bool */ protected $mMinorEdit = false; /** @var string|null */ protected $mTimestamp; /** @var int using the DELETED_XXX and SUPPRESSED_XXX flags */ protected $mDeleted = 0; /** @var int|null */ protected $mSize; /** @var string|null */ protected $mSha1; /** @var int|null */ protected $mParentId; /** @var CommentStoreComment|null */ protected $mComment; /** @var PageIdentity */ protected $mPage; /** @var RevisionSlots */ protected $mSlots; /** * @note Avoid calling this constructor directly. Use the appropriate methods * in RevisionStore instead. * * @param PageIdentity $page The page this RevisionRecord is associated with. * @param RevisionSlots $slots The slots of this revision. * @param false|string $wikiId Relevant wiki id or self::LOCAL for the current one. */ public function __construct( PageIdentity $page, RevisionSlots $slots, $wikiId = self::LOCAL ) { $this->assertWikiIdParam( $wikiId ); $this->mPage = $page; $this->mSlots = $slots; $this->wikiId = $wikiId; $this->mPageId = $this->getArticleId( $page ); } /** * @param RevisionRecord $rec * * @return bool True if this RevisionRecord is known to have same content as $rec. * False if the content is different (or not known to be the same). */ public function hasSameContent( RevisionRecord $rec ): bool { if ( $rec === $this ) { return true; } if ( $this->getId() !== null && $this->getId() === $rec->getId() ) { return true; } // check size before hash, since size is quicker to compute if ( $this->getSize() !== $rec->getSize() ) { return false; } // instead of checking the hash, we could also check the content addresses of all slots. if ( $this->getSha1() === $rec->getSha1() ) { return true; } return false; } /** * Returns the Content of the given slot of this revision. * Call getSlotNames() to get a list of available slots. * * Note that for mutable Content objects, each call to this method will return a * fresh clone. * * Use getContentOrThrow() for more specific error information. * * @param string $role The role name of the desired slot * @param int $audience * @param Authority|null $performer user on whose behalf to check * * @return Content|null The content of the given slot, or null on error * @throws RevisionAccessException */ public function getContent( $role, $audience = self::FOR_PUBLIC, ?Authority $performer = null ): ?Content { try { $content = $this->getSlot( $role, $audience, $performer )->getContent(); } catch ( BadRevisionException | SuppressedDataException $e ) { return null; } return $content->copy(); } /** * Get the Content of the given slot of this revision. * * @param string $role The role name of the desired slot * @param int $audience * @param Authority|null $performer user on whose behalf to check * * @return Content * @throws SuppressedDataException if the content is not viewable by the given audience * @throws BadRevisionException if the content is missing or corrupted * @throws RevisionAccessException */ public function getContentOrThrow( $role, $audience = self::FOR_PUBLIC, ?Authority $performer = null ): Content { if ( !$this->audienceCan( self::DELETED_TEXT, $audience, $performer ) ) { throw new SuppressedDataException( 'Access to the content has been suppressed for this audience' ); } $content = $this->getSlot( $role, $audience, $performer )->getContent(); return $content->copy(); } /** * Returns meta-data for the given slot. * * @param string $role The role name of the desired slot * @param int $audience * @param Authority|null $performer user on whose behalf to check * * @throws RevisionAccessException if the slot does not exist or slot data * could not be lazy-loaded. * @return SlotRecord The slot meta-data. If access to the slot's content is forbidden, * calling getContent() on the SlotRecord will throw an exception. */ public function getSlot( $role, $audience = self::FOR_PUBLIC, ?Authority $performer = null ): SlotRecord { $slot = $this->mSlots->getSlot( $role ); if ( !$this->audienceCan( self::DELETED_TEXT, $audience, $performer ) ) { return SlotRecord::newWithSuppressedContent( $slot ); } return $slot; } /** * Returns whether the given slot is defined in this revision. * * @param string $role The role name of the desired slot * * @return bool */ public function hasSlot( $role ): bool { return $this->mSlots->hasSlot( $role ); } /** * Returns the slot names (roles) of all slots present in this revision. * getContent() will succeed only for the names returned by this method. * * @return string[] */ public function getSlotRoles(): array { return $this->mSlots->getSlotRoles(); } /** * Returns the slots defined for this revision. * * @note This provides access to slot content with no audience checks applied. * Calling getContent() on the RevisionSlots object returned here, or on any * SlotRecord it returns from getSlot(), will not fail due to access restrictions. * If audience checks are desired, use getSlot( $role, $audience, $performer ) * or getContent( $role, $audience, $performer ) instead. * * @return RevisionSlots */ public function getSlots(): RevisionSlots { return $this->mSlots; } /** * Returns the slots that originate in this revision. * * Note that this does not include any slots inherited from some earlier revision, * even if they are different from the slots in the immediate parent revision. * This is the case for rollbacks: slots of a rollback revision are inherited from * the rollback target, and are different from the slots in the parent revision, * which was rolled back. * * To find all slots modified by this revision against its immediate parent * revision, use RevisionSlotsUpdate::newFromRevisionSlots(). * * @return RevisionSlots */ public function getOriginalSlots(): RevisionSlots { return new RevisionSlots( $this->mSlots->getOriginalSlots() ); } /** * Returns slots inherited from some previous revision. * * "Inherited" slots are all slots that do not originate in this revision. * Note that these slots may still differ from the one in the parent revision. * This is the case for rollbacks: slots of a rollback revision are inherited from * the rollback target, and are different from the slots in the parent revision, * which was rolled back. * * @return RevisionSlots */ public function getInheritedSlots(): RevisionSlots { return new RevisionSlots( $this->mSlots->getInheritedSlots() ); } /** * Returns primary slots (those that are not derived). * * @return RevisionSlots * @since 1.36 */ public function getPrimarySlots(): RevisionSlots { return new RevisionSlots( $this->mSlots->getPrimarySlots() ); } /** * Get revision ID. Depending on the concrete subclass, this may return null if * the revision ID is not known (e.g. because the revision does not yet exist * in the database). * * MCR migration note: this replaced Revision::getId * * @param string|false $wikiId The wiki ID expected by the caller. * @return int|null */ public function getId( $wikiId = self::LOCAL ) { $this->deprecateInvalidCrossWiki( $wikiId, '1.36' ); return $this->mId; } /** * Get parent revision ID (the original previous page revision). * If there is no parent revision, this returns 0. * If the parent revision is undefined or unknown, this returns null. * * @note As of MW 1.31, the database schema allows the parent ID to be * NULL to indicate that it is unknown. * * MCR migration note: this replaced Revision::getParentId * * @param string|false $wikiId The wiki ID expected by the caller. * @return int|null */ public function getParentId( $wikiId = self::LOCAL ) { $this->deprecateInvalidCrossWiki( $wikiId, '1.36' ); return $this->mParentId; } /** * Returns the nominal size of this revision, in bogo-bytes. * May be calculated on the fly if not known, which may in the worst * case may involve loading all content. * * MCR migration note: this replaced Revision::getSize * * @throws RevisionAccessException if the size was unknown and could not be calculated. * @return int */ abstract public function getSize(); /** * Returns the base36 sha1 of this revision. This hash is derived from the * hashes of all slots associated with the revision. * May be calculated on the fly if not known, which may in the worst * case may involve loading all content. * * MCR migration note: this replaced Revision::getSha1 * * @throws RevisionAccessException if the hash was unknown and could not be calculated. * @return string */ abstract public function getSha1(); /** * Get the page ID. If the page does not yet exist, the page ID is 0. * * MCR migration note: this replaced Revision::getPage * * @param string|false $wikiId The wiki ID expected by the caller. * @return int */ public function getPageId( $wikiId = self::LOCAL ) { $this->deprecateInvalidCrossWiki( $wikiId, '1.36' ); return $this->mPageId; } /** * Get the ID of the wiki this revision belongs to. * * @return string|false The wiki's logical name, of false to indicate the local wiki. */ public function getWikiId() { return $this->wikiId; } /** * Returns the title of the page this revision is associated with as a LinkTarget object. * * @throws InvalidArgumentException if this revision does not belong to a local wiki * @return LinkTarget */ public function getPageAsLinkTarget() { // TODO: Should be TitleValue::newFromPage( $this->mPage ), // but Title is used too much still, so let's keep propagating it return Title::newFromPageIdentity( $this->mPage ); } /** * Returns the page this revision belongs to. * * MCR migration note: this replaced Revision::getTitle * * @since 1.36 * * @return PageIdentity */ public function getPage(): PageIdentity { return $this->mPage; } /** * Fetch revision's author's user identity, if it's available to the specified audience. * If the specified audience does not have access to it, null will be * returned. Depending on the concrete subclass, null may also be returned if the user is * not yet specified. * * MCR migration note: this replaced Revision::getUser * * @param int $audience One of: * RevisionRecord::FOR_PUBLIC to be displayed to all users * RevisionRecord::FOR_THIS_USER to be displayed to the given user * RevisionRecord::RAW get the ID regardless of permissions * @param Authority|null $performer user on whose behalf to check * @return UserIdentity|null */ public function getUser( $audience = self::FOR_PUBLIC, ?Authority $performer = null ) { if ( !$this->audienceCan( self::DELETED_USER, $audience, $performer ) ) { return null; } else { return $this->mUser; } } /** * Fetch revision comment, if it's available to the specified audience. * If the specified audience does not have access to the comment, * this will return null. Depending on the concrete subclass, null may also be returned * if the comment is not yet specified. * * MCR migration note: this replaced Revision::getComment * * @param int $audience One of: * RevisionRecord::FOR_PUBLIC to be displayed to all users * RevisionRecord::FOR_THIS_USER to be displayed to the given user * RevisionRecord::RAW get the text regardless of permissions * @param Authority|null $performer user on whose behalf to check * * @return CommentStoreComment|null */ public function getComment( $audience = self::FOR_PUBLIC, ?Authority $performer = null ) { if ( !$this->audienceCan( self::DELETED_COMMENT, $audience, $performer ) ) { return null; } else { return $this->mComment; } } /** * MCR migration note: this replaced Revision::isMinor * * @return bool */ public function isMinor() { return (bool)$this->mMinorEdit; } /** * MCR migration note: this replaced Revision::isDeleted * * @param int $field One of DELETED_* bitfield constants * * @return bool */ public function isDeleted( $field ) { return ( $this->getVisibility() & $field ) == $field; } /** * Get the deletion bitfield of the revision * * MCR migration note: this replaced Revision::getVisibility * * @return int */ public function getVisibility() { return (int)$this->mDeleted; } /** * MCR migration note: this replaced Revision::getTimestamp. * * May return null if the timestamp was not specified. * * @return string|null */ public function getTimestamp() { return $this->mTimestamp; } /** * Check that the given audience has access to the given field. * * MCR migration note: this corresponded to Revision::userCan * * @param int $field One of self::DELETED_TEXT, * self::DELETED_COMMENT, * self::DELETED_USER * @param int $audience One of: * RevisionRecord::FOR_PUBLIC to be displayed to all users * RevisionRecord::FOR_THIS_USER to be displayed to the given user * RevisionRecord::RAW get the text regardless of permissions * @param Authority|null $performer user on whose behalf to check * * @return bool */ public function audienceCan( $field, $audience, ?Authority $performer = null ) { if ( $audience == self::FOR_PUBLIC && $this->isDeleted( $field ) ) { return false; } elseif ( $audience == self::FOR_THIS_USER ) { if ( !$performer ) { throw new InvalidArgumentException( 'An Authority object must be given when checking FOR_THIS_USER audience.' ); } if ( !$this->userCan( $field, $performer ) ) { return false; } } return true; } /** * Determine if the give authority is allowed to view a particular * field of this revision, if it's marked as deleted. * * MCR migration note: this corresponded to Revision::userCan * * @param int $field One of self::DELETED_TEXT, * self::DELETED_COMMENT, * self::DELETED_USER * @param Authority $performer user on whose behalf to check * @return bool */ public function userCan( $field, Authority $performer ) { return self::userCanBitfield( $this->getVisibility(), $field, $performer, $this->mPage ); } /** * Determine if the current user is allowed to view a particular * field of this revision, if it's marked as deleted. This is used * by various classes to avoid duplication. * * MCR migration note: this replaced Revision::userCanBitfield * * @param int $bitfield Current field * @param int $field One of self::DELETED_TEXT = File::DELETED_FILE, * self::DELETED_COMMENT = File::DELETED_COMMENT, * self::DELETED_USER = File::DELETED_USER * @param Authority $performer user on whose behalf to check * @param PageIdentity|null $page A PageIdentity object to check for per-page restrictions on, * instead of just plain user rights * @return bool */ public static function userCanBitfield( $bitfield, $field, Authority $performer, ?PageIdentity $page = null ) { if ( $bitfield & $field ) { // aspect is deleted if ( $bitfield & self::DELETED_RESTRICTED ) { $permissions = [ 'suppressrevision', 'viewsuppressed' ]; } elseif ( $field & self::DELETED_TEXT ) { $permissions = [ 'deletedtext' ]; } else { $permissions = [ 'deletedhistory' ]; } $permissionlist = implode( ', ', $permissions ); if ( $page === null ) { wfDebug( "Checking for $permissionlist due to $field match on $bitfield" ); return $performer->isAllowedAny( ...$permissions ); } else { wfDebug( "Checking for $permissionlist on $page due to $field match on $bitfield" ); foreach ( $permissions as $perm ) { if ( $performer->authorizeRead( $perm, $page ) ) { return true; } } return false; } } else { return true; } } /** * Returns whether this RevisionRecord is ready for insertion, that is, whether it contains all * information needed to save it to the database. This should trivially be true for * RevisionRecords loaded from the database. * * Note that this may return true even if getId() or getPage() return null or 0, since these * are generally assigned while the revision is saved to the database, and may not be available * before. * * @return bool */ public function isReadyForInsertion() { // NOTE: don't check getSize() and getSha1(), since that may cause the full content to // be loaded in order to calculate the values. Just assume these methods will not return // null if mSlots is not empty. // NOTE: getId() and getPageId() may return null before a revision is saved, so don't // check them. return $this->getTimestamp() !== null && $this->getComment( self::RAW ) !== null && $this->getUser( self::RAW ) !== null && $this->mSlots->getSlotRoles() !== []; } /** * Checks whether the revision record is a stored current revision. * @since 1.35 * @return bool */ public function isCurrent() { return false; } } PK ! ��`N�B �B RenderedRevision.phpnu �Iw�� <?php /** * This file is part of MediaWiki. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Revision; use InvalidArgumentException; use LogicException; use MediaWiki\Content\Content; use MediaWiki\Content\Renderer\ContentRenderer; use MediaWiki\Page\PageReference; use MediaWiki\Parser\ParserOptions; use MediaWiki\Parser\ParserOutput; use MediaWiki\Parser\ParserOutputFlags; use MediaWiki\Permissions\Authority; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Wikimedia\Assert\Assert; /** * RenderedRevision represents the rendered representation of a revision. It acts as a lazy provider * of ParserOutput objects for the revision's individual slots, as well as a combined ParserOutput * of all slots. * * @since 1.32 */ class RenderedRevision implements SlotRenderingProvider { /** @var RevisionRecord */ private $revision; /** * @var ParserOptions */ private $options; /** * @var int Audience to check when accessing content. */ private $audience = RevisionRecord::FOR_PUBLIC; /** * @var Authority|null The user to use for audience checks during content access. */ private $performer = null; /** * @var ParserOutput|null The combined ParserOutput for the revision, * initialized lazily by getRevisionParserOutput(). */ private $revisionOutput = null; /** * @var ParserOutput[] The ParserOutput for each slot, * initialized lazily by getSlotParserOutput(). */ private $slotsOutput = []; /** * @var callable Callback for combining slot output into revision output. * Signature: function ( RenderedRevision $this, array $hints ): ParserOutput. */ private $combineOutput; /** * @var LoggerInterface For profiling ParserOutput re-use. */ private $saveParseLogger; /** * @var ContentRenderer Service to render content. */ private $contentRenderer; /** * @note Application logic should not instantiate RenderedRevision instances directly, * but should use a RevisionRenderer instead. * * @param RevisionRecord $revision The revision to render. The content for rendering will be * taken from this RevisionRecord. However, if the RevisionRecord is not complete * according isReadyForInsertion(), but a revision ID is known, the parser may load * the revision from the database if it needs revision meta data to handle magic * words like {{REVISIONUSER}}. * @param ParserOptions $options * @param ContentRenderer $contentRenderer * @param callable $combineOutput Callback for combining slot output into revision output. * Signature: function ( RenderedRevision $this, array $hints ): ParserOutput. * @param int $audience Use RevisionRecord::FOR_PUBLIC, FOR_THIS_USER, or RAW. * @param Authority|null $performer Required if $audience is FOR_THIS_USER. */ public function __construct( RevisionRecord $revision, ParserOptions $options, ContentRenderer $contentRenderer, callable $combineOutput, $audience = RevisionRecord::FOR_PUBLIC, ?Authority $performer = null ) { $this->options = $options; $this->setRevisionInternal( $revision ); $this->contentRenderer = $contentRenderer; $this->combineOutput = $combineOutput; $this->saveParseLogger = new NullLogger(); if ( $audience === RevisionRecord::FOR_THIS_USER && !$performer ) { throw new InvalidArgumentException( 'User must be specified when setting audience to FOR_THIS_USER' ); } $this->audience = $audience; $this->performer = $performer; } /** * @param LoggerInterface $saveParseLogger */ public function setSaveParseLogger( LoggerInterface $saveParseLogger ) { $this->saveParseLogger = $saveParseLogger; } /** * @return bool Whether the revision's content has been hidden from unprivileged users. */ public function isContentDeleted() { return $this->revision->isDeleted( RevisionRecord::DELETED_TEXT ); } /** * @return RevisionRecord */ public function getRevision() { return $this->revision; } /** * @return ParserOptions */ public function getOptions() { return $this->options; } /** * Sets a ParserOutput to be returned by getRevisionParserOutput(). * * @note For internal use by RevisionRenderer only! This method may be modified * or removed without notice per the deprecation policy. * * @internal * * @param ParserOutput $output */ public function setRevisionParserOutput( ParserOutput $output ) { $this->revisionOutput = $output; // If there is only one slot, we assume that the combined output is identical // with the main slot's output. This is intended to prevent a redundant re-parse of // the content in case getSlotParserOutput( SlotRecord::MAIN ) is called, for instance // from ContentHandler::getSecondaryDataUpdates. if ( $this->revision->getSlotRoles() === [ SlotRecord::MAIN ] ) { $this->slotsOutput[ SlotRecord::MAIN ] = $output; } } /** * @param array $hints Hints given as an associative array. Known keys: * - 'generate-html' => bool: Whether the caller is interested in output HTML (as opposed * to just meta-data). Default is to generate HTML. * @phan-param array{generate-html?:bool} $hints * * @return ParserOutput */ public function getRevisionParserOutput( array $hints = [] ) { $withHtml = $hints['generate-html'] ?? true; if ( !$this->revisionOutput || ( $withHtml && !$this->revisionOutput->hasText() ) ) { $output = call_user_func( $this->combineOutput, $this, $hints ); Assert::postcondition( $output instanceof ParserOutput, 'Callback did not return a ParserOutput object!' ); $this->revisionOutput = $output; } return $this->revisionOutput; } /** * @param string $role * @param array $hints Hints given as an associative array. Known keys: * - 'generate-html' => bool: Whether the caller is interested in output HTML (as opposed * to just meta-data). Default is to generate HTML. * - 'previous-output' => ?ParserOutput: An optional "previously parsed" * version of this slot; used to allow Parsoid selective updates. * @phan-param array{generate-html?:bool,previous-output?:?ParserOutput} $hints * * @throws SuppressedDataException if the content is not accessible for the audience * specified in the constructor. * @throws BadRevisionException * @throws RevisionAccessException * @return ParserOutput */ public function getSlotParserOutput( $role, array $hints = [] ) { $withHtml = $hints['generate-html'] ?? true; if ( !isset( $this->slotsOutput[ $role ] ) || ( $withHtml && !$this->slotsOutput[ $role ]->hasText() ) ) { $content = $this->revision->getContentOrThrow( $role, $this->audience, $this->performer ); // XXX: allow SlotRoleHandler to control the ParserOutput? $output = $this->getSlotParserOutputUncached( $content, $hints ); if ( $withHtml && !$output->hasText() ) { throw new LogicException( 'HTML generation was requested, but ' . get_class( $content ) . ' that passed to ' . 'ContentRenderer::getParserOutput() returns a ParserOutput with no text set.' ); } // Detach watcher, to ensure option use is not recorded in the wrong ParserOutput. $this->options->registerWatcher( null ); $this->slotsOutput[ $role ] = $output; } return $this->slotsOutput[$role]; } /** * @note This method exists to make duplicate parses easier to see during profiling * @param Content $content * @param array{generate-html?:bool,previous-output?:?ParserOutput} $hints * @return ParserOutput */ private function getSlotParserOutputUncached( Content $content, array $hints ): ParserOutput { return $this->contentRenderer->getParserOutput( $content, $this->revision->getPage(), $this->revision, $this->options, $hints ); } /** * Updates the RevisionRecord after the revision has been saved. This can be used to discard * and cached ParserOutput so parser functions like {{REVISIONTIMESTAMP}} or {{REVISIONID}} * are re-evaluated. * * @note There should be no need to call this for null-edits. * * @param RevisionRecord $rev */ public function updateRevision( RevisionRecord $rev ) { if ( $rev->getId() === $this->revision->getId() ) { return; } if ( $this->revision->getId() ) { throw new LogicException( 'RenderedRevision already has a revision with ID ' . $this->revision->getId() . ', can\'t update to revision with ID ' . $rev->getId() ); } if ( !$this->revision->getSlots()->hasSameContent( $rev->getSlots() ) ) { throw new LogicException( 'Cannot update to a revision with different content!' ); } $this->setRevisionInternal( $rev ); $this->pruneRevisionSensitiveOutput( $this->revision->getPageId(), $this->revision->getId(), $this->revision->getTimestamp() ); } /** * Prune any output that depends on the revision ID. * * @param int|bool $actualPageId The actual page id, to check the used speculative page ID * against; false, to not purge on vary-page-id; true, to purge on vary-page-id * unconditionally. * @param int|bool $actualRevId The actual rev id, to check the used speculative rev ID * against,; false, to not purge on vary-revision-id; true, to purge on * vary-revision-id unconditionally. * @param string|bool $actualRevTimestamp The actual rev timestamp, to check against the * parser output revision timestamp; false, to not purge on vary-revision-timestamp; * true, to purge on vary-revision-timestamp unconditionally. */ private function pruneRevisionSensitiveOutput( $actualPageId, $actualRevId, $actualRevTimestamp ) { if ( $this->revisionOutput ) { if ( $this->outputVariesOnRevisionMetaData( $this->revisionOutput, $actualPageId, $actualRevId, $actualRevTimestamp ) ) { $this->revisionOutput = null; } } else { $this->saveParseLogger->debug( __METHOD__ . ": no prepared revision output" ); } foreach ( $this->slotsOutput as $role => $output ) { if ( $this->outputVariesOnRevisionMetaData( $output, $actualPageId, $actualRevId, $actualRevTimestamp ) ) { unset( $this->slotsOutput[$role] ); } } } /** * @param RevisionRecord $revision */ private function setRevisionInternal( RevisionRecord $revision ) { $this->revision = $revision; // Force the parser to use $this->revision to resolve magic words like {{REVISIONUSER}} // if the revision is either known to be complete, or it doesn't have a revision ID set. // If it's incomplete and we have a revision ID, the parser can do better by loading // the revision from the database if needed to handle a magic word. // // The following considerations inform the logic described above: // // 1) If we have a saved revision already loaded, we want the parser to use it, instead of // loading it again. // // 2) If the revision is a fake that wraps some kind of synthetic content, such as an // error message from Article, it should be used directly and things like {{REVISIONUSER}} // should not expected to work, since there may not even be an actual revision to // refer to. // // 3) If the revision is a fake constructed around a page, a Content object, and // a revision ID, to provide backwards compatibility to code that has access to those // but not to a complete RevisionRecord for rendering, then we want the Parser to // load the actual revision from the database when it encounters a magic word like // {{REVISIONUSER}}, but we don't want to load that revision ahead of time just in case. // // 4) Previewing an edit to a template should use the submitted unsaved // MutableRevisionRecord for self-transclusions in the template's documentation (see T7278). // That revision would be complete except for the ID field. // // 5) Pre-save transform would provide a RevisionRecord that has all meta-data but is // incomplete due to not yet having content set. However, since it doesn't have a revision // ID either, the below code would still force it to be used, allowing // {{subst::REVISIONUSER}} to function as expected. if ( $this->revision->isReadyForInsertion() || !$this->revision->getId() ) { $oldCallback = $this->options->getCurrentRevisionRecordCallback(); $this->options->setCurrentRevisionRecordCallback( function ( PageReference $parserPage, $parser = null ) use ( $oldCallback ) { if ( $this->revision->getPage()->isSamePageAs( $parserPage ) ) { return $this->revision; } else { return call_user_func( $oldCallback, $parserPage, $parser ); } } ); } } /** * @param ParserOutput $parserOutput * @param int|bool $actualPageId The actual page id, to check the used speculative page ID * against; false, to not purge on vary-page-id; true, to purge on vary-page-id * unconditionally. * @param int|bool $actualRevId The actual rev id, to check the used speculative rev ID * against,; false, to not purge on vary-revision-id; true, to purge on * vary-revision-id unconditionally. * @param string|bool $actualRevTimestamp The actual rev timestamp, to check against the * parser output revision timestamp; false, to not purge on vary-revision-timestamp; * true, to purge on vary-revision-timestamp unconditionally. * @return bool */ private function outputVariesOnRevisionMetaData( ParserOutput $parserOutput, $actualPageId, $actualRevId, $actualRevTimestamp ) { $logger = $this->saveParseLogger; $varyMsg = __METHOD__ . ": cannot use prepared output for '{title}'"; $context = [ 'title' => (string)$this->revision->getPage() ]; if ( $parserOutput->getOutputFlag( ParserOutputFlags::VARY_REVISION ) ) { // If {{PAGEID}} resolved to 0, then that word need to resolve to the actual page ID $logger->info( "$varyMsg (vary-revision)", $context ); return true; } elseif ( $parserOutput->getOutputFlag( ParserOutputFlags::VARY_REVISION_ID ) && $actualRevId !== false && ( $actualRevId === true || $parserOutput->getSpeculativeRevIdUsed() !== $actualRevId ) ) { $logger->info( "$varyMsg (vary-revision-id and wrong ID)", $context ); return true; } elseif ( $parserOutput->getOutputFlag( ParserOutputFlags::VARY_REVISION_TIMESTAMP ) && $actualRevTimestamp !== false && ( $actualRevTimestamp === true || $parserOutput->getRevisionTimestampUsed() !== $actualRevTimestamp ) ) { $logger->info( "$varyMsg (vary-revision-timestamp and wrong timestamp)", $context ); return true; } elseif ( $parserOutput->getOutputFlag( ParserOutputFlags::VARY_PAGE_ID ) && $actualPageId !== false && ( $actualPageId === true || $parserOutput->getSpeculativePageIdUsed() !== $actualPageId ) ) { $logger->info( "$varyMsg (vary-page-id and wrong ID)", $context ); return true; } elseif ( $parserOutput->getOutputFlag( ParserOutputFlags::VARY_REVISION_EXISTS ) ) { // If {{REVISIONID}} resolved to '', it now needs to resolve to '-'. // Note that edit stashing always uses '-', which can be used for both // edit filter checks and canonical parser cache. $logger->info( "$varyMsg (vary-revision-exists)", $context ); return true; } elseif ( $parserOutput->getOutputFlag( ParserOutputFlags::VARY_REVISION_SHA1 ) && $parserOutput->getRevisionUsedSha1Base36() !== $this->revision->getSha1() ) { // If a self-transclusion used the proposed page text, it must match the final // page content after PST transformations and automatically merged edit conflicts $logger->info( "$varyMsg (vary-revision-sha1 with wrong SHA-1)", $context ); return true; } // NOTE: In the original fix for T135261, the output was discarded if ParserOutputFlags::VARY_USER was // set for a null-edit. The reason was that the original rendering in that case was // targeting the user making the null-edit, not the user who made the original edit, // causing {{REVISIONUSER}} to return the wrong name. // This case is now expected to be handled by the code in RevisionRenderer that // constructs the ParserOptions: For a null-edit, setCurrentRevisionRecordCallback is // called with the old, existing revision. $logger->debug( __METHOD__ . ": reusing prepared output for '{title}'", $context ); return false; } } PK ! _�s�, , ArchiveSelectQueryBuilder.phpnu �Iw�� <?php /** * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Revision; use Wikimedia\Rdbms\IReadableDatabase; use Wikimedia\Rdbms\SelectQueryBuilder; /** * Help and centralize querying archive table. * * @since 1.41 */ class ArchiveSelectQueryBuilder extends SelectQueryBuilder { /** * @internal use RevisionStore::newSelectQueryBuilder() instead. * @param IReadableDatabase $db */ public function __construct( IReadableDatabase $db ) { parent::__construct( $db ); $this->select( [ 'ar_id', 'ar_page_id', 'ar_namespace', 'ar_title', 'ar_rev_id', 'ar_timestamp', 'ar_minor_edit', 'ar_deleted', 'ar_len', 'ar_parent_id', 'ar_sha1', 'ar_actor', 'ar_user' => 'archive_actor.actor_user', 'ar_user_text' => 'archive_actor.actor_name', ] ) ->from( 'archive' ) ->join( 'actor', 'archive_actor', 'actor_id=ar_actor' ); } /** * Join the query with comment table and several fields to allow easier query. * * @return $this */ public function joinComment() { $this->fields( [ 'ar_comment_text' => 'comment_ar_comment.comment_text', 'ar_comment_data' => 'comment_ar_comment.comment_data', 'ar_comment_cid' => 'comment_ar_comment.comment_id', ] ); $this->join( 'comment', "comment_ar_comment", 'comment_ar_comment.comment_id = ar_comment_id' ); return $this; } } PK ! <�` ` RevisionArchiveRecord.phpnu �Iw�� <?php /** * A RevisionRecord representing a revision of a deleted page persisted in the archive table. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Revision; use MediaWiki\CommentStore\CommentStoreComment; use MediaWiki\Page\PageIdentity; use MediaWiki\Permissions\Authority; use MediaWiki\User\UserIdentity; use MediaWiki\Utils\MWTimestamp; use stdClass; use Wikimedia\Assert\Assert; /** * A RevisionRecord representing a revision of a deleted page persisted in the archive table. * Most getters on RevisionArchiveRecord will never return null. However, getId() and * getParentId() may indeed return null if this information was not stored when the archive entry * was created. * * @since 1.31 * @since 1.32 Renamed from MediaWiki\Storage\RevisionArchiveRecord */ class RevisionArchiveRecord extends RevisionRecord { /** * @var int */ protected $mArchiveId; /** * @note Avoid calling this constructor directly. Use the appropriate methods * in RevisionStore instead. * * @param PageIdentity $page The page this RevisionRecord is associated with. * @param UserIdentity $user * @param CommentStoreComment $comment * @param stdClass $row An archive table row. Use RevisionStore::getArchiveQueryInfo() to build * a query that yields the required fields. * @param RevisionSlots $slots The slots of this revision. * @param false|string $wikiId Relevant wiki or self::LOCAL for the current one. */ public function __construct( PageIdentity $page, UserIdentity $user, CommentStoreComment $comment, stdClass $row, RevisionSlots $slots, $wikiId = self::LOCAL ) { parent::__construct( $page, $slots, $wikiId ); $timestamp = MWTimestamp::convert( TS_MW, $row->ar_timestamp ); Assert::parameter( is_string( $timestamp ), '$row->rev_timestamp', 'must be a valid timestamp' ); $this->mArchiveId = intval( $row->ar_id ); // NOTE: ar_page_id may be different from $this->mPage->getId() in some cases, // notably when a partially restored page has been moved, and a new page has been created // with the same title. Archive rows for that title will then have the wrong page id. $this->mPageId = isset( $row->ar_page_id ) ? intval( $row->ar_page_id ) : $this->getArticleId( $this->mPage ); // NOTE: ar_parent_id = 0 indicates that there is no parent revision, while null // indicates that the parent revision is unknown. As per MW 1.31, the database schema // allows ar_parent_id to be NULL. $this->mParentId = isset( $row->ar_parent_id ) ? intval( $row->ar_parent_id ) : null; $this->mId = isset( $row->ar_rev_id ) ? intval( $row->ar_rev_id ) : null; $this->mComment = $comment; $this->mUser = $user; $this->mTimestamp = $timestamp; $this->mMinorEdit = (bool)$row->ar_minor_edit; $this->mDeleted = intval( $row->ar_deleted ); $this->mSize = isset( $row->ar_len ) ? intval( $row->ar_len ) : null; $this->mSha1 = !empty( $row->ar_sha1 ) ? $row->ar_sha1 : null; } /** * Get archive row ID * * @return int */ public function getArchiveId() { return $this->mArchiveId; } /** * @param string|false $wikiId The wiki ID expected by the caller. * @return int|null The revision id, or null if the original revision ID * was not recorded in the archive table. */ public function getId( $wikiId = self::LOCAL ) { // overwritten just to refine the contract specification. return parent::getId( $wikiId ); } /** * @throws RevisionAccessException if the size was unknown and could not be calculated. * @return int The nominal revision size, never null. May be computed on the fly. */ public function getSize() { // If length is null, calculate and remember it (potentially SLOW!). // This is for compatibility with old database rows that don't have the field set. $this->mSize ??= $this->mSlots->computeSize(); return $this->mSize; } /** * @throws RevisionAccessException if the hash was unknown and could not be calculated. * @return string The revision hash, never null. May be computed on the fly. */ public function getSha1() { // If hash is null, calculate it and remember (potentially SLOW!) // This is for compatibility with old database rows that don't have the field set. $this->mSha1 ??= $this->mSlots->computeSha1(); return $this->mSha1; } /** * @param int $audience * @param Authority|null $performer * * @return UserIdentity The identity of the revision author, null if access is forbidden. */ public function getUser( $audience = self::FOR_PUBLIC, ?Authority $performer = null ) { // overwritten just to add a guarantee to the contract return parent::getUser( $audience, $performer ); } /** * @param int $audience * @param Authority|null $performer * * @return CommentStoreComment The revision comment, null if access is forbidden. */ public function getComment( $audience = self::FOR_PUBLIC, ?Authority $performer = null ) { // overwritten just to add a guarantee to the contract return parent::getComment( $audience, $performer ); } /** * @return string never null */ public function getTimestamp() { // overwritten just to add a guarantee to the contract return parent::getTimestamp(); } public function userCan( $field, Authority $performer ) { // This revision belongs to a deleted page, so check the relevant permissions as well. (T345777) // Viewing the content requires either 'deletedtext' or 'undelete' (for legacy reasons) if ( $field === self::DELETED_TEXT && !$performer->authorizeRead( 'deletedtext', $this->getPage() ) && !$performer->authorizeRead( 'undelete', $this->getPage() ) ) { return false; } // Viewing the edit summary requires 'deletedhistory' if ( $field === self::DELETED_COMMENT && !$performer->authorizeRead( 'deletedhistory', $this->getPage() ) ) { return false; } // Other fields of revisions of deleted pages are public, per T232389 (unless revision-deleted) return parent::userCan( $field, $performer ); } public function audienceCan( $field, $audience, ?Authority $performer = null ) { // This revision belongs to a deleted page, so check the relevant permissions as well. (T345777) // See userCan(). if ( $audience == self::FOR_PUBLIC && ( $field === self::DELETED_TEXT || $field === self::DELETED_COMMENT ) ) { // TODO: Should this use PermissionManager::isEveryoneAllowed() or something? // But RevisionRecord::audienceCan() doesn't do that either… return false; } // This calls userCan(), which checks the user's permissions return parent::audienceCan( $field, $audience, $performer ); } /** * @see RevisionStore::isComplete * * @return bool always true. */ public function isReadyForInsertion() { return true; } } PK ! T�nw� � SuppressedDataException.phpnu �Iw�� <?php /** * Exception representing a failure to look up a revision. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Revision; /** * Exception raised in response to an audience check when attempting to * access suppressed information without permission. * * @newable * @since 1.31 * @since 1.32 Renamed from MediaWiki\Storage\SuppressedDataException */ class SuppressedDataException extends RevisionAccessException { } PK ! l�E�< < RevisionFactory.phpnu �Iw�� <?php /** * Service for constructing RevisionRecord objects. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Revision; use MediaWiki\Page\PageIdentity; use Wikimedia\Rdbms\IDBAccessObject; use Wikimedia\Rdbms\IReadableDatabase; /** * Service for constructing RevisionRecord objects. * * @since 1.31 * @since 1.32 Renamed from MediaWiki\Storage\RevisionFactory * * @note This was written to act as a drop-in replacement for the corresponding * static methods in the old Revision class (which was later removed in 1.37). */ interface RevisionFactory { /** * Constructs a RevisionRecord given a database row and content slots. * * MCR migration note: this replaced Revision::newFromRow for rows based on the * revision, slot, and content tables defined for MCR since MW1.31. * * @param \stdClass $row A query result row as a raw object. * Use getQueryInfo() to build a query that yields the required fields. * @param int $queryFlags Flags for lazy loading behavior, see IDBAccessObject::READ_XXX. * @param PageIdentity|null $page A page object for the revision. * * @return RevisionRecord */ public function newRevisionFromRow( $row, $queryFlags = IDBAccessObject::READ_NORMAL, ?PageIdentity $page = null ); /** * Make a fake RevisionRecord object from an archive table row. This is queried * for permissions or even inserted (as in Special:Undelete). * * The user ID and user name may optionally be supplied using the aliases * ar_user and ar_user_text (the names of fields which existed before * MW 1.34). * * MCR migration note: this replaced Revision::newFromArchiveRow * * @param \stdClass $row A query result row as a raw object. * Use getArchiveQueryInfo() to build a query that yields the required fields. * @param int $queryFlags Flags for lazy loading behavior, see IDBAccessObject::READ_XXX. * @param PageIdentity|null $page * @param array $overrides An associative array that allows fields in $row to be overwritten. * Keys in this array correspond to field names in $row without the "ar_" prefix, so * $overrides['user'] will override $row->ar_user, etc. * * @return RevisionRecord */ public function newRevisionFromArchiveRow( $row, $queryFlags = IDBAccessObject::READ_NORMAL, ?PageIdentity $page = null, array $overrides = [] ); /** * Return the tables, fields, and join conditions to be selected to create * a new RevisionArchiveRecord object. * * @since 1.37, since 1.31 on RevisionStore * @deprecated since 1.41 use RevisionStore::newArchiveSelectQueryBuilder() instead. * * @return array[] With three keys: * - tables: (string[]) to include in the `$table` to `IDatabase->select()` or `SelectQueryBuilder::tables` * - fields: (string[]) to include in the `$vars` to `IDatabase->select()` or `SelectQueryBuilder::fields` * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` or `SelectQueryBuilder::joinConds` * @phan-return array{tables:string[],fields:string[],joins:array} */ public function getArchiveQueryInfo(); /** * Return the tables, fields, and join conditions to be selected to create * a new RevisionStoreRecord object. * * MCR migration note: this replaced Revision::getQueryInfo * * If the format of fields returned changes in any way then the cache key provided by * self::getRevisionRowCacheKey should be updated. * * @since 1.37, since 1.31 on RevisionStore * @deprecated since 1.41 use RevisionStore::newSelectQueryBuilder() instead. * * @param array $options Any combination of the following strings * - 'page': Join with the page table, and select fields to identify the page * - 'user': Join with the user table, and select the user name * * @return array[] With three keys: * - tables: (string[]) to include in the `$table` to `IDatabase->select()` or `SelectQueryBuilder::tables` * - fields: (string[]) to include in the `$vars` to `IDatabase->select()` or `SelectQueryBuilder::fields` * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` or `SelectQueryBuilder::joinConds` * @phan-return array{tables:string[],fields:string[],joins:array} */ public function getQueryInfo( $options = [] ); /** * Return a SelectQueryBuilder to allow querying revision store * * @since 1.41 * * @param IReadableDatabase $dbr A db object to do the query on. * * @return RevisionSelectQueryBuilder */ public function newSelectQueryBuilder( IReadableDatabase $dbr ): RevisionSelectQueryBuilder; /** * Return a SelectQueryBuilder to allow querying archive table * * @since 1.41 * * @param IReadableDatabase $dbr A db object to do the query on. * * @return ArchiveSelectQueryBuilder */ public function newArchiveSelectQueryBuilder( IReadableDatabase $dbr ): ArchiveSelectQueryBuilder; /** * Determine whether the parameter is a row containing all the fields * that RevisionFactory needs to create a RevisionRecord from the row. * * @param mixed $row * @param string $table 'archive' or empty * @return bool */ public function isRevisionRow( $row, string $table = '' ); } PK ! �!j#�. �. RevisionRenderer.phpnu �Iw�� <?php /** * This file is part of MediaWiki. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Revision; use InvalidArgumentException; use MediaWiki\Content\Renderer\ContentRenderer; use MediaWiki\Html\Html; use MediaWiki\Parser\ParserOptions; use MediaWiki\Parser\ParserOutput; use MediaWiki\Permissions\Authority; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Wikimedia\Rdbms\ILoadBalancer; /** * The RevisionRenderer service provides access to rendered output for revisions. * It does so by acting as a factory for RenderedRevision instances, which in turn * provide lazy access to ParserOutput objects. * * One key responsibility of RevisionRenderer is implementing the layout used to combine * the output of multiple slots. * * @since 1.32 */ class RevisionRenderer { /** @var LoggerInterface */ private $saveParseLogger; /** @var ILoadBalancer */ private $loadBalancer; /** @var SlotRoleRegistry */ private $roleRegistry; /** @var ContentRenderer */ private $contentRenderer; /** @var string|false */ private $dbDomain; /** * @param ILoadBalancer $loadBalancer * @param SlotRoleRegistry $roleRegistry * @param ContentRenderer $contentRenderer * @param string|false $dbDomain DB domain of the relevant wiki or false for the current one */ public function __construct( ILoadBalancer $loadBalancer, SlotRoleRegistry $roleRegistry, ContentRenderer $contentRenderer, $dbDomain = false ) { $this->loadBalancer = $loadBalancer; $this->roleRegistry = $roleRegistry; $this->contentRenderer = $contentRenderer; $this->dbDomain = $dbDomain; $this->saveParseLogger = new NullLogger(); } /** * @param LoggerInterface $saveParseLogger */ public function setLogger( LoggerInterface $saveParseLogger ) { $this->saveParseLogger = $saveParseLogger; } // phpcs:disable Generic.Files.LineLength.TooLong /** * @param RevisionRecord $rev * @param ParserOptions|null $options * @param Authority|null $forPerformer User for privileged access. Default is unprivileged * (public) access, unless the 'audience' hint is set to something else RevisionRecord::RAW. * @param array{use-master?:bool,audience?:int,known-revision-output?:ParserOutput,causeAction?:?string,previous-output?:?ParserOutput} $hints * Hints given as an associative array. Known keys: * - 'use-master' Use primary DB when rendering for the parser cache during save. * Default is to use a replica. * - 'audience' the audience to use for content access. Default is * RevisionRecord::FOR_PUBLIC if $forUser is not set, RevisionRecord::FOR_THIS_USER * if $forUser is set. Can be set to RevisionRecord::RAW to disable audience checks. * - 'known-revision-output' a combined ParserOutput for the revision, perhaps from * some cache. the caller is responsible for ensuring that the ParserOutput indeed * matched the $rev and $options. This mechanism is intended as a temporary stop-gap, * for the time until caches have been changed to store RenderedRevision states instead * of ParserOutput objects. * - 'previous-output' A previously-rendered ParserOutput for this page. This * can be used by Parsoid for selective updates. * - 'causeAction' the reason for rendering. This should be informative, for used for * logging and debugging. * * @return RenderedRevision|null The rendered revision, or null if the audience checks fails. * @throws BadRevisionException * @throws RevisionAccessException */ // phpcs:enable Generic.Files.LineLength.TooLong public function getRenderedRevision( RevisionRecord $rev, ?ParserOptions $options = null, ?Authority $forPerformer = null, array $hints = [] ) { if ( $rev->getWikiId() !== $this->dbDomain ) { throw new InvalidArgumentException( 'Mismatching wiki ID ' . $rev->getWikiId() ); } $audience = $hints['audience'] ?? ( $forPerformer ? RevisionRecord::FOR_THIS_USER : RevisionRecord::FOR_PUBLIC ); if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, $audience, $forPerformer ) ) { // Returning null here is awkward, but consistent with the signature of // RevisionRecord::getContent(). return null; } if ( !$options ) { $options = $forPerformer ? ParserOptions::newFromUser( $forPerformer->getUser() ) : ParserOptions::newFromAnon(); } if ( isset( $hints['causeAction'] ) ) { $options->setRenderReason( $hints['causeAction'] ); } $usePrimary = $hints['use-master'] ?? false; $dbIndex = $usePrimary ? DB_PRIMARY // use latest values : DB_REPLICA; // T154554 $options->setSpeculativeRevIdCallback( function () use ( $dbIndex ) { return $this->getSpeculativeRevId( $dbIndex ); } ); $options->setSpeculativePageIdCallback( function () use ( $dbIndex ) { return $this->getSpeculativePageId( $dbIndex ); } ); if ( !$rev->getId() && $rev->getTimestamp() ) { // This is an unsaved revision with an already determined timestamp. // Make the "current" time used during parsing match that of the revision. // Any REVISION* parser variables will match up if the revision is saved. $options->setTimestamp( $rev->getTimestamp() ); } $previousOutput = $hints['previous-output'] ?? null; $renderedRevision = new RenderedRevision( $rev, $options, $this->contentRenderer, function ( RenderedRevision $rrev, array $hints ) use ( $options, $previousOutput ) { $h = [ 'previous-output' => $previousOutput ] + $hints; return $this->combineSlotOutput( $rrev, $options, $h ); }, $audience, $forPerformer ); $renderedRevision->setSaveParseLogger( $this->saveParseLogger ); if ( isset( $hints['known-revision-output'] ) ) { $renderedRevision->setRevisionParserOutput( $hints['known-revision-output'] ); } return $renderedRevision; } private function getSpeculativeRevId( $dbIndex ) { // Use a separate primary DB connection in order to see the latest data, by avoiding // stale data from REPEATABLE-READ snapshots. $flags = ILoadBalancer::CONN_TRX_AUTOCOMMIT; $db = $this->loadBalancer->getConnection( $dbIndex, [], $this->dbDomain, $flags ); return 1 + (int)$db->newSelectQueryBuilder() ->select( 'MAX(rev_id)' ) ->from( 'revision' ) ->caller( __METHOD__ )->fetchField(); } private function getSpeculativePageId( $dbIndex ) { // Use a separate primary DB connection in order to see the latest data, by avoiding // stale data from REPEATABLE-READ snapshots. $flags = ILoadBalancer::CONN_TRX_AUTOCOMMIT; $db = $this->loadBalancer->getConnection( $dbIndex, [], $this->dbDomain, $flags ); return 1 + (int)$db->newSelectQueryBuilder() ->select( 'MAX(page_id)' ) ->from( 'page' ) ->caller( __METHOD__ )->fetchField(); } /** * This implements the layout for combining the output of multiple slots. * * @todo Use placement hints from SlotRoleHandlers instead of hard-coding the layout. * * @param RenderedRevision $rrev * @param ParserOptions $options * @param array $hints see RenderedRevision::getRevisionParserOutput() * * @return ParserOutput * @throws SuppressedDataException * @throws BadRevisionException * @throws RevisionAccessException */ private function combineSlotOutput( RenderedRevision $rrev, ParserOptions $options, array $hints = [] ) { $revision = $rrev->getRevision(); $slots = $revision->getSlots()->getSlots(); $withHtml = $hints['generate-html'] ?? true; $previousOutputs = $this->splitSlotOutput( $rrev, $options, $hints['previous-output'] ?? null ); // short circuit if there is only the main slot // T351026 hack: if use-parsoid is set, only return main slot output for now // T351113 will remove this hack. if ( array_keys( $slots ) === [ SlotRecord::MAIN ] || $options->getUseParsoid() ) { $h = [ 'previous-output' => $previousOutputs[SlotRecord::MAIN] ] + $hints; return $rrev->getSlotParserOutput( SlotRecord::MAIN, $h ); } // move main slot to front if ( isset( $slots[SlotRecord::MAIN] ) ) { $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots; } $combinedOutput = new ParserOutput( null ); $slotOutput = []; $options = $rrev->getOptions(); $options->registerWatcher( [ $combinedOutput, 'recordOption' ] ); foreach ( $slots as $role => $slot ) { $h = [ 'previous-output' => $previousOutputs[$role] ] + $hints; $out = $rrev->getSlotParserOutput( $role, $h ); $slotOutput[$role] = $out; // XXX: should the SlotRoleHandler be able to intervene here? $combinedOutput->mergeInternalMetaDataFrom( $out ); $combinedOutput->mergeTrackingMetaDataFrom( $out ); } if ( $withHtml ) { $html = ''; $first = true; /** @var ParserOutput $out */ foreach ( $slotOutput as $role => $out ) { $roleHandler = $this->roleRegistry->getRoleHandler( $role ); // TODO: put more fancy layout logic here, see T200915. $layout = $roleHandler->getOutputLayoutHints(); $display = $layout['display'] ?? 'section'; if ( $display === 'none' ) { continue; } if ( $first ) { // skip header for the first slot $first = false; } else { // NOTE: this placeholder is hydrated by ParserOutput::getText(). $headText = Html::element( 'mw:slotheader', [], $role ); $html .= Html::rawElement( 'h1', [ 'class' => 'mw-slot-header' ], $headText ); } // XXX: do we want to put a wrapper div around the output? // Do we want to let $roleHandler do that? $html .= $out->getRawText(); $combinedOutput->mergeHtmlMetaDataFrom( $out ); } $combinedOutput->setRawText( $html ); } $options->registerWatcher( null ); return $combinedOutput; } /** * This reverses ::combineSlotOutput() in order to enable selective * update of individual slots. * * @todo Currently this doesn't do much other than disable selective * update if there is more than one slot. But in the case where * slot combination is reversible, this should reverse it and attempt * to reconstruct the original split ParserOutputs from the merged * ParserOutput. * * @param RenderedRevision $rrev * @param ParserOptions $options * @param ?ParserOutput $previousOutput A combined ParserOutput for a * previous parse, or null if none available. * @return array<string,?ParserOutput> A mapping from role name to a * previous ParserOutput for that slot in the previous parse */ private function splitSlotOutput( RenderedRevision $rrev, ParserOptions $options, ?ParserOutput $previousOutput ) { // If there is no previous parse, then there is nothing to split. $revision = $rrev->getRevision(); $revslots = $revision->getSlots(); if ( $previousOutput === null ) { return array_fill_keys( $revslots->getSlotRoles(), null ); } // short circuit if there is only the main slot // T351026 hack: if use-parsoid is set, only return main slot output for now // T351113 will remove this hack. if ( $revslots->getSlotRoles() === [ SlotRecord::MAIN ] || $options->getUseParsoid() ) { return [ SlotRecord::MAIN => $previousOutput ]; } // @todo Currently slot combination is not reversible return array_fill_keys( $revslots->getSlotRoles(), null ); } } PK ! � Ά � MutableRevisionSlots.phpnu �Iw�� <?php /** * Mutable version of RevisionSlots, for constructing a new revision. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Revision; use MediaWiki\Content\Content; /** * Mutable version of RevisionSlots, for constructing a new revision. * * @since 1.31 * @since 1.32 Renamed from MediaWiki\Storage\MutableRevisionSlots */ class MutableRevisionSlots extends RevisionSlots { /** * @var callable|null */ private $resetCallback; /** * Constructs a MutableRevisionSlots that inherits from the given * list of slots. * * @param SlotRecord[] $slots * @param callable|null $resetCallback Callback to be triggered whenever slots change. * Signature: function ( MutableRevisionSlots ): void. * * @return MutableRevisionSlots */ public static function newFromParentRevisionSlots( array $slots, ?callable $resetCallback = null ) { $inherited = []; foreach ( $slots as $slot ) { $role = $slot->getRole(); $inherited[$role] = SlotRecord::newInherited( $slot ); } return new MutableRevisionSlots( $inherited, $resetCallback ); } /** * @param SlotRecord[] $slots An array of SlotRecords. * @param callable|null $resetCallback Callback to be triggered whenever slots change. * Signature: function ( MutableRevisionSlots ): void. */ public function __construct( array $slots = [], ?callable $resetCallback = null ) { // @phan-suppress-next-line PhanTypeInvalidCallableArraySize parent::__construct( $slots ); $this->resetCallback = $resetCallback; } /** * Sets the given slot. * If a slot with the same role is already present, it is replaced. * * @param SlotRecord $slot */ public function setSlot( SlotRecord $slot ) { if ( !is_array( $this->slots ) ) { $this->getSlots(); // initialize $this->slots } $role = $slot->getRole(); $this->slots[$role] = $slot; $this->triggerResetCallback(); } /** * Sets the given slot to an inherited version of $slot. * If a slot with the same role is already present, it is replaced. * * @param SlotRecord $slot */ public function inheritSlot( SlotRecord $slot ) { $this->setSlot( SlotRecord::newInherited( $slot ) ); } /** * Sets the content for the slot with the given role. * If a slot with the same role is already present, it is replaced. * * @param string $role * @param Content $content */ public function setContent( $role, Content $content ) { $slot = SlotRecord::newUnsaved( $role, $content ); $this->setSlot( $slot ); } /** * Remove the slot for the given role, discontinue the corresponding stream. * * @param string $role */ public function removeSlot( $role ) { if ( !is_array( $this->slots ) ) { $this->getSlots(); // initialize $this->slots } unset( $this->slots[$role] ); $this->triggerResetCallback(); } /** * Trigger the reset callback supplied to the constructor, if any. */ private function triggerResetCallback() { if ( $this->resetCallback ) { ( $this->resetCallback )( $this ); } } } PK ! �"�� RevisionStoreFactory.phpnu �Iw�� <?php /** * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * Attribution notice: when this file was created, much of its content was taken * from the Revision.php file as present in release 1.30. Refer to the history * of that file for original authorship (that file was removed entirely in 1.37, * but its history can still be found in prior versions of MediaWiki). * * @file */ namespace MediaWiki\Revision; use MediaWiki\CommentStore\CommentStore; use MediaWiki\Content\IContentHandlerFactory; use MediaWiki\HookContainer\HookContainer; use MediaWiki\Page\PageStoreFactory; use MediaWiki\Storage\BlobStoreFactory; use MediaWiki\Storage\NameTableStoreFactory; use MediaWiki\Title\TitleFactory; use MediaWiki\User\ActorStore; use MediaWiki\User\ActorStoreFactory; use Psr\Log\LoggerInterface; use Wikimedia\Assert\Assert; use Wikimedia\ObjectCache\BagOStuff; use Wikimedia\ObjectCache\WANObjectCache; use Wikimedia\Rdbms\ILBFactory; /** * Factory service for RevisionStore instances. This allows RevisionStores to be created for * cross-wiki access. * * @warning Beware compatibility issues with schema migration in the context of cross-wiki access! * This class assumes that all wikis are at compatible migration stages for all relevant schemas. * Relevant schemas are: revision storage (MCR), the revision comment table, and the actor table. * Migration stages are compatible as long as a) there are no wikis in the cluster that only write * the old schema or b) there are no wikis that read only the new schema. * * @since 1.32 */ class RevisionStoreFactory { /** @var BlobStoreFactory */ private $blobStoreFactory; /** @var ILBFactory */ private $dbLoadBalancerFactory; /** @var WANObjectCache */ private $cache; /** @var BagOStuff */ private $localCache; /** @var LoggerInterface */ private $logger; /** @var CommentStore */ private $commentStore; /** @var ActorStoreFactory */ private $actorStoreFactory; /** @var NameTableStoreFactory */ private $nameTables; /** @var SlotRoleRegistry */ private $slotRoleRegistry; /** @var IContentHandlerFactory */ private $contentHandlerFactory; /** @var PageStoreFactory */ private $pageStoreFactory; /** @var TitleFactory */ private $titleFactory; /** @var HookContainer */ private $hookContainer; /** * @param ILBFactory $dbLoadBalancerFactory * @param BlobStoreFactory $blobStoreFactory * @param NameTableStoreFactory $nameTables * @param SlotRoleRegistry $slotRoleRegistry * @param WANObjectCache $cache * @param BagOStuff $localCache * @param CommentStore $commentStore * @param ActorStoreFactory $actorStoreFactory * @param LoggerInterface $logger * @param IContentHandlerFactory $contentHandlerFactory * @param PageStoreFactory $pageStoreFactory * @param TitleFactory $titleFactory * @param HookContainer $hookContainer */ public function __construct( ILBFactory $dbLoadBalancerFactory, BlobStoreFactory $blobStoreFactory, NameTableStoreFactory $nameTables, SlotRoleRegistry $slotRoleRegistry, WANObjectCache $cache, BagOStuff $localCache, CommentStore $commentStore, ActorStoreFactory $actorStoreFactory, LoggerInterface $logger, IContentHandlerFactory $contentHandlerFactory, PageStoreFactory $pageStoreFactory, TitleFactory $titleFactory, HookContainer $hookContainer ) { $this->dbLoadBalancerFactory = $dbLoadBalancerFactory; $this->blobStoreFactory = $blobStoreFactory; $this->slotRoleRegistry = $slotRoleRegistry; $this->nameTables = $nameTables; $this->cache = $cache; $this->localCache = $localCache; $this->commentStore = $commentStore; $this->actorStoreFactory = $actorStoreFactory; $this->logger = $logger; $this->contentHandlerFactory = $contentHandlerFactory; $this->pageStoreFactory = $pageStoreFactory; $this->titleFactory = $titleFactory; $this->hookContainer = $hookContainer; } /** * @since 1.32 * * @param false|string $dbDomain DB domain of the relevant wiki or false for the current one * * @return RevisionStore for the given wikiId with all necessary services */ public function getRevisionStore( $dbDomain = false ): RevisionStore { return $this->getStore( $dbDomain, $this->actorStoreFactory->getActorStore( $dbDomain ) ); } /** * @since 1.42 * * @param false|string $dbDomain DB domain of the relevant wiki or false for the current one * * @return RevisionStore for the given wikiId with all necessary services */ public function getRevisionStoreForImport( $dbDomain = false ): RevisionStore { return $this->getStore( $dbDomain, $this->actorStoreFactory->getActorStoreForImport( $dbDomain ) ); } /** * @since 1.43 * * @param false|string $dbDomain DB domain of the relevant wiki or false for the current one * * @return RevisionStore for the given wikiId with all necessary services */ public function getRevisionStoreForUndelete( $dbDomain = false ): RevisionStore { return $this->getStore( $dbDomain, $this->actorStoreFactory->getActorStoreForUndelete( $dbDomain ) ); } /** * @param false|string $dbDomain * @param ActorStore $actorStore * * @return RevisionStore */ private function getStore( $dbDomain, ActorStore $actorStore ) { Assert::parameterType( [ 'string', 'false' ], $dbDomain, '$dbDomain' ); $store = new RevisionStore( $this->dbLoadBalancerFactory->getMainLB( $dbDomain ), $this->blobStoreFactory->newSqlBlobStore( $dbDomain ), $this->cache, // Pass cache local to wiki; Leave cache sharing to RevisionStore. $this->localCache, $this->commentStore, $this->nameTables->getContentModels( $dbDomain ), $this->nameTables->getSlotRoles( $dbDomain ), $this->slotRoleRegistry, $actorStore, $this->contentHandlerFactory, $this->pageStoreFactory->getPageStore( $dbDomain ), $this->titleFactory, $this->hookContainer, $dbDomain ); $store->setLogger( $this->logger ); return $store; } } PK ! �k���! �! RevisionSlots.phpnu �Iw�� <?php /** * Value object representing the set of slots belonging to a revision. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Revision; use MediaWiki\Content\Content; use Wikimedia\Assert\Assert; use Wikimedia\NonSerializable\NonSerializableTrait; /** * Value object representing the set of slots belonging to a revision. * * @note RevisionSlots provides "raw" access to the slots and does not apply audience checks. * If audience checks are desired, use RevisionRecord::getSlot() or RevisionRecord::getContent() * instead. * * @newable * * @since 1.31 * @since 1.32 Renamed from MediaWiki\Storage\RevisionSlots */ class RevisionSlots { use NonSerializableTrait; /** @var SlotRecord[]|callable */ protected $slots; /** * @stable to call. * * @param SlotRecord[]|callable $slots SlotRecords, * or a callback that returns such a structure. */ public function __construct( $slots ) { Assert::parameterType( [ 'array', 'callable' ], $slots, '$slots' ); if ( is_callable( $slots ) ) { $this->slots = $slots; } else { $this->setSlotsInternal( $slots ); } } /** * @param SlotRecord[] $slots */ private function setSlotsInternal( array $slots ): void { Assert::parameterElementType( SlotRecord::class, $slots, '$slots' ); $this->slots = []; // re-key the slot array foreach ( $slots as $slot ) { $role = $slot->getRole(); $this->slots[$role] = $slot; } } /** * Returns the Content of the given slot. * Call getSlotNames() to get a list of available slots. * * Note that for mutable Content objects, each call to this method will return a * fresh clone. * * @see SlotRecord::getContent() * * @param string $role The role name of the desired slot * * @throws RevisionAccessException if the slot does not exist or slot data * could not be lazy-loaded. See SlotRecord::getContent() for details. * @return Content */ public function getContent( $role ): Content { // Return a copy to be safe. Immutable content objects return $this from copy(). return $this->getSlot( $role )->getContent()->copy(); } /** * Returns the SlotRecord of the given slot. * Call getSlotNames() to get a list of available slots. * * @param string $role The role name of the desired slot * * @throws RevisionAccessException if the slot does not exist or slot data * could not be lazy-loaded. * @return SlotRecord */ public function getSlot( $role ): SlotRecord { $slots = $this->getSlots(); if ( isset( $slots[$role] ) ) { return $slots[$role]; } else { throw new RevisionAccessException( 'No such slot: {role}', [ 'role' => $role ] ); } } /** * Returns whether the given slot is set. * * @param string $role The role name of the desired slot * * @return bool */ public function hasSlot( $role ): bool { $slots = $this->getSlots(); return isset( $slots[$role] ); } /** * Returns the slot names (roles) of all slots present in this revision. * getContent() will succeed only for the names returned by this method. * * @return string[] */ public function getSlotRoles(): array { $slots = $this->getSlots(); return array_keys( $slots ); } /** * Computes the total nominal size of the revision's slots, in bogo-bytes. * * @warning This is potentially expensive! It may cause some slots' content to be loaded * and deserialized. * * @return int */ public function computeSize(): int { return array_reduce( $this->getPrimarySlots(), static function ( $accu, SlotRecord $slot ) { return $accu + $slot->getSize(); }, 0 ); } /** * Returns an associative array that maps role names to SlotRecords. Each SlotRecord * represents the content meta-data of a slot, together they define the content of * a revision. * * @note This may cause the content meta-data for the revision to be lazy-loaded. * * @return SlotRecord[] revision slot/content rows, keyed by slot role name. */ public function getSlots(): array { if ( is_callable( $this->slots ) ) { $slots = call_user_func( $this->slots ); Assert::postcondition( is_array( $slots ), 'Slots info callback should return an array of objects' ); $this->setSlotsInternal( $slots ); } return $this->slots; } /** * Computes the combined hash of the revisions's slots. * * @note For backwards compatibility, the combined hash of a single slot * is that slot's hash. For consistency, the combined hash of an empty set of slots * is the hash of the empty string. * * @warning This is potentially expensive! It may cause some slots' content to be loaded * and deserialized, then re-serialized and hashed. * * @return string */ public function computeSha1(): string { $slots = $this->getPrimarySlots(); ksort( $slots ); if ( !$slots ) { return SlotRecord::base36Sha1( '' ); } return array_reduce( $slots, static function ( $accu, SlotRecord $slot ) { return $accu === null ? $slot->getSha1() : SlotRecord::base36Sha1( $accu . $slot->getSha1() ); }, null ); } /** * Return all slots that belong to the revision they originate from (that is, * they are not inherited from some other revision). * * @note This may cause the slot meta-data for the revision to be lazy-loaded. * * @return SlotRecord[] */ public function getOriginalSlots(): array { return array_filter( $this->getSlots(), static function ( SlotRecord $slot ) { return !$slot->isInherited(); } ); } /** * Return all slots that are not originate in the revision they belong to (that is, * they are inherited from some other revision). * * @note This may cause the slot meta-data for the revision to be lazy-loaded. * * @return SlotRecord[] */ public function getInheritedSlots(): array { return array_filter( $this->getSlots(), static function ( SlotRecord $slot ) { return $slot->isInherited(); } ); } /** * Return all primary slots (those that are not derived). * * @return SlotRecord[] * @since 1.36 */ public function getPrimarySlots(): array { return array_filter( $this->getSlots(), static function ( SlotRecord $slot ) { return !$slot->isDerived(); } ); } /** * Checks whether the other RevisionSlots instance has the same content * as this instance. Note that this does not mean that the slots have to be the same: * they could for instance belong to different revisions. * * @param RevisionSlots $other * * @return bool */ public function hasSameContent( RevisionSlots $other ): bool { if ( $other === $this ) { return true; } $aSlots = $this->getSlots(); $bSlots = $other->getSlots(); ksort( $aSlots ); ksort( $bSlots ); if ( array_keys( $aSlots ) !== array_keys( $bSlots ) ) { return false; } foreach ( $aSlots as $role => $s ) { $t = $bSlots[$role]; if ( !$s->hasSameContent( $t ) ) { return false; } } return true; } /** * Find roles for which the $other RevisionSlots object has different content * as this RevisionSlots object, including any roles that are present in one * but not the other. * * @param RevisionSlots $other * * @return string[] a list of slot roles that are different. */ public function getRolesWithDifferentContent( RevisionSlots $other ): array { if ( $other === $this ) { return []; } $aSlots = $this->getSlots(); $bSlots = $other->getSlots(); ksort( $aSlots ); ksort( $bSlots ); $different = array_keys( array_merge( array_diff_key( $aSlots, $bSlots ), array_diff_key( $bSlots, $aSlots ) ) ); /** @var SlotRecord[] $common */ $common = array_intersect_key( $aSlots, $bSlots ); foreach ( $common as $role => $s ) { $t = $bSlots[$role]; if ( !$s->hasSameContent( $t ) ) { $different[] = $role; } } return $different; } } PK ! �Xz� � RevisionSelectQueryBuilder.phpnu �Iw�� <?php /** * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Revision; use Wikimedia\Rdbms\IReadableDatabase; use Wikimedia\Rdbms\SelectQueryBuilder; /** * Help and centralize querying revision table. * * @since 1.41 */ class RevisionSelectQueryBuilder extends SelectQueryBuilder { /** * @internal use RevisionStore::newSelectQueryBuilder() instead. * @param IReadableDatabase $db */ public function __construct( IReadableDatabase $db ) { parent::__construct( $db ); $this->select( [ 'rev_id', 'rev_page', 'rev_timestamp', 'rev_minor_edit', 'rev_deleted', 'rev_len', 'rev_parent_id', 'rev_sha1', 'rev_user' => 'actor_rev_user.actor_user', 'rev_user_text' => 'actor_rev_user.actor_name', 'rev_actor' => 'rev_actor' ] ) ->from( 'revision' ) ->join( 'actor', 'actor_rev_user', 'actor_rev_user.actor_id = rev_actor' ); } /** * Join the query with user table and add user_name field. * * @return $this */ public function joinUser() { $this->field( 'user_name' ) ->leftJoin( 'user', null, [ $this->db->expr( 'actor_rev_user.actor_user', '!=', 0 ), "user_id = actor_rev_user.actor_user" ] ); return $this; } /** * Join the query with page table and several fields to allow easier query. * * @return $this */ public function joinPage() { $this->fields( [ 'page_namespace', 'page_title', 'page_id', 'page_latest', 'page_is_redirect', 'page_len', ] ) ->join( 'page', null, 'page_id = rev_page' ); return $this; } /** * Join the query with comment table and several fields to allow easier query. * * @return $this */ public function joinComment() { $this->fields( [ 'rev_comment_text' => 'comment_rev_comment.comment_text', 'rev_comment_data' => 'comment_rev_comment.comment_data', 'rev_comment_cid' => 'comment_rev_comment.comment_id', ] ); $this->join( 'comment', "comment_rev_comment", 'comment_rev_comment.comment_id = rev_comment_id' ); return $this; } } PK ! "�BN�O �O SlotRecord.phpnu �Iw�� <?php /** * Value object representing a content slot associated with a page revision. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Revision; use InvalidArgumentException; use LogicException; use MediaWiki\Content\Content; use OutOfBoundsException; use Wikimedia\Assert\Assert; use Wikimedia\NonSerializable\NonSerializableTrait; /** * Value object representing a content slot associated with a page revision. * SlotRecord provides direct access to a Content object. * That access may be implemented through a callback. * * @since 1.31 * @since 1.32 Renamed from MediaWiki\Storage\SlotRecord */ class SlotRecord { use NonSerializableTrait; public const MAIN = 'main'; /** * @var \stdClass database result row, as a raw object. Callbacks are supported for field values, * to enable on-demand emulation of these values. This is primarily intended for use * during schema migration. */ private $row; /** * @var Content|callable */ private $content; /** * @var bool */ private $derived; /** * Returns a new SlotRecord just like the given $slot, except that calling getContent() * will fail with an exception. * * @param SlotRecord $slot * * @return SlotRecord */ public static function newWithSuppressedContent( SlotRecord $slot ) { $row = $slot->row; return new SlotRecord( $row, /** * @return never */ static function () { throw new SuppressedDataException( 'Content suppressed!' ); } ); } /** * Returns a SlotRecord for a derived slot. * * @param string $role * @param Content $content Initial content * * @return SlotRecord * @since 1.36 */ public static function newDerived( string $role, Content $content ) { return self::newUnsaved( $role, $content, true ); } /** * Constructs a new SlotRecord from an existing SlotRecord, overriding some fields. * The slot's content cannot be overwritten. * * @param SlotRecord $slot * @param array $overrides * * @return SlotRecord */ private static function newFromSlotRecord( SlotRecord $slot, array $overrides = [] ) { $row = clone $slot->row; $row->slot_id = null; // never copy the row ID! foreach ( $overrides as $key => $value ) { $row->$key = $value; } return new SlotRecord( $row, $slot->content, $slot->isDerived() ); } /** * Constructs a new SlotRecord for a new revision, inheriting the content of the given SlotRecord * of a previous revision. * * Note that a SlotRecord constructed this way are intended as prototypes, * to be used wit newSaved(). They are incomplete, so some getters such as * getRevision() will fail. * * @param SlotRecord $slot * * @return SlotRecord */ public static function newInherited( SlotRecord $slot ) { // We can't inherit from a Slot that's not attached to a revision. $slot->getRevision(); $slot->getOrigin(); $slot->getAddress(); // NOTE: slot_origin and content_address are copied from $slot. return self::newFromSlotRecord( $slot, [ 'slot_revision_id' => null, ] ); } /** * Constructs a new Slot from a Content object for a new revision. * This is the preferred way to construct a slot for storing Content that * resulted from a user edit. The slot is assumed to be not inherited. * * Note that a SlotRecord constructed this way are intended as prototypes, * to be used wit newSaved(). They are incomplete, so some getters such as * getAddress() will fail. * * @param string $role * @param Content $content * @param bool $derived * @return SlotRecord An incomplete proto-slot object, to be used with newSaved() later. */ public static function newUnsaved( string $role, Content $content, bool $derived = false ) { $row = [ 'slot_id' => null, // not yet known 'slot_revision_id' => null, // not yet known 'slot_origin' => null, // not yet known, will be set in newSaved() 'content_size' => null, // compute later 'content_sha1' => null, // compute later 'slot_content_id' => null, // not yet known, will be set in newSaved() 'content_address' => null, // not yet known, will be set in newSaved() 'role_name' => $role, 'model_name' => $content->getModel(), ]; return new SlotRecord( (object)$row, $content, $derived ); } /** * Constructs a complete SlotRecord for a newly saved revision, based on the incomplete * proto-slot. This adds information that has only become available during saving, * particularly the revision ID, content ID and content address. * * @param int $revisionId the revision the slot is to be associated with (field slot_revision_id). * If $protoSlot already has a revision, it must be the same. * @param int|null $contentId the ID of the row in the content table describing the content * referenced by $contentAddress (field slot_content_id). * If $protoSlot already has a content ID, it must be the same. * @param string $contentAddress the slot's content address (field content_address). * If $protoSlot already has an address, it must be the same. * @param SlotRecord $protoSlot The proto-slot that was provided as input for creating a new * revision. $protoSlot must have a content address if inherited. * * @return SlotRecord If the state of $protoSlot is inappropriate for saving a new revision. */ public static function newSaved( int $revisionId, ?int $contentId, string $contentAddress, SlotRecord $protoSlot ) { if ( $protoSlot->hasRevision() && $protoSlot->getRevision() !== $revisionId ) { throw new LogicException( "Mismatching revision ID $revisionId: " . "The slot already belongs to revision {$protoSlot->getRevision()}. " . "Use SlotRecord::newInherited() to re-use content between revisions." ); } if ( $protoSlot->hasAddress() && $protoSlot->getAddress() !== $contentAddress ) { throw new LogicException( "Mismatching blob address $contentAddress: " . "The slot already has content at {$protoSlot->getAddress()}." ); } if ( $protoSlot->hasContentId() && $protoSlot->getContentId() !== $contentId ) { throw new LogicException( "Mismatching content ID $contentId: " . "The slot already has content row {$protoSlot->getContentId()} associated." ); } if ( $protoSlot->isInherited() ) { if ( !$protoSlot->hasAddress() ) { throw new InvalidArgumentException( "An inherited blob should have a content address!" ); } if ( !$protoSlot->hasField( 'slot_origin' ) ) { throw new InvalidArgumentException( "A saved inherited slot should have an origin set!" ); } $origin = $protoSlot->getOrigin(); } else { $origin = $revisionId; } return self::newFromSlotRecord( $protoSlot, [ 'slot_revision_id' => $revisionId, 'slot_content_id' => $contentId, 'slot_origin' => $origin, 'content_address' => $contentAddress, ] ); } /** * The following fields are supported by the $row parameter: * * $row->blob_data * $row->blob_address * * @param \stdClass $row A database row composed of fields of the slot and content tables, * as a raw object. Any field value can be a callback that produces the field value * given this SlotRecord as a parameter. However, plain strings cannot be used as * callbacks here, for security reasons. * @param Content|callable $content The content object associated with the slot, or a * callback that will return that Content object, given this SlotRecord as a parameter. * @param bool $derived Is this handler for a derived slot? Derived slots allow information that * is derived from the content of a page to be stored even if it is generated * asynchronously or updated later. Their size is not included in the revision size, * their hash does not contribute to the revision hash, and updates are not included * in revision history. */ public function __construct( \stdClass $row, $content, bool $derived = false ) { Assert::parameterType( [ 'Content', 'callable' ], $content, '$content' ); Assert::parameter( property_exists( $row, 'slot_revision_id' ), '$row->slot_revision_id', 'must exist' ); Assert::parameter( property_exists( $row, 'slot_content_id' ), '$row->slot_content_id', 'must exist' ); Assert::parameter( property_exists( $row, 'content_address' ), '$row->content_address', 'must exist' ); Assert::parameter( property_exists( $row, 'model_name' ), '$row->model_name', 'must exist' ); Assert::parameter( property_exists( $row, 'slot_origin' ), '$row->slot_origin', 'must exist' ); Assert::parameter( !property_exists( $row, 'slot_inherited' ), '$row->slot_inherited', 'must not exist' ); Assert::parameter( !property_exists( $row, 'slot_revision' ), '$row->slot_revision', 'must not exist' ); $this->row = $row; $this->content = $content; $this->derived = $derived; } /** * Returns the Content of the given slot. * * @note This is free to load Content from whatever subsystem is necessary, * performing potentially expensive operations and triggering I/O-related * failure modes. * * @note This method does not apply audience filtering. * * @throws SuppressedDataException if access to the content is not allowed according * to the audience check performed by RevisionRecord::getSlot(). * @throws BadRevisionException if the revision is permanently missing * @throws RevisionAccessException for other storage access errors * * @return Content The slot's content. This is a direct reference to the internal instance, * copy before exposing to application logic! */ public function getContent() { if ( $this->content instanceof Content ) { return $this->content; } $obj = call_user_func( $this->content, $this ); Assert::postcondition( $obj instanceof Content, 'Slot content callback should return a Content object' ); $this->content = $obj; return $this->content; } /** * Returns the string value of a data field from the database row supplied to the constructor. * If the field was set to a callback, that callback is invoked and the result returned. * * @param string $name * * @throws OutOfBoundsException * @throws IncompleteRevisionException * @return mixed Returns the field's value, never null. */ private function getField( $name ) { if ( !isset( $this->row->$name ) ) { // distinguish between unknown and uninitialized fields if ( property_exists( $this->row, $name ) ) { throw new IncompleteRevisionException( 'Uninitialized field: {name}', [ 'name' => $name ] ); } else { throw new OutOfBoundsException( 'No such field: ' . $name ); } } $value = $this->row->$name; // NOTE: allow callbacks, but don't trust plain string callables from the database! if ( !is_string( $value ) && is_callable( $value ) ) { $value = call_user_func( $value, $this ); $this->setField( $name, $value ); } return $value; } /** * Returns the string value of a data field from the database row supplied to the constructor. * * @param string $name * * @throws OutOfBoundsException * @throws IncompleteRevisionException * @return string */ private function getStringField( $name ) { return strval( $this->getField( $name ) ); } /** * Returns the int value of a data field from the database row supplied to the constructor. * * @param string $name * * @throws OutOfBoundsException * @throws IncompleteRevisionException * @return int */ private function getIntField( $name ) { return intval( $this->getField( $name ) ); } /** * @param string $name * @return bool whether this record contains the given field */ private function hasField( $name ) { if ( isset( $this->row->$name ) ) { // if the field is a callback, resolve first, then re-check if ( !is_string( $this->row->$name ) && is_callable( $this->row->$name ) ) { $this->getField( $name ); } } return isset( $this->row->$name ); } /** * Returns the ID of the revision this slot is associated with. * * @return int */ public function getRevision() { return $this->getIntField( 'slot_revision_id' ); } /** * Returns the revision ID of the revision that originated the slot's content. * * @return int */ public function getOrigin() { return $this->getIntField( 'slot_origin' ); } /** * Whether this slot was inherited from an older revision. * * If this SlotRecord is already attached to a revision, this returns true * if the slot's revision of origin is the same as the revision it belongs to. * * If this SlotRecord is not yet attached to a revision, this returns true * if the slot already has an address. * * @return bool */ public function isInherited() { if ( $this->hasRevision() ) { return $this->getRevision() !== $this->getOrigin(); } else { return $this->hasAddress(); } } /** * Whether this slot has an address. Slots will have an address if their * content has been stored. While building a new revision, * SlotRecords will not have an address associated. * * @return bool */ public function hasAddress() { return $this->hasField( 'content_address' ); } /** * Whether this slot has an origin (revision ID that originated the slot's content. * * @since 1.32 * * @return bool */ public function hasOrigin() { return $this->hasField( 'slot_origin' ); } /** * Whether this slot has a content ID. Slots will have a content ID if their * content has been stored in the content table. While building a new revision, * SlotRecords will not have an ID associated. * * Also, during schema migration, hasContentId() may return false when encountering an * un-migrated database entry in SCHEMA_COMPAT_WRITE_BOTH mode. * It will however always return true for saved revisions on SCHEMA_COMPAT_READ_NEW mode, * or without SCHEMA_COMPAT_WRITE_NEW mode. In the latter case, an emulated content ID * is used, derived from the revision's text ID. * * Note that hasContentId() returning false while hasRevision() returns true always * indicates an unmigrated row in SCHEMA_COMPAT_WRITE_BOTH mode, as described above. * For an unsaved slot, both these methods would return false. * * @since 1.32 * * @return bool */ public function hasContentId() { return $this->hasField( 'slot_content_id' ); } /** * Whether this slot has revision ID associated. Slots will have a revision ID associated * only if they were loaded as part of an existing revision. While building a new revision, * Slotrecords will not have a revision ID associated. * * @return bool */ public function hasRevision() { return $this->hasField( 'slot_revision_id' ); } /** * Returns the role of the slot. * * @return string */ public function getRole() { return $this->getStringField( 'role_name' ); } /** * Returns the address of this slot's content. * This address can be used with BlobStore to load the Content object. * * @return string */ public function getAddress() { return $this->getStringField( 'content_address' ); } /** * Returns the ID of the content meta data row associated with the slot. * This information should be irrelevant to application logic, it is here to allow * the construction of a full row for the revision table. * * Note that this method may return an emulated value during schema migration in * SCHEMA_COMPAT_WRITE_OLD mode. See RevisionStore::emulateContentId for more information. * * @return int */ public function getContentId() { return $this->getIntField( 'slot_content_id' ); } /** * Returns the content size * * @return int size of the content, in bogo-bytes, as reported by Content::getSize. */ public function getSize() { try { $size = $this->getIntField( 'content_size' ); } catch ( IncompleteRevisionException $ex ) { $size = $this->getContent()->getSize(); $this->setField( 'content_size', $size ); } return $size; } /** * Returns the content size * * @return string hash of the content. */ public function getSha1() { try { $sha1 = $this->getStringField( 'content_sha1' ); } catch ( IncompleteRevisionException $ex ) { $sha1 = null; } // Compute if missing. Missing could mean null or empty. if ( $sha1 === null || $sha1 === '' ) { $format = $this->hasField( 'format_name' ) ? $this->getStringField( 'format_name' ) : null; $data = $this->getContent()->serialize( $format ); $sha1 = self::base36Sha1( $data ); $this->setField( 'content_sha1', $sha1 ); } return $sha1; } /** * Returns the content model. This is the model name that decides * which ContentHandler is appropriate for interpreting the * data of the blob referenced by the address returned by getAddress(). * * @return string the content model of the content */ public function getModel() { try { $model = $this->getStringField( 'model_name' ); } catch ( IncompleteRevisionException $ex ) { $model = $this->getContent()->getModel(); $this->setField( 'model_name', $model ); } return $model; } /** * Returns the blob serialization format as a MIME type. * * @note When this method returns null, the caller is expected * to auto-detect the serialization format, or to rely on * the default format associated with the content model. * * @return string|null */ public function getFormat() { // XXX: we currently do not plan to store the format for each slot! if ( $this->hasField( 'format_name' ) ) { return $this->getStringField( 'format_name' ); } return null; } /** * @param string $name * @param string|int|null $value */ private function setField( $name, $value ) { $this->row->$name = $value; } /** * Get the base 36 SHA-1 value for a string of text * * MCR migration note: this replaced Revision::base36Sha1 * * @param string $blob * @return string */ public static function base36Sha1( $blob ) { return \Wikimedia\base_convert( sha1( $blob ), 16, 36, 31 ); } /** * Returns true if $other has the same content as this slot. * The check is performed based on the model, address size, and hash. * Two slots can have the same content if they use different content addresses, * but if they have the same address and the same model, they have the same content. * Two slots can have the same content if they belong to different * revisions or pages. * * Note that hasSameContent() may return false even if Content::equals returns true for * the content of two slots. This may happen if the two slots have different serializations * representing equivalent Content. Such false negatives are considered acceptable. Code * that has to be absolutely sure the Content is really not the same if hasSameContent() * returns false should call getContent() and compare the Content objects directly. * * @since 1.32 * * @param SlotRecord $other * @return bool */ public function hasSameContent( SlotRecord $other ) { if ( $other === $this ) { return true; } if ( $this->getModel() !== $other->getModel() ) { return false; } if ( $this->hasAddress() && $other->hasAddress() && $this->getAddress() == $other->getAddress() ) { return true; } if ( $this->getSize() !== $other->getSize() ) { return false; } if ( $this->getSha1() !== $other->getSha1() ) { return false; } return true; } /** * @return bool Is this a derived slot? * @since 1.36 */ public function isDerived(): bool { return $this->derived; } } PK ! � �v� � ContributionsSegment.phpnu �Iw�� <?php namespace MediaWiki\Revision; use MediaWiki\Message\Message; /** * @newable * @since 1.35 */ class ContributionsSegment { /** * @var RevisionRecord[] */ private $revisions; /** * @var string[][] */ private $tags; /** * @var string|null */ private $before; /** * @var string|null */ private $after; /** * @var array */ private $deltas; /** * @var array */ private $flags; /** * @param RevisionRecord[] $revisions * @param string[][] $tags Associative array mapping revision IDs to a map of tag names to Message objects * @param string|null $before * @param string|null $after * @param int[] $deltas An associative array mapping a revision Id to the difference in size of this revision * and its parent revision. Values may be null if the size difference is unknown. * @param array $flags Is an associative array, known fields are: * - newest: bool indicating whether this segment is the newest in time * - oldest: bool indicating whether this segment is the oldest in time */ public function __construct( array $revisions, array $tags, ?string $before, ?string $after, array $deltas = [], array $flags = [] ) { $this->revisions = $revisions; $this->tags = $tags; $this->before = $before; $this->after = $after; $this->deltas = $deltas; $this->flags = $flags; } /** * Get tags and associated metadata for a given revision * * @param int $revId a revision ID * * @return Message[] Associative array mapping tag name to a Message object storing tag display data */ public function getTagsForRevision( $revId ): array { return $this->tags[$revId] ?? []; } /** * @return RevisionRecord[] */ public function getRevisions(): array { return $this->revisions; } /** * @return string|null */ public function getBefore(): ?string { return $this->before; } /** * @return string|null */ public function getAfter(): ?string { return $this->after; } /** * Returns the difference in size of the given revision and its parent revision. * Returns null if the size difference is unknown. * @param int $revid Revision id * @return int|null */ public function getDeltaForRevision( int $revid ): ?int { return $this->deltas[$revid] ?? null; } /** * The value of the 'newest' field of the flags passed to the constructor, or false * if that field was not set. * * @return bool */ public function isNewest(): bool { return $this->flags['newest'] ?? false; } /** * The value of the 'oldest' field of the flags passed to the constructor, or false * if that field was not set. * * @return bool */ public function isOldest(): bool { return $this->flags['oldest'] ?? false; } } PK ! r�D� � SlotRoleRegistry.phpnu �Iw�� <?php /** * This file is part of MediaWiki. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Revision; use InvalidArgumentException; use LogicException; use MediaWiki\Page\PageIdentity; use MediaWiki\Storage\NameTableStore; use Wikimedia\Assert\Assert; /** * A registry service for SlotRoleHandlers, used to define which slot roles are available on * which page. * * Extensions may use the SlotRoleRegistry to register the slots they define. * * In the context of the SlotRoleRegistry, it is useful to distinguish between "defined" and "known" * slot roles: A slot role is "defined" if defineRole() or defineRoleWithModel() was called for * that role. A slot role is "known" if the NameTableStore provided to the constructor as the * $roleNamesStore parameter has an ID associated with that role, which essentially means that * the role at some point has been used on the wiki. Roles that are not "defined" but are * "known" typically belong to extensions that used to be installed on the wiki, but no longer are. * Such slots should be considered ok for display and administrative operations, but only "defined" * slots should be supported for editing. * * @since 1.33 */ class SlotRoleRegistry { private NameTableStore $roleNamesStore; /** @var array<string,callable> */ private array $instantiators = []; /** @var array<string,SlotRoleHandler> */ private array $handlers = []; public function __construct( NameTableStore $roleNamesStore ) { $this->roleNamesStore = $roleNamesStore; } /** * Defines a slot role. * * For use by extensions that wish to define roles beyond the main slot role. * * @see defineRoleWithModel() * * @param string $role The role name of the slot to define. This should follow the * same convention as message keys: * @param callable $instantiator called with $role as a parameter; * Signature: function ( string $role ): SlotRoleHandler */ public function defineRole( string $role, callable $instantiator ): void { $role = strtolower( $role ); if ( isset( $this->instantiators[$role] ) ) { throw new LogicException( "Role $role is already defined" ); } $this->instantiators[$role] = $instantiator; } /** * Defines a slot role that allows only the given content model, and has no special * behavior. * * For use by extensions that wish to define roles beyond the main slot role, but have * no need to implement any special behavior for that slot. * * @see defineRole() * * @param string $role The role name of the slot to define, see defineRole() * for more information. * @param string $model A content model name, see ContentHandler * @param array $layout See SlotRoleHandler getOutputLayoutHints * @param bool $derived see SlotRoleHandler constructor * @since 1.36 optional $derived parameter added */ public function defineRoleWithModel( string $role, string $model, array $layout = [], bool $derived = false ): void { $this->defineRole( $role, static function ( $role ) use ( $model, $layout, $derived ) { return new SlotRoleHandler( $role, $model, $layout, $derived ); } ); } /** * Gets the SlotRoleHandler that should be used when processing content of the given role. * * @param string $role * * @throws InvalidArgumentException If $role is not a known slot role. * @return SlotRoleHandler The handler to be used for $role. This may be a * FallbackSlotRoleHandler if the slot is "known" but not "defined". */ public function getRoleHandler( string $role ): SlotRoleHandler { $role = strtolower( $role ); if ( !isset( $this->handlers[$role] ) ) { if ( !isset( $this->instantiators[$role] ) ) { if ( $this->isKnownRole( $role ) ) { // The role has no handler defined, but is represented in the database. // This may happen e.g. when the extension that defined the role was uninstalled. wfWarn( __METHOD__ . ": known but undefined slot role $role" ); $this->handlers[$role] = new FallbackSlotRoleHandler( $role ); } else { // The role doesn't have a handler defined, and is not represented in // the database. Something must be quite wrong. throw new InvalidArgumentException( "Unknown role $role" ); } } else { $handler = call_user_func( $this->instantiators[$role], $role ); Assert::postcondition( $handler instanceof SlotRoleHandler, "Instantiator for $role role must return a SlotRoleHandler" ); $this->handlers[$role] = $handler; } } return $this->handlers[$role]; } /** * Returns the list of roles allowed when creating a new revision on the given page. * The choice should not depend on external state, such as the page content. * Note that existing revisions of that page are not guaranteed to comply with this list. * * All implementations of this method are required to return at least all "required" roles. * * @param PageIdentity $page * * @return string[] */ public function getAllowedRoles( PageIdentity $page ): array { // TODO: allow this to be overwritten per namespace (or page type) // TODO: decide how to control which slots are offered for editing by default (T209927) return $this->getDefinedRoles(); } /** * Returns the list of roles required when creating a new revision on the given page. * The should not depend on external state, such as the page content. * Note that existing revisions of that page are not guaranteed to comply with this list. * * All required roles are implicitly considered "allowed", so any roles * returned by this method will also be returned by getAllowedRoles(). * * @param PageIdentity $page * * @return string[] */ public function getRequiredRoles( PageIdentity $page ): array { // TODO: allow this to be overwritten per namespace (or page type) return [ SlotRecord::MAIN ]; } /** * Returns the list of roles defined by calling defineRole(). * * This list should be used when enumerating slot roles that can be used for editing. * * @return string[] */ public function getDefinedRoles(): array { return array_keys( $this->instantiators ); } /** * Returns the list of known roles, including the ones returned by getDefinedRoles(), * and roles that exist according to the NameTableStore provided to the constructor. * * This list should be used when enumerating slot roles that can be used in queries or * for display. * * @return string[] */ public function getKnownRoles(): array { return array_unique( array_merge( $this->getDefinedRoles(), $this->roleNamesStore->getMap() ) ); } /** * Whether the given role is defined, that is, it was defined by calling defineRole(). */ public function isDefinedRole( string $role ): bool { $role = strtolower( $role ); return isset( $this->instantiators[$role] ); } /** * Whether the given role is known, that is, it's either defined or exist according to * the NameTableStore provided to the constructor. */ public function isKnownRole( string $role ): bool { $role = strtolower( $role ); return in_array( $role, $this->getKnownRoles(), true ); } } PK ! �ʹE� � RevisionAccessException.phpnu �Iw�� <?php /** * Exception representing a failure to look up a revision. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Revision; use RuntimeException; use Throwable; use Wikimedia\NormalizedException\INormalizedException; use Wikimedia\NormalizedException\NormalizedExceptionTrait; /** * Exception representing a failure to look up a revision. * * @newable * @since 1.31 * @since 1.32 Renamed from MediaWiki\Storage\RevisionAccessException */ class RevisionAccessException extends RuntimeException implements INormalizedException { use NormalizedExceptionTrait; /** * @stable to call * @param string $normalizedMessage The exception message, with PSR-3 style placeholders. * @param array $messageContext Message context, with values for the placeholders. * @param int $code The exception code. * @param Throwable|null $previous The previous throwable used for the exception chaining. */ public function __construct( string $normalizedMessage = '', array $messageContext = [], int $code = 0, ?Throwable $previous = null ) { $this->normalizedMessage = $normalizedMessage; $this->messageContext = $messageContext; parent::__construct( self::getMessageFromNormalizedMessage( $normalizedMessage, $messageContext ), $code, $previous ); } } PK ! ���� IncompleteRevisionException.phpnu �Iw�� <?php /** * Exception representing a failure to look up a revision. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Revision; /** * Exception throw when trying to access undefined fields on an incomplete RevisionRecord. * * @newable * @since 1.31 * @since 1.32 Renamed from MediaWiki\Storage\IncompleteRevisionException */ class IncompleteRevisionException extends RevisionAccessException { } PK ! ���g� � RevisionStoreRecord.phpnu �Iw�� <?php /** * A RevisionRecord representing an existing revision persisted in the revision table. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Revision; use InvalidArgumentException; use MediaWiki\CommentStore\CommentStoreComment; use MediaWiki\Page\PageIdentity; use MediaWiki\Permissions\Authority; use MediaWiki\User\UserIdentity; use MediaWiki\Utils\MWTimestamp; /** * A RevisionRecord representing an existing revision persisted in the revision table. * RevisionStoreRecord has no optional fields, getters will never return null. * * @since 1.31 * @since 1.32 Renamed from MediaWiki\Storage\RevisionStoreRecord */ class RevisionStoreRecord extends RevisionRecord { /** @var bool */ protected $mCurrent = false; /** * @note Avoid calling this constructor directly. Use the appropriate methods * in RevisionStore instead. * * @param PageIdentity $page The page this RevisionRecord is associated with. * @param UserIdentity $user * @param CommentStoreComment $comment * @param \stdClass $row A row from the revision table. Use RevisionStore::getQueryInfo() to build * a query that yields the required fields. * @param RevisionSlots $slots The slots of this revision. * @param false|string $wikiId Relevant wiki id or self::LOCAL for the current one. */ public function __construct( PageIdentity $page, UserIdentity $user, CommentStoreComment $comment, \stdClass $row, RevisionSlots $slots, $wikiId = self::LOCAL ) { parent::__construct( $page, $slots, $wikiId ); $this->mId = intval( $row->rev_id ); $this->mPageId = intval( $row->rev_page ); $this->mComment = $comment; // Don't use MWTimestamp::convert, instead let any detailed exception from MWTimestamp // bubble up (T254210) $timestamp = ( new MWTimestamp( $row->rev_timestamp ) )->getTimestamp( TS_MW ); $this->mUser = $user; $this->mMinorEdit = (bool)$row->rev_minor_edit; $this->mTimestamp = $timestamp; $this->mDeleted = intval( $row->rev_deleted ); // NOTE: rev_parent_id = 0 indicates that there is no parent revision, while null // indicates that the parent revision is unknown. As per MW 1.31, the database schema // allows rev_parent_id to be NULL. $this->mParentId = isset( $row->rev_parent_id ) ? intval( $row->rev_parent_id ) : null; $this->mSize = isset( $row->rev_len ) ? intval( $row->rev_len ) : null; $this->mSha1 = !empty( $row->rev_sha1 ) ? $row->rev_sha1 : null; // NOTE: we must not call $this->mTitle->getLatestRevID() here, since the state of // page_latest may be in limbo during revision creation. In that case, calling // $this->mTitle->getLatestRevID() would cause a bad value to be cached in the Title // object. During page creation, that bad value would be 0. if ( isset( $row->page_latest ) ) { $this->mCurrent = ( $row->rev_id == $row->page_latest ); } $pageIdBasedOnPage = $this->getArticleId( $this->mPage ); if ( $this->mPageId && $pageIdBasedOnPage && $this->mPageId !== $pageIdBasedOnPage ) { throw new InvalidArgumentException( 'The given page (' . $this->mPage . ')' . ' does not belong to page ID ' . $this->mPageId . ' but actually belongs to ' . $this->getArticleId( $this->mPage ) ); } } /** * @inheritDoc */ public function isCurrent() { return $this->mCurrent; } /** * MCR migration note: this replaced Revision::isDeleted * * @param int $field One of DELETED_* bitfield constants * * @return bool */ public function isDeleted( $field ) { if ( $this->isCurrent() && $field === self::DELETED_TEXT ) { // Current revisions of pages cannot have the content hidden. Skipping this // check is very useful for Parser as it fetches templates using newKnownCurrent(). // Calling getVisibility() in that case triggers a verification database query. return false; // no need to check } return parent::isDeleted( $field ); } public function userCan( $field, Authority $performer ) { if ( $this->isCurrent() && $field === self::DELETED_TEXT ) { // Current revisions of pages cannot have the content hidden. Skipping this // check is very useful for Parser as it fetches templates using newKnownCurrent(). // Calling getVisibility() in that case triggers a verification database query. return true; // no need to check } return parent::userCan( $field, $performer ); } /** * @param string|false $wikiId The wiki ID expected by the caller. * @return int|null The revision id, never null. */ public function getId( $wikiId = self::LOCAL ) { // overwritten just to add a guarantee to the contract return parent::getId( $wikiId ); } /** * @throws RevisionAccessException if the size was unknown and could not be calculated. * @return int The nominal revision size, never null. May be computed on the fly. */ public function getSize() { // If length is null, calculate and remember it (potentially SLOW!). // This is for compatibility with old database rows that don't have the field set. $this->mSize ??= $this->mSlots->computeSize(); return $this->mSize; } /** * @throws RevisionAccessException if the hash was unknown and could not be calculated. * @return string The revision hash, never null. May be computed on the fly. */ public function getSha1() { // If hash is null, calculate it and remember (potentially SLOW!) // This is for compatibility with old database rows that don't have the field set. $this->mSha1 ??= $this->mSlots->computeSha1(); return $this->mSha1; } /** * @param int $audience * @param Authority|null $performer * * @return UserIdentity The identity of the revision author, null if access is forbidden. */ public function getUser( $audience = self::FOR_PUBLIC, ?Authority $performer = null ) { // overwritten just to add a guarantee to the contract return parent::getUser( $audience, $performer ); } /** * @param int $audience * @param Authority|null $performer * * @return CommentStoreComment The revision comment, null if access is forbidden. */ public function getComment( $audience = self::FOR_PUBLIC, ?Authority $performer = null ) { // overwritten just to add a guarantee to the contract return parent::getComment( $audience, $performer ); } /** * @return string timestamp, never null */ public function getTimestamp() { // overwritten just to add a guarantee to the contract return parent::getTimestamp(); } /** * @see RevisionStore::isComplete * * @return bool always true. */ public function isReadyForInsertion() { return true; } } PK ! ��T T SlotRoleHandler.phpnu �Iw�� <?php /** * This file is part of MediaWiki. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Revision; use MediaWiki\Linker\LinkTarget; use MediaWiki\Page\PageIdentity; /** * SlotRoleHandler instances are used to declare the existence and behavior of slot roles. * Most importantly, they control which content model can be used for the slot, and how it is * represented in the rendered version of page content. * * @stable to extend * * @since 1.33 */ class SlotRoleHandler { /** * @var string */ private $role; /** * @var string[] * @see getOutputLayoutHints */ private $layout = [ 'display' => 'section', // use 'none' to suppress 'region' => 'center', 'placement' => 'append' ]; /** * @var bool */ private $derived; /** * @var string */ private $contentModel; /** * @stable to call * * @param string $role The name of the slot role defined by this SlotRoleHandler. See * SlotRoleRegistry::defineRole for more information. * @param string $contentModel The default content model for this slot. As per the default * implementation of isAllowedModel(), also the only content model allowed for the * slot. Subclasses may however handle default and allowed models differently. * @param string[] $layout Layout hints, for use by RevisionRenderer. See getOutputLayoutHints. * @param bool $derived Is this handler for a derived slot? Derived slots allow information that * is derived from the content of a page to be stored even if it is generated * asynchronously or updated later. Their size is not included in the revision size, * their hash does not contribute to the revision hash, and updates are not included * in revision history. * @since 1.36 optional $derived parameter added */ public function __construct( $role, $contentModel, $layout = [], bool $derived = false ) { $this->role = $role; $this->contentModel = $contentModel; $this->layout = array_merge( $this->layout, $layout ); $this->derived = $derived; } /** * @return string The role this SlotRoleHandler applies to */ public function getRole() { return $this->role; } /** * Layout hints for use while laying out the combined output of all slots, typically by * RevisionRenderer. The layout hints are given as an associative array. Well-known keys * to use: * * * "display": how the output of this slot should be represented. Supported values: * - "section": show as a top level section of the region. * - "none": do not show at all * Further values that may be supported in the future include "box" and "banner". * * "region": in which region of the page the output should be placed. Supported values: * - "center": the central content area. * Further values that may be supported in the future include "top" and "bottom", "left" * and "right", "header" and "footer". * * "placement": placement relative to other content of the same area. * - "append": place at the end, after any output processed previously. * Further values that may be supported in the future include "prepend". A "weight" key * may be introduced for more fine grained control. * * @stable to override * @return string[] an associative array of hints */ public function getOutputLayoutHints() { return $this->layout; } /** * @return bool Is this a handler for a derived slot? * @since 1.36 */ public function isDerived(): bool { return $this->derived; } /** * The message key for the translation of the slot name. * * @stable to override * @return string */ public function getNameMessageKey() { return 'slot-name-' . $this->role; } /** * Determines the content model to use by default for this slot on the given page. * * The default implementation always returns the content model provided to the constructor. * Subclasses may base the choice on default model on the page title or namespace. * The choice should not depend on external state, such as the page content. * * @stable to override * * @param LinkTarget|PageIdentity $page * * @return string */ public function getDefaultModel( $page ) { return $this->contentModel; } /** * Determines whether the given model can be used on this slot on the given page. * * The default implementation checks whether $model is the content model provided to the * constructor. Subclasses may allow other models and may base the decision on the page title * or namespace. The choice should not depend on external state, such as the page content. * * @stable to override * * @note This should be checked when creating new revisions. Existing revisions * are not guaranteed to comply with the return value. * * @param string $model * @param PageIdentity $page * * @return bool */ public function isAllowedModel( $model, PageIdentity $page ) { return ( $model === $this->contentModel ); } /** * Whether this slot should be considered when determining whether a page should be counted * as an "article" in the site statistics. * * For a page to be considered countable, one of the page's slots must return true from this * method, and Content::isCountable() must return true for the content of that slot. * * The default implementation always returns false. * * @stable to override * * @return bool */ public function supportsArticleCount() { return false; } } PK ! ^�� � FallbackSlotRoleHandler.phpnu �Iw�� <?php /** * This file is part of MediaWiki. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Revision; use MediaWiki\Linker\LinkTarget; use MediaWiki\Page\PageIdentity; /** * A SlotRoleHandler for providing basic functionality for undefined slot roles. * * This class is intended to be used when encountering slots with a role that used to be defined * by an extension, but no longer is backed by hany specific handler, since the extension in * question has been uninstalled. It may also be used for pages imported from another wiki. * * @since 1.33 */ class FallbackSlotRoleHandler extends SlotRoleHandler { public function __construct( $role ) { parent::__construct( $role, CONTENT_MODEL_UNKNOWN ); } /** * @param LinkTarget $page * * @return bool Always false, to prevent undefined slots from being used in new revisions. */ public function isAllowedOn( LinkTarget $page ) { return false; } /** * @param string $model * @param PageIdentity $page * * @return bool Always false, to prevent undefined slots from being used for * arbitrary content. */ public function isAllowedModel( $model, PageIdentity $page ) { return false; } public function getOutputLayoutHints() { // TODO: should we return [ 'display' => 'none'] here, causing undefined slots // to be hidden? We'd still need some place to surface the content of such // slots, see T209923. return parent::getOutputLayoutHints(); } } PK ! {��� * Hook/ContentHandlerDefaultModelForHook.phpnu �Iw�� <?php namespace MediaWiki\Revision\Hook; use MediaWiki\Title\Title; /** * This is a hook handler interface, see docs/Hooks.md. * Use the hook name "ContentHandlerDefaultModelFor" to register handlers implementing this interface. * * @stable to implement * @ingroup Hooks */ interface ContentHandlerDefaultModelForHook { /** * This hook is called when the default content model is determined for a * given title. Use this hook to assign a different model for that title. * * @since 1.35 * * @param Title $title Title in question * @param string &$model Model name. Use with CONTENT_MODEL_XXX constants. * @return bool|void True or no return value to continue or false to abort */ public function onContentHandlerDefaultModelFor( $title, &$model ); } PK ! <7e<� � # Hook/RevisionRecordInsertedHook.phpnu �Iw�� <?php namespace MediaWiki\Revision\Hook; use MediaWiki\Revision\RevisionRecord; /** * This is a hook handler interface, see docs/Hooks.md. * Use the hook name "RevisionRecordInserted" to register handlers implementing this interface. * * @stable to implement * @ingroup Hooks */ interface RevisionRecordInsertedHook { /** * This hook is called after a revision is inserted into the database. * * @since 1.35 * * @param RevisionRecord $revisionRecord RevisionRecord that has just been inserted * @return bool|void True or no return value to continue or false to abort */ public function onRevisionRecordInserted( $revisionRecord ); } PK ! ?��5�* �* MutableRevisionRecord.phpnu �Iw�� <?php /** * Mutable RevisionRecord implementation, for building new revision entries programmatically. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Revision; use InvalidArgumentException; use MediaWiki\CommentStore\CommentStoreComment; use MediaWiki\Content\Content; use MediaWiki\Page\PageIdentity; use MediaWiki\Storage\RevisionSlotsUpdate; use MediaWiki\User\UserIdentity; use MediaWiki\Utils\MWTimestamp; /** * Mutable RevisionRecord implementation, for building new revision entries programmatically. * Provides setters for all fields. * * @newable * * @since 1.31 * @since 1.32 Renamed from MediaWiki\Storage\MutableRevisionRecord * @property MutableRevisionSlots $mSlots */ class MutableRevisionRecord extends RevisionRecord { /** * Returns an incomplete MutableRevisionRecord which uses $parent as its * parent revision, and inherits all slots form it. If saved unchanged, * the new revision will act as a null-revision. * * @param RevisionRecord $parent * * @return MutableRevisionRecord */ public static function newFromParentRevision( RevisionRecord $parent ) { $rev = new MutableRevisionRecord( $parent->getPage(), $parent->getWikiId() ); foreach ( $parent->getSlotRoles() as $role ) { $slot = $parent->getSlot( $role, self::RAW ); $rev->inheritSlot( $slot ); } $rev->setPageId( $parent->getPageId() ); $rev->setParentId( $parent->getId() ); return $rev; } /** * Returns a MutableRevisionRecord which is an updated version of $revision with $slots * added. * @param RevisionRecord $revision * @param SlotRecord[] $slots * @return MutableRevisionRecord * @since 1.36 */ public static function newUpdatedRevisionRecord( RevisionRecord $revision, array $slots ): MutableRevisionRecord { $newRevisionRecord = new MutableRevisionRecord( $revision->getPage(), $revision->getWikiId() ); $newRevisionRecord->setId( $revision->getId( $revision->getWikiId() ) ); $newRevisionRecord->setPageId( $revision->getPageId( $revision->getWikiId() ) ); $newRevisionRecord->setParentId( $revision->getParentId( $revision->getWikiId() ) ); $newRevisionRecord->setUser( $revision->getUser() ); foreach ( $revision->getSlots()->getSlots() as $slot ) { $newRevisionRecord->setSlot( $slot ); } foreach ( $slots as $slot ) { $newRevisionRecord->setSlot( $slot ); } return $newRevisionRecord; } /** * @stable to call. * * @param PageIdentity $page The page this RevisionRecord is associated with. * @param false|string $wikiId Relevant wiki id or self::LOCAL for the current one. */ public function __construct( PageIdentity $page, $wikiId = self::LOCAL ) { $slots = new MutableRevisionSlots( [], function () { $this->resetAggregateValues(); } ); parent::__construct( $page, $slots, $wikiId ); } /** * @param int $parentId * @return self */ public function setParentId( int $parentId ) { $this->mParentId = $parentId; return $this; } /** * Sets the given slot. If a slot with the same role is already present in the revision, * it is replaced. * * @note This can only be used with a fresh "unattached" SlotRecord. Calling code that has a * SlotRecord from another revision should use inheritSlot(). Calling code that has access to * a Content object can use setContent(). * * @note This may cause the slot meta-data for the revision to be lazy-loaded. * * @note Calling this method will cause the revision size and hash to be re-calculated upon * the next call to getSize() and getSha1(), respectively. * * @param SlotRecord $slot * @return self */ public function setSlot( SlotRecord $slot ) { if ( $slot->hasRevision() && $slot->getRevision() !== $this->getId() ) { throw new InvalidArgumentException( 'The given slot must be an unsaved, unattached one. ' . 'This slot is already attached to revision ' . $slot->getRevision() . '. ' . 'Use inheritSlot() instead to preserve a slot from a previous revision.' ); } $this->mSlots->setSlot( $slot ); return $this; } /** * "Inherits" the given slot's content. * * If a slot with the same role is already present in the revision, it is replaced. * * @note This may cause the slot meta-data for the revision to be lazy-loaded. * * @param SlotRecord $parentSlot * @return self */ public function inheritSlot( SlotRecord $parentSlot ) { $this->mSlots->inheritSlot( $parentSlot ); return $this; } /** * Sets the content for the slot with the given role. * * If a slot with the same role is already present in the revision, it is replaced. * Calling code that has access to a SlotRecord can use inheritSlot() instead. * * @note This may cause the slot meta-data for the revision to be lazy-loaded. * * @note Calling this method will cause the revision size and hash to be re-calculated upon * the next call to getSize() and getSha1(), respectively. * * @param string $role * @param Content $content * @return self */ public function setContent( $role, Content $content ) { $this->mSlots->setContent( $role, $content ); return $this; } /** * Removes the slot with the given role from this revision. * This effectively ends the "stream" with that role on the revision's page. * Future revisions will no longer inherit this slot, unless it is added back explicitly. * * @note This may cause the slot meta-data for the revision to be lazy-loaded. * * @note Calling this method will cause the revision size and hash to be re-calculated upon * the next call to getSize() and getSha1(), respectively. * * @param string $role * @return self */ public function removeSlot( $role ) { $this->mSlots->removeSlot( $role ); return $this; } /** * Applies the given update to the slots of this revision. * * @param RevisionSlotsUpdate $update * @return self */ public function applyUpdate( RevisionSlotsUpdate $update ) { $update->apply( $this->mSlots ); return $this; } /** * @param CommentStoreComment $comment * @return self */ public function setComment( CommentStoreComment $comment ) { $this->mComment = $comment; return $this; } /** * Set revision hash, for optimization. Prevents getSha1() from re-calculating the hash. * * @note This should only be used if the calling code is sure that the given hash is correct * for the revision's content, and there is no chance of the content being manipulated * later. When in doubt, this method should not be called. * * @param string $sha1 SHA1 hash as a base36 string. * @return self */ public function setSha1( string $sha1 ) { $this->mSha1 = $sha1; return $this; } /** * Set nominal revision size, for optimization. Prevents getSize() from re-calculating the size. * * @note This should only be used if the calling code is sure that the given size is correct * for the revision's content, and there is no chance of the content being manipulated * later. When in doubt, this method should not be called. * * @param int $size nominal size in bogo-bytes * @return self */ public function setSize( int $size ) { $this->mSize = $size; return $this; } /** * @param int $visibility * @return self */ public function setVisibility( int $visibility ) { $this->mDeleted = $visibility; return $this; } /** * @param string $timestamp A timestamp understood by MWTimestamp * @return self */ public function setTimestamp( string $timestamp ) { $this->mTimestamp = MWTimestamp::convert( TS_MW, $timestamp ); return $this; } /** * @param bool $minorEdit * @return self */ public function setMinorEdit( bool $minorEdit ) { $this->mMinorEdit = $minorEdit; return $this; } /** * Set the revision ID. * * MCR migration note: this replaced Revision::setId * * @warning Use this with care, especially when preparing a revision for insertion * into the database! The revision ID should only be fixed in special cases * like preserving the original ID when restoring a revision. * * @param int $id * @return self */ public function setId( int $id ) { $this->mId = $id; return $this; } /** * Sets the user identity associated with the revision * * @param UserIdentity $user * @return self */ public function setUser( UserIdentity $user ) { $this->mUser = $user; return $this; } /** * @param int $pageId * @return self */ public function setPageId( int $pageId ) { $pageIdBasedOnPage = $this->getArticleId( $this->mPage ); if ( $pageIdBasedOnPage && $pageIdBasedOnPage !== $this->getArticleId( $this->mPage ) ) { throw new InvalidArgumentException( 'The given page does not belong to page ID ' . $this->mPageId ); } $this->mPageId = $pageId; return $this; } /** * Returns the nominal size of this revision. * * MCR migration note: this replaced Revision::getSize * * @return int The nominal size, may be computed on the fly if not yet known. */ public function getSize() { // If not known, re-calculate and remember. Will be reset when slots change. $this->mSize ??= $this->mSlots->computeSize(); return $this->mSize; } /** * Returns the base36 sha1 of this revision. * * MCR migration note: this replaced Revision::getSha1 * * @return string The revision hash, may be computed on the fly if not yet known. */ public function getSha1() { // If not known, re-calculate and remember. Will be reset when slots change. $this->mSha1 ??= $this->mSlots->computeSha1(); return $this->mSha1; } /** * Returns the slots defined for this revision as a MutableRevisionSlots instance, * which can be modified to defined the slots for this revision. * * @return MutableRevisionSlots */ public function getSlots(): MutableRevisionSlots { // Overwritten just to guarantee the more narrow return type. // @phan-suppress-next-line PhanTypeMismatchReturnSuperType return parent::getSlots(); } /** * Invalidate cached aggregate values such as hash and size. * Used as a callback by MutableRevisionSlots. */ private function resetAggregateValues() { $this->mSize = null; $this->mSha1 = null; } } PK ! {��)b b MainSlotRoleHandler.phpnu �Iw�� <?php /** * This file is part of MediaWiki. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Revision; use MediaWiki\Content\IContentHandlerFactory; use MediaWiki\HookContainer\HookContainer; use MediaWiki\HookContainer\HookRunner; use MediaWiki\Linker\LinkTarget; use MediaWiki\Page\PageIdentity; use MediaWiki\Title\TitleFactory; use MWUnknownContentModelException; /** * A SlotRoleHandler for the main slot. While most slot roles serve a specific purpose and * thus typically exhibit the same behaviour on all pages, the main slot is used for different * things in different pages, typically depending on the namespace, a "file extension" in * the page name, or the content model of the slot's content. * * MainSlotRoleHandler implements some of the per-namespace and per-model behavior that was * supported prior to MediaWiki Version 1.33. * * @since 1.33 */ class MainSlotRoleHandler extends SlotRoleHandler { /** * @var string[] A mapping of namespaces to content models. * @see $wgNamespaceContentModels */ private $namespaceContentModels; /** @var IContentHandlerFactory */ private $contentHandlerFactory; /** @var HookRunner */ private $hookRunner; /** @var TitleFactory */ private $titleFactory; /** * @param string[] $namespaceContentModels A mapping of namespaces to content models, * typically from $wgNamespaceContentModels. * @param IContentHandlerFactory $contentHandlerFactory * @param HookContainer $hookContainer * @param TitleFactory $titleFactory */ public function __construct( array $namespaceContentModels, IContentHandlerFactory $contentHandlerFactory, HookContainer $hookContainer, TitleFactory $titleFactory ) { parent::__construct( SlotRecord::MAIN, CONTENT_MODEL_WIKITEXT ); $this->namespaceContentModels = $namespaceContentModels; $this->contentHandlerFactory = $contentHandlerFactory; $this->hookRunner = new HookRunner( $hookContainer ); $this->titleFactory = $titleFactory; } public function supportsArticleCount() { return true; } /** * @param string $model * @param PageIdentity $page * * @return bool * @throws MWUnknownContentModelException */ public function isAllowedModel( $model, PageIdentity $page ) { $title = $this->titleFactory->newFromPageIdentity( $page ); $handler = $this->contentHandlerFactory->getContentHandler( $model ); return $handler->canBeUsedOn( $title ); } /** * @param LinkTarget|PageIdentity $page * * @return string */ public function getDefaultModel( $page ) { // NOTE: this method must not rely on $title->getContentModel() directly or indirectly, // because it is used to initialize the mContentModel member. $ext = ''; $ns = $page->getNamespace(); $model = $this->namespaceContentModels[$ns] ?? null; // Hook can determine default model if ( $page instanceof PageIdentity ) { $title = $this->titleFactory->newFromPageIdentity( $page ); } else { $title = $this->titleFactory->newFromLinkTarget( $page ); } // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args if ( !$this->hookRunner->onContentHandlerDefaultModelFor( $title, $model ) && $model !== null ) { return $model; } // Could this page contain code based on the title? $isCodePage = $ns === NS_MEDIAWIKI && preg_match( '!\.(css|js|json)$!u', $title->getText(), $m ); if ( $isCodePage ) { $ext = $m[1]; } // Is this a user subpage containing code? $isCodeSubpage = $ns === NS_USER && !$isCodePage && preg_match( "/\\/.*\\.(js|css|json)$/", $title->getText(), $m ); if ( $isCodeSubpage ) { $ext = $m[1]; } // Is this wikitext, according to $wgNamespaceContentModels or the DefaultModelFor hook? $isWikitext = $model === null || $model == CONTENT_MODEL_WIKITEXT; $isWikitext = $isWikitext && !$isCodePage && !$isCodeSubpage; if ( !$isWikitext ) { switch ( $ext ) { case 'js': return CONTENT_MODEL_JAVASCRIPT; case 'css': return CONTENT_MODEL_CSS; case 'json': return CONTENT_MODEL_JSON; default: return $model ?? CONTENT_MODEL_TEXT; } } // We established that it must be wikitext return CONTENT_MODEL_WIKITEXT; } } PK ! ��j ArchivedRevisionLookup.phpnu �Iw�� <?php /** * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Revision; use MediaWiki\MediaWikiServices; use MediaWiki\Page\PageIdentity; use Wikimedia\Rdbms\IConnectionProvider; use Wikimedia\Rdbms\IResultWrapper; use Wikimedia\Rdbms\SelectQueryBuilder; /** * @since 1.38 */ class ArchivedRevisionLookup { /** @var IConnectionProvider */ private $dbProvider; /** @var RevisionStore */ private $revisionStore; /** * @param IConnectionProvider $dbProvider * @param RevisionStore $revisionStore */ public function __construct( IConnectionProvider $dbProvider, RevisionStore $revisionStore ) { $this->dbProvider = $dbProvider; $this->revisionStore = $revisionStore; } /** * List the revisions of the given page. Returns result wrapper with * various archive table fields. * * @param PageIdentity $page * @param array $extraConds Extra conditions to be added to the query * @param ?int $limit The limit to be applied to the query, or null for no limit * @return IResultWrapper */ public function listRevisions( PageIdentity $page, array $extraConds = [], ?int $limit = null ) { $queryBuilder = $this->revisionStore->newArchiveSelectQueryBuilder( $this->dbProvider->getReplicaDatabase() ) ->joinComment() ->where( $extraConds ) ->andWhere( [ 'ar_namespace' => $page->getNamespace(), 'ar_title' => $page->getDBkey() ] ); // NOTE: ordering by ar_timestamp and ar_id, to remove ambiguity. // XXX: Ideally, we would be ordering by ar_timestamp and ar_rev_id, but since we // don't have an index on ar_rev_id, that causes a file sort. $queryBuilder->orderBy( [ 'ar_timestamp', 'ar_id' ], SelectQueryBuilder::SORT_DESC ); if ( $limit !== null ) { $queryBuilder->limit( $limit ); } MediaWikiServices::getInstance()->getChangeTagsStore()->modifyDisplayQueryBuilder( $queryBuilder, 'archive' ); return $queryBuilder->caller( __METHOD__ )->fetchResultSet(); } /** * Return a RevisionRecord object containing data for the deleted revision. * * @internal only for use in SpecialUndelete * * @param PageIdentity $page * @param string $timestamp * @return RevisionRecord|null */ public function getRevisionRecordByTimestamp( PageIdentity $page, $timestamp ): ?RevisionRecord { return $this->getRevisionByConditions( $page, [ 'ar_timestamp' => $this->dbProvider->getReplicaDatabase()->timestamp( $timestamp ) ] ); } /** * Return the archived revision with the given ID. * * @param PageIdentity|null $page * @param int $revId * @return RevisionRecord|null */ public function getArchivedRevisionRecord( ?PageIdentity $page, int $revId ): ?RevisionRecord { return $this->getRevisionByConditions( $page, [ 'ar_rev_id' => $revId ] ); } /** * @param PageIdentity|null $page * @param array $conditions * @param array $options * * @return RevisionRecord|null */ private function getRevisionByConditions( ?PageIdentity $page, array $conditions, array $options = [] ): ?RevisionRecord { $queryBuilder = $this->revisionStore->newArchiveSelectQueryBuilder( $this->dbProvider->getReplicaDatabase() ) ->joinComment() ->where( $conditions ) ->options( $options ); if ( $page ) { $queryBuilder->andWhere( [ 'ar_namespace' => $page->getNamespace(), 'ar_title' => $page->getDBkey() ] ); } $row = $queryBuilder->caller( __METHOD__ )->fetchRow(); if ( $row ) { return $this->revisionStore->newRevisionFromArchiveRow( $row, 0, $page ); } return null; } /** * Return the most-previous revision, either live or deleted, against * the deleted revision given by timestamp. * * May produce unexpected results in case of history merges or other * unusual time issues. * * @param PageIdentity $page * @param string $timestamp * @return RevisionRecord|null Null when there is no previous revision */ public function getPreviousRevisionRecord( PageIdentity $page, string $timestamp ): ?RevisionRecord { $dbr = $this->dbProvider->getReplicaDatabase(); // Check the previous deleted revision... $row = $dbr->newSelectQueryBuilder() ->select( [ 'ar_rev_id', 'ar_timestamp' ] ) ->from( 'archive' ) ->where( [ 'ar_namespace' => $page->getNamespace(), 'ar_title' => $page->getDBkey(), $dbr->expr( 'ar_timestamp', '<', $dbr->timestamp( $timestamp ) ), ] ) ->orderBy( 'ar_timestamp DESC' ) ->caller( __METHOD__ )->fetchRow(); $prevDeleted = $row ? wfTimestamp( TS_MW, $row->ar_timestamp ) : false; $prevDeletedId = $row ? intval( $row->ar_rev_id ) : null; $row = $dbr->newSelectQueryBuilder() ->select( [ 'rev_id', 'rev_timestamp' ] ) ->from( 'page' ) ->join( 'revision', null, 'page_id = rev_page' ) ->where( [ 'page_namespace' => $page->getNamespace(), 'page_title' => $page->getDBkey(), $dbr->expr( 'rev_timestamp', '<', $dbr->timestamp( $timestamp ) ) ] ) ->orderBy( 'rev_timestamp DESC' ) ->caller( __METHOD__ )->fetchRow(); $prevLive = $row ? wfTimestamp( TS_MW, $row->rev_timestamp ) : false; $prevLiveId = $row ? intval( $row->rev_id ) : null; if ( $prevLive && $prevLive > $prevDeleted ) { // Most prior revision was live $rec = $this->revisionStore->getRevisionById( $prevLiveId ); } elseif ( $prevDeleted ) { // Most prior revision was deleted $rec = $this->getArchivedRevisionRecord( $page, $prevDeletedId ); } else { $rec = null; } return $rec; } /** * Returns the ID of the latest deleted revision. * * @param PageIdentity $page * * @return int|false The revision's ID, or false if there is no deleted revision. */ public function getLastRevisionId( PageIdentity $page ) { $revId = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder() ->select( 'ar_rev_id' ) ->from( 'archive' ) ->where( [ 'ar_namespace' => $page->getNamespace(), 'ar_title' => $page->getDBkey() ] ) ->orderBy( [ 'ar_timestamp', 'ar_id' ], SelectQueryBuilder::SORT_DESC ) ->caller( __METHOD__ )->fetchField(); return $revId ? intval( $revId ) : false; } /** * Quick check if any archived revisions are present for the page. * This says nothing about whether the page currently exists in the page table or not. * * @param PageIdentity $page * * @return bool */ public function hasArchivedRevisions( PageIdentity $page ): bool { $row = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder() ->select( '1' ) // We don't care about the value. Allow the database to optimize. ->from( 'archive' ) ->where( [ 'ar_namespace' => $page->getNamespace(), 'ar_title' => $page->getDBkey() ] ) ->caller( __METHOD__ ) ->fetchRow(); return (bool)$row; } } PK ! C�� � SlotRenderingProvider.phpnu �Iw�� <?php namespace MediaWiki\Revision; use MediaWiki\Parser\ParserOutput; /** * A lazy provider of ParserOutput objects for a revision's individual slots. * * @since 1.32 */ interface SlotRenderingProvider { /** * @param string $role * @param array $hints Hints given as an associative array. Known keys: * - 'generate-html' => bool: Whether the caller is interested in output HTML (as opposed * to just meta-data). Default is to generate HTML. * * @throws SuppressedDataException if the content is not accessible for the audience * specified in the constructor. * @return ParserOutput */ public function getSlotParserOutput( $role, array $hints = [] ); } PK ! E��UJ J BadRevisionException.phpnu �Iw�� <?php namespace MediaWiki\Revision; /** * Exception raised when the text of a revision is permanently missing or * corrupt. This wraps BadBlobException which is thrown by the Storage layer. * To mark a revision as permanently missing, use findBadBlobs.php. */ class BadRevisionException extends RevisionAccessException { } PK ! 1�t RevisionLookup.phpnu �Iw�� <?php /** * Service for looking up page revisions. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Revision; use MediaWiki\Linker\LinkTarget; use MediaWiki\Page\PageIdentity; use Wikimedia\Rdbms\IDBAccessObject; /** * Service for looking up page revisions. * * @note This was written to act as a drop-in replacement for the corresponding * static methods in the old Revision class (which was later removed in 1.37). * * @since 1.31 * @since 1.32 Renamed from MediaWiki\Storage\RevisionLookup */ interface RevisionLookup { /** * Load a page revision from a given revision ID number. * Returns null if no such revision can be found. * * MCR migration note: this replaced Revision::newFromId * * $flags include: * * @param int $id * @param int $flags bit field, see IDBAccessObject::READ_XXX * @param PageIdentity|null $page The page the revision belongs to. * Providing the page may improve performance. * * @return RevisionRecord|null */ public function getRevisionById( $id, $flags = 0, ?PageIdentity $page = null ); /** * Load either the current, or a specified, revision * that's attached to a given link target. If not attached * to that link target, will return null. * * MCR migration note: this replaced Revision::newFromTitle * * @param LinkTarget|PageIdentity $page Calling with LinkTarget is deprecated since 1.36 * @param int $revId (optional) * @param int $flags bit field, see IDBAccessObject::READ_XXX * @return RevisionRecord|null */ public function getRevisionByTitle( $page, $revId = 0, $flags = 0 ); /** * Load either the current, or a specified, revision * that's attached to a given page ID. * Returns null if no such revision can be found. * * MCR migration note: this replaced Revision::newFromPageId * * @param int $pageId * @param int $revId (optional) * @param int $flags bit field, see IDBAccessObject::READ_XXX * @return RevisionRecord|null */ public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ); /** * Load the revision for the given title with the given timestamp. * WARNING: Timestamps may in some circumstances not be unique, * so this isn't the best key to use. * * MCR migration note: this replaced Revision::loadFromTimestamp * * @param LinkTarget|PageIdentity $page Calling with LinkTarget is deprecated since 1.36 * @param string $timestamp * @param int $flags Bitfield (optional) include: * IDBAccessObject::READ_LATEST: Select the data from the primary DB * IDBAccessObject::READ_LOCKING: Select & lock the data from the primary DB * Default: IDBAccessObject::READ_NORMAL * @return RevisionRecord|null */ public function getRevisionByTimestamp( $page, string $timestamp, int $flags = IDBAccessObject::READ_NORMAL ): ?RevisionRecord; /** * Get previous revision for this title * * MCR migration note: this replaced Revision::getPrevious * * @param RevisionRecord $rev * @param int $flags (optional) $flags include: * IDBAccessObject::READ_LATEST: Select the data from the primary DB * * @return RevisionRecord|null */ public function getPreviousRevision( RevisionRecord $rev, $flags = 0 ); /** * Get next revision for this title * * MCR migration note: this replaced Revision::getNext * * @param RevisionRecord $rev * @param int $flags (optional) $flags include: * IDBAccessObject::READ_LATEST: Select the data from the primary DB * * @return RevisionRecord|null */ public function getNextRevision( RevisionRecord $rev, $flags = 0 ); /** * Get rev_timestamp from rev_id, without loading the rest of the row. * * MCR migration note: this replaced Revision::getTimestampFromId * * @param int $id * @param int $flags * @return string|false False if not found * @since 1.34 (present earlier in RevisionStore) */ public function getTimestampFromId( $id, $flags = 0 ); /** * Load a revision based on a known page ID and current revision ID from the DB * * This method allows for the use of caching, though accessing anything that normally * requires permission checks (aside from the text) will trigger a small DB lookup. * * MCR migration note: this replaced Revision::newKnownCurrent * * @param PageIdentity $page the associated page * @param int $revId current revision of this page * * @return RevisionRecord|false Returns false if missing */ public function getKnownCurrentRevision( PageIdentity $page, $revId = 0 ); /** * Get the first revision of the page. * * @since 1.35 * @param LinkTarget|PageIdentity $page Calling with LinkTarget is deprecated since 1.36 * @param int $flags bit field, see IDBAccessObject::READ_* constants. * @return RevisionRecord|null */ public function getFirstRevision( $page, int $flags = IDBAccessObject::READ_NORMAL ): ?RevisionRecord; } PK ! �k�� � RevisionStoreCacheRecord.phpnu �Iw�� <?php /** * A RevisionStoreRecord loaded from the cache. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\Revision; use MediaWiki\CommentStore\CommentStoreComment; use MediaWiki\Page\PageIdentity; use MediaWiki\Permissions\Authority; use MediaWiki\User\UserIdentity; /** * A cached RevisionStoreRecord. Ensures that changes performed "behind the back" * of the cache do not cause the revision record to deliver stale data. * * @internal * @since 1.33 */ class RevisionStoreCacheRecord extends RevisionStoreRecord { /** * @var null|callable ( int $revId ): [ int $rev_deleted, UserIdentity $user ] */ private $mCallback; /** * @note Avoid calling this constructor directly. Use the appropriate methods * in RevisionStore instead. * * @param callable $callback Callback for loading data. * Signature: function ( int $revId ): [ int $rev_deleted, UserIdentity $user ] * @param PageIdentity $page The page this RevisionRecord is associated with. * @param UserIdentity $user * @param CommentStoreComment $comment * @param \stdClass $row A row from the revision table. Use RevisionStore::getQueryInfo() to build * a query that yields the required fields. * @param RevisionSlots $slots The slots of this revision. * @param false|string $wikiID Relevant wiki id or self::LOCAL for the current one. */ public function __construct( callable $callback, PageIdentity $page, UserIdentity $user, CommentStoreComment $comment, $row, RevisionSlots $slots, $wikiID = self::LOCAL ) { parent::__construct( $page, $user, $comment, $row, $slots, $wikiID ); $this->mCallback = $callback; } /** * Overridden to ensure that we return a fresh value and not a cached one. * * @return int */ public function getVisibility() { if ( $this->mCallback ) { $this->loadFreshRow(); } return parent::getVisibility(); } /** * Overridden to ensure that we return a fresh value and not a cached one. * * @param int $audience * @param Authority|null $performer * * @return UserIdentity The identity of the revision author, null if access is forbidden. */ public function getUser( $audience = self::FOR_PUBLIC, ?Authority $performer = null ) { if ( $this->mCallback ) { $this->loadFreshRow(); } return parent::getUser( $audience, $performer ); } /** * Load a fresh row from the database to ensure we return updated information * * @throws RevisionAccessException if the row could not be loaded */ private function loadFreshRow() { [ $freshRevDeleted, $freshUser ] = call_user_func( $this->mCallback, $this->mId ); // Set to null to ensure we do not make unnecessary queries for subsequent getter calls, // and to allow the closure to be freed. $this->mCallback = null; if ( $freshRevDeleted !== null && $freshUser !== null ) { $this->mDeleted = intval( $freshRevDeleted ); $this->mUser = $freshUser; } else { throw new RevisionAccessException( 'Unable to load fresh row for rev_id: {rev_id}', [ 'rev_id' => $this->mId ] ); } } } PK ! ��5r�� �� RevisionStore.phpnu �Iw�� PK ! ��M �M 9� RevisionRecord.phpnu �Iw�� PK ! ��`N�B �B RenderedRevision.phpnu �Iw�� PK ! _�s�, , 8D ArchiveSelectQueryBuilder.phpnu �Iw�� PK ! <�` ` �L RevisionArchiveRecord.phpnu �Iw�� PK ! T�nw� � Zj SuppressedDataException.phpnu �Iw�� PK ! l�E�< < >o RevisionFactory.phpnu �Iw�� PK ! �!j#�. �. �� RevisionRenderer.phpnu �Iw�� PK ! � Ά � � MutableRevisionSlots.phpnu �Iw�� PK ! �"�� �� RevisionStoreFactory.phpnu �Iw�� PK ! �k���! �! @� RevisionSlots.phpnu �Iw�� PK ! �Xz� � F RevisionSelectQueryBuilder.phpnu �Iw�� PK ! "�BN�O �O Y SlotRecord.phpnu �Iw�� PK ! � �v� � �\ ContributionsSegment.phpnu �Iw�� PK ! r�D� � �g SlotRoleRegistry.phpnu �Iw�� PK ! �ʹE� � ن RevisionAccessException.phpnu �Iw�� PK ! ���� � IncompleteRevisionException.phpnu �Iw�� PK ! ���g� � � RevisionStoreRecord.phpnu �Iw�� PK ! ��T T ð SlotRoleHandler.phpnu �Iw�� PK ! ^�� � Z� FallbackSlotRoleHandler.phpnu �Iw�� PK ! {��� * E� Hook/ContentHandlerDefaultModelForHook.phpnu �Iw�� PK ! <7e<� � # �� Hook/RevisionRecordInsertedHook.phpnu �Iw�� PK ! ?��5�* �* �� MutableRevisionRecord.phpnu �Iw�� PK ! {��)b b � MainSlotRoleHandler.phpnu �Iw�� PK ! ��j ArchivedRevisionLookup.phpnu �Iw�� PK ! C�� � �4 SlotRenderingProvider.phpnu �Iw�� PK ! E��UJ J �7 BadRevisionException.phpnu �Iw�� PK ! 1�t h9 RevisionLookup.phpnu �Iw�� PK ! �k�� � �O RevisionStoreCacheRecord.phpnu �Iw�� PK � �^
| ver. 1.1 | |
.
| PHP 8.4.18 | Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Ñтраницы: 0.02 |
proxy
|
phpinfo
|
ÐаÑтройка