Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
79.44% covered (warning)
79.44%
85 / 107
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImportableOldRevisionImporter
79.44% covered (warning)
79.44%
85 / 107
50.00% covered (danger)
50.00%
1 / 2
26.21
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 import
77.55% covered (warning)
77.55%
76 / 98
0.00% covered (danger)
0.00%
0 / 1
25.99
1<?php
2
3use MediaWiki\CommentStore\CommentStoreComment;
4use MediaWiki\Context\RequestContext;
5use MediaWiki\MediaWikiServices;
6use MediaWiki\Page\Event\PageLatestRevisionChangedEvent;
7use MediaWiki\Page\WikiPageFactory;
8use MediaWiki\Revision\MutableRevisionRecord;
9use MediaWiki\Revision\RevisionStore;
10use MediaWiki\Revision\SlotRoleRegistry;
11use MediaWiki\Storage\PageUpdater;
12use MediaWiki\Storage\PageUpdaterFactory;
13use MediaWiki\Title\Title;
14use MediaWiki\User\UserFactory;
15use Psr\Log\LoggerInterface;
16use Wikimedia\Rdbms\IConnectionProvider;
17use Wikimedia\Rdbms\IDBAccessObject;
18use Wikimedia\Rdbms\SelectQueryBuilder;
19
20/**
21 * @since 1.31
22 */
23class ImportableOldRevisionImporter implements OldRevisionImporter {
24
25    private bool $doUpdates;
26    private LoggerInterface $logger;
27    private IConnectionProvider $dbProvider;
28    private RevisionStore $revisionStore;
29    private SlotRoleRegistry $slotRoleRegistry;
30    private WikiPageFactory $wikiPageFactory;
31    private PageUpdaterFactory $pageUpdaterFactory;
32    private UserFactory $userFactory;
33
34    public function __construct(
35        $doUpdates,
36        LoggerInterface $logger,
37        IConnectionProvider $dbProvider,
38        RevisionStore $revisionStore,
39        SlotRoleRegistry $slotRoleRegistry,
40        ?WikiPageFactory $wikiPageFactory = null,
41        ?PageUpdaterFactory $pageUpdaterFactory = null,
42        ?UserFactory $userFactory = null
43    ) {
44        $this->doUpdates = $doUpdates;
45        $this->logger = $logger;
46        $this->dbProvider = $dbProvider;
47        $this->revisionStore = $revisionStore;
48        $this->slotRoleRegistry = $slotRoleRegistry;
49
50        $services = MediaWikiServices::getInstance();
51        // @todo: temporary - remove when FileImporter extension is updated
52        $this->wikiPageFactory = $wikiPageFactory ?? $services->getWikiPageFactory();
53        $this->pageUpdaterFactory = $pageUpdaterFactory ?? $services->getPageUpdaterFactory();
54        $this->userFactory = $userFactory ?? $services->getUserFactory();
55    }
56
57    /** @inheritDoc */
58    public function import( ImportableOldRevision $importableRevision, $doUpdates = true ) {
59        $dbw = $this->dbProvider->getPrimaryDatabase();
60
61        # Sneak a single revision into place
62        $user = $importableRevision->getUserObj() ?: $this->userFactory->newFromName( $importableRevision->getUser() );
63        if ( $user ) {
64            $userId = $user->getId();
65            $userText = $user->getName();
66        } else {
67            $userId = 0;
68            $userText = $importableRevision->getUser();
69            $user = $this->userFactory->newAnonymous();
70        }
71
72        // avoid memory leak...?
73        Title::clearCaches();
74
75        $page = $this->wikiPageFactory->newFromTitle( $importableRevision->getTitle() );
76        $page->loadPageData( IDBAccessObject::READ_LATEST );
77        $mustCreatePage = !$page->exists();
78        if ( $mustCreatePage ) {
79            $pageId = $page->insertOn( $dbw );
80        } else {
81            $pageId = $page->getId();
82
83            // Note: sha1 has been in XML dumps since 2012. If you have an
84            // older dump, the duplicate detection here won't work.
85            if ( $importableRevision->getSha1Base36() !== false ) {
86                $prior = (bool)$dbw->newSelectQueryBuilder()
87                    ->select( '1' )
88                    ->from( 'revision' )
89                    ->where( [
90                        'rev_page' => $pageId,
91                        'rev_timestamp' => $dbw->timestamp( $importableRevision->getTimestamp() ),
92                        'rev_sha1' => $importableRevision->getSha1Base36()
93                    ] )
94                    ->caller( __METHOD__ )->fetchField();
95                if ( $prior ) {
96                    // @todo FIXME: This could fail slightly for multiple matches :P
97                    $this->logger->debug( __METHOD__ . ": skipping existing revision for [[" .
98                        $importableRevision->getTitle()->getPrefixedText() . "]], timestamp " .
99                        $importableRevision->getTimestamp() . "\n" );
100                    return false;
101                }
102            }
103        }
104
105        if ( !$pageId ) {
106            // This seems to happen if two clients simultaneously try to import the
107            // same page
108            $this->logger->debug( __METHOD__ . ': got invalid $pageId when importing revision of [[' .
109                $importableRevision->getTitle()->getPrefixedText() . ']], timestamp ' .
110                $importableRevision->getTimestamp() . "\n" );
111            return false;
112        }
113
114        // Select previous version to make size diffs correct
115        // @todo This assumes that multiple revisions of the same page are imported
116        // in order from oldest to newest.
117        $queryBuilder = $this->revisionStore->newSelectQueryBuilder( $dbw )
118            ->joinComment()
119            ->where( [ 'rev_page' => $pageId ] )
120            ->andWhere( $dbw->expr(
121                'rev_timestamp', '<=', $dbw->timestamp( $importableRevision->getTimestamp() )
122            ) )
123            ->orderBy( [ 'rev_timestamp', 'rev_id' ], SelectQueryBuilder::SORT_DESC );
124        $prevRevRow = $queryBuilder->caller( __METHOD__ )->fetchRow();
125
126        # @todo FIXME: Use original rev_id optionally (better for backups)
127        # Insert the row
128        $revisionRecord = new MutableRevisionRecord( $importableRevision->getTitle() );
129        $revisionRecord->setParentId( $prevRevRow ? (int)$prevRevRow->rev_id : 0 );
130        $revisionRecord->setComment(
131            CommentStoreComment::newUnsavedComment( $importableRevision->getComment() )
132        );
133
134        try {
135            $revUser = $this->userFactory->newFromAnyId( $userId, $userText );
136        } catch ( InvalidArgumentException ) {
137            $revUser = RequestContext::getMain()->getUser();
138        }
139        $revisionRecord->setUser( $revUser );
140
141        $originalRevision = $prevRevRow
142            ? $this->revisionStore->newRevisionFromRow(
143                $prevRevRow,
144                IDBAccessObject::READ_LATEST,
145                $importableRevision->getTitle()
146            )
147            : null;
148
149        foreach ( $importableRevision->getSlotRoles() as $role ) {
150            if ( !$this->slotRoleRegistry->isDefinedRole( $role ) ) {
151                throw new RuntimeException( "Undefined slot role $role" );
152            }
153
154            $newContent = $importableRevision->getContent( $role );
155            if ( !$originalRevision || !$originalRevision->hasSlot( $role ) ) {
156                $revisionRecord->setContent( $role, $newContent );
157            } else {
158                $originalSlot = $originalRevision->getSlot( $role );
159                if ( !$originalSlot->hasSameContent( $importableRevision->getSlot( $role ) ) ) {
160                    $revisionRecord->setContent( $role, $newContent );
161                } else {
162                    $revisionRecord->inheritSlot( $originalRevision->getSlot( $role ) );
163                }
164            }
165        }
166
167        $revisionRecord->setTimestamp( $importableRevision->getTimestamp() );
168        $revisionRecord->setMinorEdit( $importableRevision->getMinor() );
169        $revisionRecord->setPageId( $pageId );
170
171        $updater = $this->pageUpdaterFactory->newDerivedPageDataUpdater( $page );
172        $latestRev = $updater->grabCurrentRevision();
173        $latestRevId = $latestRev ? $latestRev->getId() : null;
174
175        $inserted = $this->revisionStore->insertRevisionOn( $revisionRecord, $dbw );
176        if ( $latestRev ) {
177            // If not found (false), cast to 0 so that the page is updated
178            // Just to be on the safe side, even though it should always be found
179            $latestRevTimestamp = $latestRev->getTimestamp();
180        } else {
181            $latestRevTimestamp = 0;
182        }
183        if ( $importableRevision->getTimestamp() >= $latestRevTimestamp ) {
184            $changed = $page->updateRevisionOn( $dbw, $inserted, $latestRevId );
185        } else {
186            $changed = false;
187        }
188
189        $tags = $importableRevision->getTags();
190        if ( $tags !== [] ) {
191            MediaWikiServices::getInstance()->getChangeTagsStore()->addTags( $tags, null, $inserted->getId() );
192        }
193
194        if ( $changed !== false && $this->doUpdates ) {
195            $this->logger->debug( __METHOD__ . ": running updates" );
196            // countable/oldcountable stuff is handled in WikiImporter::finishImportPage
197
198            $options = [
199                PageLatestRevisionChangedEvent::FLAG_SILENT => true,
200                PageLatestRevisionChangedEvent::FLAG_IMPLICIT => true,
201                'created' => $mustCreatePage,
202                'oldcountable' => 'no-change',
203            ];
204
205            $updater->setCause( PageUpdater::CAUSE_IMPORT );
206            $updater->setPerformer( $user ); // TODO: get the actual performer, not the revision author.
207            $updater->prepareUpdate( $inserted, $options );
208            $updater->doUpdates();
209        }
210
211        return true;
212    }
213
214}