Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
78.72% |
222 / 282 |
|
36.36% |
4 / 11 |
CRAP | |
0.00% |
0 / 1 |
TalkpageImportOperation | |
78.72% |
222 / 282 |
|
36.36% |
4 / 11 |
48.48 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
import | |
69.74% |
53 / 76 |
|
0.00% |
0 / 1 |
11.25 | |||
importHeader | |
88.24% |
30 / 34 |
|
0.00% |
0 / 1 |
4.03 | |||
importTopic | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getTopicState | |
42.86% |
3 / 7 |
|
0.00% |
0 / 1 |
2.75 | |||
getFirstRevision | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
createTopicState | |
100.00% |
36 / 36 |
|
100.00% |
1 / 1 |
1 | |||
getExistingTopicState | |
42.86% |
3 / 7 |
|
0.00% |
0 / 1 |
6.99 | |||
importSummary | |
86.11% |
31 / 36 |
|
0.00% |
0 / 1 |
3.02 | |||
importPost | |
95.24% |
40 / 42 |
|
0.00% |
0 / 1 |
4 | |||
importObjectWithHistory | |
41.94% |
13 / 31 |
|
0.00% |
0 / 1 |
7.13 |
1 | <?php |
2 | |
3 | namespace Flow\Import; |
4 | |
5 | use Flow\Import\SourceStore\Exception as ImportSourceStoreException; |
6 | use Flow\Model\AbstractRevision; |
7 | use Flow\Model\Header; |
8 | use Flow\Model\PostRevision; |
9 | use Flow\Model\PostSummary; |
10 | use Flow\Model\TopicListEntry; |
11 | use Flow\Model\Workflow; |
12 | use Flow\OccupationController; |
13 | use MediaWiki\Exception\MWExceptionHandler; |
14 | use MediaWiki\MediaWikiServices; |
15 | use MediaWiki\Title\Title; |
16 | use MediaWiki\User\User; |
17 | use MediaWiki\Utils\MWTimestamp; |
18 | |
19 | class TalkpageImportOperation { |
20 | /** |
21 | * @var IImportSource |
22 | */ |
23 | protected $importSource; |
24 | |
25 | /** @var User User doing the conversion actions (e.g. initial description, wikitext |
26 | * archive edit). However, actions will be attributed to the original user when |
27 | * possible (e.g. the user who did the original LQT reply) |
28 | */ |
29 | protected $user; |
30 | |
31 | /** @var OccupationController */ |
32 | protected $occupationController; |
33 | |
34 | /** |
35 | * @param IImportSource $source |
36 | * @param User $user The import user; this will only be used when there is no |
37 | * 'original' user |
38 | * @param OccupationController $occupationController |
39 | */ |
40 | public function __construct( IImportSource $source, User $user, OccupationController $occupationController ) { |
41 | $this->importSource = $source; |
42 | $this->user = $user; |
43 | $this->occupationController = $occupationController; |
44 | } |
45 | |
46 | /** |
47 | * @param PageImportState $state |
48 | * @return bool True if import completed successfully |
49 | * @throws ImportSourceStoreException |
50 | * @throws \Exception |
51 | */ |
52 | public function import( PageImportState $state ) { |
53 | $destinationTitle = $state->boardWorkflow->getArticleTitle(); |
54 | $state->logger->info( 'Importing to ' . $destinationTitle->getPrefixedText() ); |
55 | $isNew = $state->boardWorkflow->isNew(); |
56 | $state->logger->debug( 'Workflow isNew: ' . var_export( $isNew, true ) ); |
57 | if ( $isNew ) { |
58 | // Explicitly allow creation of board |
59 | $creationStatus = $this->occupationController->safeAllowCreation( |
60 | $destinationTitle, |
61 | $this->user, |
62 | /* $mustNotExist = */ true |
63 | ); |
64 | if ( !$creationStatus->isGood() ) { |
65 | throw new ImportException( |
66 | "safeAllowCreation failed to allow the import destination, with the following error:\n" . |
67 | $creationStatus->getWikiText() |
68 | ); |
69 | } |
70 | |
71 | // Makes sure the page exists and a Flow-specific revision has been inserted |
72 | $status = $this->occupationController->ensureFlowRevision( |
73 | MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $destinationTitle ), |
74 | $state->boardWorkflow |
75 | ); |
76 | $state->logger->debug( |
77 | 'ensureFlowRevision status isOK: ' . var_export( $status->isOK(), true ) |
78 | ); |
79 | $state->logger->debug( |
80 | 'ensureFlowRevision status isGood: ' . var_export( $status->isGood(), true ) |
81 | ); |
82 | |
83 | if ( $status->isOK() ) { |
84 | $ensureValue = $status->getValue(); |
85 | $revisionRecord = $ensureValue['revision-record']; |
86 | $state->logger->debug( |
87 | 'ensureFlowRevision already-existed: ' . var_export( |
88 | $ensureValue['already-existed'], |
89 | true |
90 | ) |
91 | ); |
92 | $revisionId = $revisionRecord->getId(); |
93 | $pageId = $revisionRecord->getPageId(); |
94 | $state->logger->debug( |
95 | "ensureFlowRevision revision ID: $revisionId, page ID: $pageId" |
96 | ); |
97 | |
98 | $state->put( $state->boardWorkflow, [] ); |
99 | } else { |
100 | throw new ImportException( "ensureFlowRevision failed to create the Flow board" ); |
101 | } |
102 | } |
103 | |
104 | $imported = $failed = 0; |
105 | $header = $this->importSource->getHeader(); |
106 | try { |
107 | $state->begin(); |
108 | $this->importHeader( $state, $header ); |
109 | $state->commit(); |
110 | $state->postprocessor->afterHeaderImported( $state, $header ); |
111 | $imported++; |
112 | } catch ( ImportSourceStoreException $e ) { |
113 | // errors from the source store are more serious and should |
114 | // not just be logged and swallowed. This may indicate that |
115 | // we are not properly recording progress. |
116 | $state->rollback(); |
117 | throw $e; |
118 | } catch ( \Exception $e ) { |
119 | $state->rollback(); |
120 | MWExceptionHandler::logException( $e ); |
121 | $state->logger->error( 'Failed importing header: ' . $header->getObjectKey() ); |
122 | $state->logger->error( (string)$e ); |
123 | $failed++; |
124 | } |
125 | |
126 | foreach ( $this->importSource->getTopics() as $topic ) { |
127 | try { |
128 | // @todo this may be too large of a chunk for one commit, unsure |
129 | $state->begin(); |
130 | $topicState = $this->getTopicState( $state, $topic ); |
131 | $this->importTopic( $topicState, $topic ); |
132 | $state->commit(); |
133 | $state->postprocessor->afterTopicImported( $topicState, $topic ); |
134 | $state->clearManagerGroup(); |
135 | |
136 | $imported++; |
137 | } catch ( ImportSourceStoreException $e ) { |
138 | // errors from the source store are more serious and shuld |
139 | // not juts be logged and swallowed. This may indicate that |
140 | // we are not properly recording progress. |
141 | $state->rollback(); |
142 | throw $e; |
143 | } catch ( \Exception $e ) { |
144 | $state->rollback(); |
145 | MWExceptionHandler::logException( $e ); |
146 | $state->logger->error( 'Failed importing topic: ' . $topic->getObjectKey() ); |
147 | $state->logger->error( (string)$e ); |
148 | $failed++; |
149 | } |
150 | } |
151 | $state->logger->info( "Imported $imported items, failed $failed" ); |
152 | |
153 | return $failed === 0; |
154 | } |
155 | |
156 | /** |
157 | * @param PageImportState $pageState |
158 | * @param IImportHeader $importHeader |
159 | */ |
160 | public function importHeader( PageImportState $pageState, IImportHeader $importHeader ) { |
161 | $pageState->logger->info( 'Importing header' ); |
162 | if ( !$importHeader->getRevisions()->valid() ) { |
163 | $pageState->logger->info( 'no revisions located for header' ); |
164 | |
165 | // No revisions |
166 | return; |
167 | } |
168 | |
169 | /* |
170 | * We don't need $pageState->getImportedId( $importHeader ) here, there |
171 | * can only be 1 header per workflow and we already know the workflow, |
172 | * might as well query it from the workflow instead of using the id from |
173 | * the source store. |
174 | * reason I prefer not to use source store is that a header import is |
175 | * incomplete (it doesn't import full history, just the last revision. |
176 | */ |
177 | $existingId = $pageState->boardWorkflow->getId(); |
178 | if ( $existingId && $pageState->getTopRevision( 'Header', $existingId ) ) { |
179 | $pageState->logger->info( 'header previously imported' ); |
180 | |
181 | return; |
182 | } |
183 | |
184 | $revisions = $this->importObjectWithHistory( |
185 | $importHeader, |
186 | static function ( IObjectRevision $rev ) use ( $pageState ) { |
187 | return Header::create( |
188 | $pageState->boardWorkflow, |
189 | $pageState->createUser( $rev->getAuthor() ), |
190 | $rev->getText(), |
191 | 'wikitext', |
192 | 'create-header' |
193 | ); |
194 | }, |
195 | 'edit-header', |
196 | $pageState, |
197 | $pageState->boardWorkflow->getArticleTitle() |
198 | ); |
199 | |
200 | $pageState->put( |
201 | $revisions, |
202 | [ |
203 | 'workflow' => $pageState->boardWorkflow, |
204 | ] |
205 | ); |
206 | $pageState->recordAssociation( |
207 | reset( $revisions )->getCollectionId(), |
208 | $importHeader |
209 | ); |
210 | |
211 | $pageState->logger->info( 'Imported ' . count( $revisions ) . ' revisions for header' ); |
212 | } |
213 | |
214 | /** |
215 | * @param TopicImportState $topicState |
216 | * @param IImportTopic $importTopic |
217 | */ |
218 | public function importTopic( TopicImportState $topicState, IImportTopic $importTopic ) { |
219 | $summary = $importTopic->getTopicSummary(); |
220 | if ( $summary ) { |
221 | $this->importSummary( $topicState, $summary ); |
222 | } |
223 | |
224 | foreach ( $importTopic->getReplies() as $post ) { |
225 | $this->importPost( $topicState, $post, $topicState->topicTitle ); |
226 | } |
227 | |
228 | $topicState->commitLastUpdated(); |
229 | $topicState->parent->logger->info( "Finished importing topic" ); |
230 | } |
231 | |
232 | /** |
233 | * @param PageImportState $state |
234 | * @param IImportTopic $importTopic |
235 | * @return TopicImportState |
236 | */ |
237 | protected function getTopicState( PageImportState $state, IImportTopic $importTopic ) { |
238 | // Check if it's already been imported |
239 | $topicState = $this->getExistingTopicState( $state, $importTopic ); |
240 | if ( $topicState ) { |
241 | $state->logger->info( |
242 | 'Continuing import to ' . $topicState->topicWorkflow->getArticleTitle()->getPrefixedText() |
243 | ); |
244 | |
245 | return $topicState; |
246 | } else { |
247 | return $this->createTopicState( $state, $importTopic ); |
248 | } |
249 | } |
250 | |
251 | protected function getFirstRevision( IRevisionableObject $obj ) { |
252 | $iterator = $obj->getRevisions(); |
253 | $iterator->rewind(); |
254 | |
255 | return $iterator->current(); |
256 | } |
257 | |
258 | /** |
259 | * @param PageImportState $state |
260 | * @param IImportTopic $importTopic |
261 | * @return TopicImportState |
262 | */ |
263 | protected function createTopicState( PageImportState $state, IImportTopic $importTopic ) { |
264 | $state->logger->info( 'Importing new topic' ); |
265 | $topicWorkflow = Workflow::create( |
266 | 'topic', |
267 | $state->boardWorkflow->getArticleTitle() |
268 | ); |
269 | $state->setWorkflowTimestamp( |
270 | $topicWorkflow, |
271 | $this->getFirstRevision( $importTopic )->getTimestamp() |
272 | ); |
273 | |
274 | $topicListEntry = TopicListEntry::create( |
275 | $state->boardWorkflow, |
276 | $topicWorkflow |
277 | ); |
278 | |
279 | $titleRevisions = $this->importObjectWithHistory( |
280 | $importTopic, |
281 | static function ( IObjectRevision $rev ) use ( $state, $topicWorkflow ) { |
282 | return PostRevision::createTopicPost( |
283 | $topicWorkflow, |
284 | $state->createUser( $rev->getAuthor() ), |
285 | $rev->getText() |
286 | ); |
287 | }, |
288 | 'edit-title', |
289 | $state, |
290 | $topicWorkflow->getArticleTitle() |
291 | ); |
292 | |
293 | // @phan-suppress-next-line PhanTypeMismatchArgumentSuperType |
294 | $topicState = new TopicImportState( $state, $topicWorkflow, end( $titleRevisions ) ); |
295 | $topicMetadata = $topicState->getMetadata(); |
296 | |
297 | // This should all match the order in TopicListBlock->commit (board/ |
298 | // discussion workflow is inserted before this method is called). |
299 | |
300 | $state->put( $topicWorkflow, $topicMetadata ); |
301 | // TLE must be before topic title, otherwise you get an error importing the Topic Title |
302 | // Flow/includes/Data/Index/BoardHistoryIndex.php: |
303 | // No topic list contains topic XXX, called for revision YYY |
304 | $state->put( $topicListEntry, $topicMetadata ); |
305 | $state->put( $titleRevisions, $topicMetadata ); |
306 | |
307 | $state->recordAssociation( $topicWorkflow->getId(), $importTopic ); |
308 | |
309 | $state->logger->info( |
310 | 'Finished importing topic title with ' . count( $titleRevisions ) . ' revisions' |
311 | ); |
312 | |
313 | return $topicState; |
314 | } |
315 | |
316 | /** |
317 | * @param PageImportState $state |
318 | * @param IImportTopic $importTopic |
319 | * @return TopicImportState|null |
320 | */ |
321 | protected function getExistingTopicState( PageImportState $state, IImportTopic $importTopic ) { |
322 | $topicId = $state->getImportedId( $importTopic ); |
323 | if ( $topicId ) { |
324 | $topicWorkflow = $state->get( 'Workflow', $topicId ); |
325 | $topicTitle = $state->getTopRevision( 'PostRevision', $topicId ); |
326 | if ( $topicWorkflow instanceof Workflow && $topicTitle instanceof PostRevision ) { |
327 | return new TopicImportState( $state, $topicWorkflow, $topicTitle ); |
328 | } |
329 | } |
330 | |
331 | return null; |
332 | } |
333 | |
334 | /** |
335 | * @param TopicImportState $state |
336 | * @param IImportSummary $importSummary |
337 | */ |
338 | public function importSummary( TopicImportState $state, IImportSummary $importSummary ) { |
339 | $state->parent->logger->info( "Importing summary" ); |
340 | $existingId = $state->parent->getImportedId( $importSummary ); |
341 | if ( $existingId ) { |
342 | $summary = $state->parent->getTopRevision( 'PostSummary', $existingId ); |
343 | if ( $summary ) { |
344 | $state->recordUpdateTime( $summary->getRevisionId() ); |
345 | $state->parent->logger->info( "Summary previously imported" ); |
346 | |
347 | return; |
348 | } |
349 | } |
350 | |
351 | $revisions = $this->importObjectWithHistory( |
352 | $importSummary, |
353 | static function ( IObjectRevision $rev ) use ( $state ) { |
354 | return PostSummary::create( |
355 | $state->topicWorkflow->getArticleTitle(), |
356 | $state->topicTitle, |
357 | $state->parent->createUser( $rev->getAuthor() ), |
358 | $rev->getText(), |
359 | 'wikitext', |
360 | 'create-topic-summary' |
361 | ); |
362 | }, |
363 | 'edit-topic-summary', |
364 | $state->parent, |
365 | $state->topicWorkflow->getArticleTitle() |
366 | ); |
367 | |
368 | $metadata = [ |
369 | 'workflow' => $state->topicWorkflow, |
370 | ]; |
371 | $state->parent->put( $revisions, $metadata ); |
372 | $state->parent->recordAssociation( |
373 | reset( $revisions )->getCollectionId(), // Summary ID |
374 | $importSummary |
375 | ); |
376 | |
377 | $state->recordUpdateTime( end( $revisions )->getRevisionId() ); |
378 | $state->parent->logger->info( |
379 | "Finished importing summary with " . count( $revisions ) . " revisions" |
380 | ); |
381 | } |
382 | |
383 | /** |
384 | * @param TopicImportState $state |
385 | * @param IImportPost $post |
386 | * @param PostRevision $replyTo |
387 | * @param string $logPrefix |
388 | * @suppress PhanTypeMismatchArgument,PhanUndeclaredMethod |
389 | */ |
390 | public function importPost( TopicImportState $state, IImportPost $post, PostRevision $replyTo, $logPrefix = '' ) { |
391 | $state->parent->logger->info( $logPrefix . "Importing post" ); |
392 | $postId = $state->parent->getImportedId( $post ); |
393 | $topRevision = false; |
394 | if ( $postId ) { |
395 | $topRevision = $state->parent->getTopRevision( 'PostRevision', $postId ); |
396 | } |
397 | |
398 | if ( $topRevision ) { |
399 | $state->parent->logger->info( $logPrefix . "Post previously imported" ); |
400 | } else { |
401 | $replyRevisions = $this->importObjectWithHistory( |
402 | $post, |
403 | static function ( IObjectRevision $rev ) use ( $replyTo, $state ) { |
404 | return $replyTo->reply( |
405 | $state->topicWorkflow, |
406 | $state->parent->createUser( $rev->getAuthor() ), |
407 | $rev->getText(), |
408 | 'wikitext' |
409 | ); |
410 | }, |
411 | 'edit-post', |
412 | $state->parent, |
413 | $state->topicWorkflow->getArticleTitle() |
414 | ); |
415 | |
416 | $topRevision = end( $replyRevisions ); |
417 | |
418 | $metadata = [ |
419 | 'workflow' => $state->topicWorkflow, |
420 | 'board-workflow' => $state->parent->boardWorkflow, |
421 | 'topic-title' => $state->topicTitle, |
422 | 'reply-to' => $replyTo, |
423 | ]; |
424 | |
425 | $state->parent->put( $replyRevisions, $metadata ); |
426 | $state->parent->recordAssociation( |
427 | $topRevision->getPostId(), |
428 | $post |
429 | ); |
430 | $state->parent->logger->info( |
431 | $logPrefix . "Finished importing post with " . count( |
432 | $replyRevisions |
433 | ) . " revisions" |
434 | ); |
435 | $state->parent->postprocessor->afterPostImported( $state, $post, $topRevision ); |
436 | } |
437 | |
438 | $state->recordUpdateTime( $topRevision->getRevisionId() ); |
439 | |
440 | foreach ( $post->getReplies() as $subReply ) { |
441 | $this->importPost( $state, $subReply, $topRevision, $logPrefix . ' ' ); |
442 | } |
443 | } |
444 | |
445 | /** |
446 | * Imports an object with all its revisions |
447 | * |
448 | * @param IRevisionableObject $object Object to import. |
449 | * @param callable $importFirstRevision Function which, given the appropriate import revision, |
450 | * creates the Flow revision. |
451 | * @param string $editChangeType The Flow change type (from FlowActions.php) for each new operation. |
452 | * @param PageImportState $state State of the import operation. |
453 | * @param Title $title Title content is rendered against |
454 | * @return AbstractRevision[] Objects to insert into the database. |
455 | * @throws ImportException |
456 | */ |
457 | public function importObjectWithHistory( |
458 | IRevisionableObject $object, |
459 | $importFirstRevision, |
460 | $editChangeType, |
461 | PageImportState $state, |
462 | Title $title |
463 | ) { |
464 | $insertObjects = []; |
465 | $revisions = $object->getRevisions(); |
466 | $revisions->rewind(); |
467 | |
468 | if ( !$revisions->valid() ) { |
469 | throw new ImportException( "Attempted to import empty history" ); |
470 | } |
471 | |
472 | $importRevision = $revisions->current(); |
473 | /** @var AbstractRevision $lastRevision */ |
474 | $insertObjects[] = $lastRevision = $importFirstRevision( $importRevision ); |
475 | $lastTimestamp = $importRevision->getTimestamp(); |
476 | |
477 | $state->setRevisionTimestamp( $lastRevision, $lastTimestamp ); |
478 | $state->recordAssociation( $lastRevision->getRevisionId(), $importRevision ); |
479 | $state->recordAssociation( $lastRevision->getCollectionId(), $importRevision ); |
480 | |
481 | $revisions->next(); |
482 | while ( $revisions->valid() ) { |
483 | $importRevision = $revisions->current(); |
484 | $insertObjects[] = $lastRevision = $lastRevision->newNextRevision( |
485 | $state->createUser( $importRevision->getAuthor() ), |
486 | $importRevision->getText(), |
487 | 'wikitext', |
488 | $editChangeType, |
489 | $title |
490 | ); |
491 | |
492 | $importTimestampObj = new MWTimestamp( $importRevision->getTimestamp() ); |
493 | $lastTimestampObj = new MWTimestamp( $lastTimestamp ); |
494 | $timeDiff = $lastTimestampObj->diff( $importTimestampObj ); |
495 | // If $import - last < 0 |
496 | if ( $timeDiff->invert ) { |
497 | throw new ImportException( "Revision listing is not sorted from oldest to newest" ); |
498 | } |
499 | |
500 | $lastTimestamp = $importRevision->getTimestamp(); |
501 | $state->setRevisionTimestamp( $lastRevision, $lastTimestamp ); |
502 | $state->recordAssociation( $lastRevision->getRevisionId(), $importRevision ); |
503 | $revisions->next(); |
504 | } |
505 | |
506 | return $insertObjects; |
507 | } |
508 | } |