Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 178 |
|
0.00% |
0 / 13 |
CRAP | |
0.00% |
0 / 1 |
Importer | |
0.00% |
0 / 178 |
|
0.00% |
0 / 13 |
1806 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
setStorage | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
put | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
handleBoard | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
12 | |||
handleHeader | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
handleTopic | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
12 | |||
handlePost | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
handleSummary | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
getRevisions | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
getRevision | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
110 | |||
mapId | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
checkTransWikiMode | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
createLocalUser | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | |
3 | namespace Flow\Dump; |
4 | |
5 | use Flow\Container; |
6 | use Flow\Data\ManagerGroup; |
7 | use Flow\DbFactory; |
8 | use Flow\Import\HistoricalUIDGenerator; |
9 | use Flow\Import\ImportException; |
10 | use Flow\Model\AbstractRevision; |
11 | use Flow\Model\Header; |
12 | use Flow\Model\PostRevision; |
13 | use Flow\Model\PostSummary; |
14 | use Flow\Model\TopicListEntry; |
15 | use Flow\Model\UUID; |
16 | use Flow\Model\Workflow; |
17 | use Flow\OccupationController; |
18 | use MediaWiki\Deferred\SiteStatsUpdate; |
19 | use MediaWiki\Extension\CentralAuth\CentralAuthServices; |
20 | use MediaWiki\Extension\CentralAuth\User\CentralAuthUser; |
21 | use MediaWiki\MediaWikiServices; |
22 | use MediaWiki\Title\Title; |
23 | use MediaWiki\User\CentralId\CentralIdLookup; |
24 | use MediaWiki\User\User; |
25 | use MediaWiki\WikiMap\WikiMap; |
26 | use RuntimeException; |
27 | use WikiImporter; |
28 | use XMLReader; |
29 | |
30 | class Importer { |
31 | /** |
32 | * @var WikiImporter |
33 | */ |
34 | protected $importer; |
35 | |
36 | /** |
37 | * @var ManagerGroup|null |
38 | */ |
39 | protected $storage; |
40 | |
41 | /** |
42 | * The most recently imported board workflow (if any). |
43 | * |
44 | * @var Workflow|null |
45 | */ |
46 | protected $boardWorkflow; |
47 | |
48 | /** |
49 | * The most recently imported topic workflow (if any). |
50 | * |
51 | * @var Workflow|null |
52 | */ |
53 | protected $topicWorkflow; |
54 | |
55 | /** |
56 | * @var array Map of old to new IDs |
57 | */ |
58 | protected $idMap = []; |
59 | |
60 | /** |
61 | * To convert between global and local user ids |
62 | * |
63 | * @var CentralIdLookup|null |
64 | */ |
65 | protected $lookup; |
66 | |
67 | /** |
68 | * Whether the current board is being imported in trans-wiki mode |
69 | * |
70 | * @var bool |
71 | */ |
72 | protected $transWikiMode = false; |
73 | |
74 | /** |
75 | * @param WikiImporter $importer |
76 | */ |
77 | public function __construct( WikiImporter $importer ) { |
78 | $this->importer = $importer; |
79 | try { |
80 | $this->lookup = MediaWikiServices::getInstance() |
81 | ->getCentralIdLookupFactory() |
82 | ->getLookup( 'CentralAuth' ); |
83 | } catch ( \Throwable $unused ) { |
84 | $this->lookup = null; |
85 | } |
86 | } |
87 | |
88 | /** |
89 | * @param ManagerGroup $storage |
90 | */ |
91 | public function setStorage( ManagerGroup $storage ) { |
92 | $this->storage = $storage; |
93 | } |
94 | |
95 | /** |
96 | * @param object $object |
97 | * @param array $metadata |
98 | */ |
99 | protected function put( $object, array $metadata = [] ) { |
100 | if ( $this->storage ) { |
101 | $this->storage->put( $object, [ 'imported' => true ] + $metadata ); |
102 | |
103 | // prevent memory from being filled up |
104 | $this->storage->clear(); |
105 | |
106 | // keep workflow objects around, so follow-up `put`s (e.g. to update |
107 | // last_update_timestamp) don't confuse it for a new object |
108 | foreach ( [ $this->boardWorkflow, $this->topicWorkflow ] as $object ) { |
109 | if ( $object ) { |
110 | $this->storage->getStorage( get_class( $object ) )->merge( $object ); |
111 | } |
112 | } |
113 | } |
114 | } |
115 | |
116 | public function handleBoard() { |
117 | $this->checkTransWikiMode( |
118 | $this->importer->nodeAttribute( 'id' ), |
119 | $this->importer->nodeAttribute( 'title' ) |
120 | ); |
121 | |
122 | $id = $this->mapId( $this->importer->nodeAttribute( 'id' ) ); |
123 | $this->importer->debug( 'Enter board handler for ' . $id ); |
124 | |
125 | $uuid = UUID::create( $id ); |
126 | $title = Title::newFromDBkey( $this->importer->nodeAttribute( 'title' ) ); |
127 | |
128 | $this->boardWorkflow = Workflow::fromStorageRow( [ |
129 | 'workflow_id' => $uuid->getAlphadecimal(), |
130 | 'workflow_type' => 'discussion', |
131 | 'workflow_wiki' => WikiMap::getCurrentWikiId(), |
132 | 'workflow_page_id' => $title->getArticleID(), |
133 | 'workflow_namespace' => $title->getNamespace(), |
134 | 'workflow_title_text' => $title->getDBkey(), |
135 | 'workflow_last_update_timestamp' => $uuid->getTimestamp( TS_MW ), |
136 | ] ); |
137 | |
138 | // create page if it does not yet exist |
139 | /** @var OccupationController $occupationController */ |
140 | $occupationController = Container::get( 'occupation_controller' ); |
141 | $creationStatus = $occupationController->safeAllowCreation( $title, $occupationController->getTalkpageManager() ); |
142 | if ( !$creationStatus->isOK() ) { |
143 | throw new RuntimeException( $creationStatus->__toString() ); |
144 | } |
145 | |
146 | $ensureStatus = $occupationController->ensureFlowRevision( |
147 | MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title ), |
148 | $this->boardWorkflow |
149 | ); |
150 | if ( !$ensureStatus->isOK() ) { |
151 | throw new RuntimeException( $ensureStatus->__toString() ); |
152 | } |
153 | |
154 | $this->put( $this->boardWorkflow, [] ); |
155 | } |
156 | |
157 | public function handleHeader() { |
158 | $id = $this->mapId( $this->importer->nodeAttribute( 'id' ) ); |
159 | $this->importer->debug( 'Enter description handler for ' . $id ); |
160 | |
161 | $metadata = [ 'workflow' => $this->boardWorkflow ]; |
162 | |
163 | $revisions = $this->getRevisions( [ Header::class, 'fromStorageRow' ] ); |
164 | foreach ( $revisions as $revision ) { |
165 | $this->put( $revision, $metadata ); |
166 | } |
167 | |
168 | /** @var Header $revision */ |
169 | $revision = end( $revisions ); |
170 | $this->boardWorkflow->updateLastUpdated( $revision->getRevisionId() ); |
171 | $this->put( $this->boardWorkflow, [] ); |
172 | } |
173 | |
174 | public function handleTopic() { |
175 | $id = $this->mapId( $this->importer->nodeAttribute( 'id' ) ); |
176 | $this->importer->debug( 'Enter topic handler for ' . $id ); |
177 | |
178 | $uuid = UUID::create( $id ); |
179 | $title = $this->boardWorkflow->getArticleTitle(); |
180 | |
181 | $this->topicWorkflow = Workflow::fromStorageRow( [ |
182 | 'workflow_id' => $uuid->getAlphadecimal(), |
183 | 'workflow_type' => 'topic', |
184 | 'workflow_wiki' => WikiMap::getCurrentWikiId(), |
185 | 'workflow_page_id' => $title->getArticleID(), |
186 | 'workflow_namespace' => $title->getNamespace(), |
187 | 'workflow_title_text' => $title->getDBkey(), |
188 | 'workflow_last_update_timestamp' => $uuid->getTimestamp( TS_MW ), |
189 | ] ); |
190 | $topicListEntry = TopicListEntry::create( $this->boardWorkflow, $this->topicWorkflow ); |
191 | |
192 | $metadata = [ |
193 | 'board-workflow' => $this->boardWorkflow, |
194 | 'workflow' => $this->topicWorkflow, |
195 | // @todo: topic-title & first-post? (used only in NotificationListener) |
196 | ]; |
197 | |
198 | // create page if it does not yet exist |
199 | /** @var OccupationController $occupationController */ |
200 | $occupationController = Container::get( 'occupation_controller' ); |
201 | $creationStatus = $occupationController->safeAllowCreation( |
202 | $this->topicWorkflow->getArticleTitle(), |
203 | $occupationController->getTalkpageManager() |
204 | ); |
205 | if ( !$creationStatus->isOK() ) { |
206 | throw new RuntimeException( $creationStatus->__toString() ); |
207 | } |
208 | |
209 | $ensureStatus = $occupationController->ensureFlowRevision( |
210 | MediaWikiServices::getInstance()->getWikiPageFactory() |
211 | ->newFromTitle( $this->topicWorkflow->getArticleTitle() ), |
212 | $this->topicWorkflow |
213 | ); |
214 | if ( !$ensureStatus->isOK() ) { |
215 | throw new RuntimeException( $ensureStatus->__toString() ); |
216 | } |
217 | |
218 | $this->put( $this->topicWorkflow, $metadata ); |
219 | $this->put( $topicListEntry, $metadata ); |
220 | } |
221 | |
222 | public function handlePost() { |
223 | $id = $this->mapId( $this->importer->nodeAttribute( 'id' ) ); |
224 | $this->importer->debug( 'Enter post handler for ' . $id ); |
225 | |
226 | $metadata = [ |
227 | 'workflow' => $this->topicWorkflow |
228 | // @todo: topic-title? (used only in NotificationListener) |
229 | ]; |
230 | |
231 | $revisions = $this->getRevisions( [ PostRevision::class, 'fromStorageRow' ] ); |
232 | foreach ( $revisions as $revision ) { |
233 | $this->put( $revision, $metadata ); |
234 | } |
235 | |
236 | /** @var PostRevision $revision */ |
237 | $revision = end( $revisions ); |
238 | $this->topicWorkflow->updateLastUpdated( $revision->getRevisionId() ); |
239 | $this->put( $this->topicWorkflow, $metadata ); |
240 | } |
241 | |
242 | public function handleSummary() { |
243 | $id = $this->mapId( $this->importer->nodeAttribute( 'id' ) ); |
244 | $this->importer->debug( 'Enter summary handler for ' . $id ); |
245 | |
246 | $metadata = [ 'workflow' => $this->topicWorkflow ]; |
247 | |
248 | $revisions = $this->getRevisions( [ PostSummary::class, 'fromStorageRow' ] ); |
249 | foreach ( $revisions as $revision ) { |
250 | $this->put( $revision, $metadata ); |
251 | } |
252 | |
253 | /** @var PostSummary $revision */ |
254 | $revision = end( $revisions ); |
255 | $this->topicWorkflow->updateLastUpdated( $revision->getRevisionId() ); |
256 | $this->put( $this->topicWorkflow, $metadata ); |
257 | } |
258 | |
259 | /** |
260 | * @param callable $callback The relevant fromStorageRow callback |
261 | * @return AbstractRevision[] |
262 | */ |
263 | protected function getRevisions( $callback ) { |
264 | $revisions = []; |
265 | |
266 | // keep processing <revision> nodes until </revisions> |
267 | while ( $this->importer->getReader()->localName !== 'revisions' || |
268 | $this->importer->getReader()->nodeType !== XMLReader::END_ELEMENT |
269 | ) { |
270 | if ( $this->importer->getReader()->localName === 'revision' ) { |
271 | $revisions[] = $this->getRevision( $callback ); |
272 | } |
273 | $this->importer->getReader()->read(); |
274 | } |
275 | |
276 | return $revisions; |
277 | } |
278 | |
279 | /** |
280 | * @param callable $callback The relevant fromStorageRow callback |
281 | * @return AbstractRevision |
282 | */ |
283 | protected function getRevision( $callback ) { |
284 | $id = $this->mapId( $this->importer->nodeAttribute( 'id' ) ); |
285 | $this->importer->debug( 'Enter revision handler for ' . $id ); |
286 | |
287 | // isEmptyElement will no longer be valid after we've started iterating |
288 | // the attributes |
289 | $empty = $this->importer->getReader()->isEmptyElement; |
290 | |
291 | $attribs = []; |
292 | |
293 | $this->importer->getReader()->moveToFirstAttribute(); |
294 | do { |
295 | $attribs[$this->importer->getReader()->name] = $this->importer->getReader()->value; |
296 | } while ( $this->importer->getReader()->moveToNextAttribute() ); |
297 | |
298 | $idFields = [ 'id', 'typeid', 'treedescendantid', 'treerevid', 'parentid', 'treeparentid', 'lasteditid' ]; |
299 | foreach ( $idFields as $idField ) { |
300 | if ( isset( $attribs[ $idField ] ) ) { |
301 | $attribs[ $idField ] = $this->mapId( $attribs[ $idField ] ); |
302 | } |
303 | } |
304 | |
305 | if ( $this->transWikiMode && $this->lookup ) { |
306 | $userFields = [ 'user', 'treeoriguser', 'moduser', 'edituser' ]; |
307 | foreach ( $userFields as $userField ) { |
308 | $globalUserIdField = 'global' . $userField . 'id'; |
309 | if ( isset( $attribs[ $globalUserIdField ] ) ) { |
310 | $localUser = $this->lookup->localUserFromCentralId( |
311 | (int)$attribs[ $globalUserIdField ], |
312 | CentralIdLookup::AUDIENCE_RAW |
313 | ); |
314 | if ( !$localUser ) { |
315 | $localUser = $this->createLocalUser( (int)$attribs[ $globalUserIdField ] ); |
316 | } |
317 | $attribs[ $userField . 'id' ] = $localUser->getId(); |
318 | $attribs[ $userField . 'wiki' ] = WikiMap::getCurrentWikiId(); |
319 | } elseif ( isset( $attribs[ $userField . 'ip' ] ) ) { |
320 | // make anons local users |
321 | $attribs[ $userField . 'wiki' ] = WikiMap::getCurrentWikiId(); |
322 | } |
323 | } |
324 | } |
325 | |
326 | // now that we've moved inside the node (to fetch attributes), |
327 | // nodeContents() is no longer reliable: is uses isEmptyContent (which |
328 | // will now no longer respond with 'true') to see if the node should be |
329 | // skipped - use the value we've fetched earlier! |
330 | $attribs['content'] = $empty ? '' : $this->importer->nodeContents(); |
331 | |
332 | // make sure there are no leftover key columns (unknown to $attribs) |
333 | $keys = array_intersect_key( array_flip( Exporter::$map ), $attribs ); |
334 | // now make sure $values columns are in the same order as $keys are |
335 | // (array_merge) and there are no leftover columns (array_intersect_key) |
336 | $values = array_intersect_key( array_merge( $keys, $attribs ), $keys ); |
337 | // combine them |
338 | $attribs = array_combine( $keys, $values ); |
339 | |
340 | // now fill in missing attributes |
341 | $keys = array_fill_keys( array_keys( Exporter::$map ), null ); |
342 | $attribs += $keys; |
343 | |
344 | return $callback( $attribs ); |
345 | } |
346 | |
347 | /** |
348 | * When in trans-wiki mode, return a new id based on the same timestamp |
349 | * |
350 | * @param string $id |
351 | * @return string |
352 | */ |
353 | private function mapId( $id ) { |
354 | if ( !$this->transWikiMode ) { |
355 | return $id; |
356 | } |
357 | |
358 | if ( !isset( $this->idMap[ $id ] ) ) { |
359 | $this->idMap[ $id ] = UUID::create( HistoricalUIDGenerator::historicalTimestampedUID88( |
360 | UUID::hex2timestamp( UUID::create( $id )->getHex() ) |
361 | ) )->getAlphadecimal(); |
362 | } |
363 | return $this->idMap[ $id ]; |
364 | } |
365 | |
366 | /** |
367 | * Check if a board already exist and should be imported in trans-wiki mode |
368 | * |
369 | * @param string $boardWorkflowId |
370 | * @param string $title |
371 | */ |
372 | private function checkTransWikiMode( $boardWorkflowId, $title ) { |
373 | /** @var DbFactory $dbFactory */ |
374 | $dbFactory = Container::get( 'db.factory' ); |
375 | $workflowExist = (bool)$dbFactory->getDB( DB_PRIMARY )->newSelectQueryBuilder() |
376 | ->select( 'workflow_id' ) |
377 | ->from( 'flow_workflow' ) |
378 | ->where( [ 'workflow_id' => UUID::create( $boardWorkflowId )->getBinary() ] ) |
379 | ->caller( __METHOD__ ) |
380 | ->fetchField(); |
381 | |
382 | if ( $workflowExist ) { |
383 | $this->importer->debug( "$title will be imported in trans-wiki mode" ); |
384 | } |
385 | $this->transWikiMode = $workflowExist; |
386 | } |
387 | |
388 | /** |
389 | * Create a local user corresponding to a global id |
390 | * |
391 | * @param int $globalUserId |
392 | * @return User Local user |
393 | * @throws ImportException |
394 | */ |
395 | private function createLocalUser( $globalUserId ) { |
396 | if ( !$this->lookup ) { |
397 | throw new ImportException( 'Creating local users is not supported without central id provider' ); |
398 | } |
399 | |
400 | $globalUser = CentralAuthUser::newFromId( $globalUserId ); |
401 | $localUser = User::newFromName( $globalUser->getName() ); |
402 | |
403 | if ( $localUser->getId() ) { |
404 | throw new ImportException( "User '{$localUser->getName()}' already exists" ); |
405 | } |
406 | |
407 | $status = CentralAuthServices::getUtilityService()->autoCreateUser( $localUser ); |
408 | if ( !$status->isGood() ) { |
409 | throw new ImportException( |
410 | "autoCreateUser failed for {$localUser->getName()}: " . print_r( $status->getErrors(), true ) |
411 | ); |
412 | } |
413 | |
414 | # Update user count |
415 | $ssUpdate = SiteStatsUpdate::factory( [ 'users' => 1 ] ); |
416 | $ssUpdate->doUpdate(); |
417 | |
418 | return $localUser; |
419 | } |
420 | } |