Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
79.44% |
85 / 107 |
|
50.00% |
1 / 2 |
CRAP | |
0.00% |
0 / 1 |
ImportableOldRevisionImporter | |
79.44% |
85 / 107 |
|
50.00% |
1 / 2 |
26.21 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
import | |
77.55% |
76 / 98 |
|
0.00% |
0 / 1 |
25.99 |
1 | <?php |
2 | |
3 | use MediaWiki\CommentStore\CommentStoreComment; |
4 | use MediaWiki\Context\RequestContext; |
5 | use MediaWiki\MediaWikiServices; |
6 | use MediaWiki\Page\Event\PageLatestRevisionChangedEvent; |
7 | use MediaWiki\Page\WikiPageFactory; |
8 | use MediaWiki\Revision\MutableRevisionRecord; |
9 | use MediaWiki\Revision\RevisionStore; |
10 | use MediaWiki\Revision\SlotRoleRegistry; |
11 | use MediaWiki\Storage\PageUpdater; |
12 | use MediaWiki\Storage\PageUpdaterFactory; |
13 | use MediaWiki\Title\Title; |
14 | use MediaWiki\User\UserFactory; |
15 | use Psr\Log\LoggerInterface; |
16 | use Wikimedia\Rdbms\IConnectionProvider; |
17 | use Wikimedia\Rdbms\IDBAccessObject; |
18 | use Wikimedia\Rdbms\SelectQueryBuilder; |
19 | |
20 | /** |
21 | * @since 1.31 |
22 | */ |
23 | class 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 | } |