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