| Current File : /home/jvzmxxx/wiki1/extensions/Flow/includes/Import/Importer.php |
<?php
namespace Flow\Import;
use Article;
use DeferredUpdates;
use Flow\Data\BufferedCache;
use Flow\Data\ManagerGroup;
use Flow\DbFactory;
use Flow\Import\Postprocessor\Postprocessor;
use Flow\Import\Postprocessor\ProcessorGroup;
use Flow\Import\SourceStore\SourceStoreInterface as ImportSourceStore;
use Flow\Import\SourceStore\Exception as ImportSourceStoreException;
use Flow\Model\AbstractRevision;
use Flow\Model\Header;
use Flow\Model\PostRevision;
use Flow\Model\PostSummary;
use Flow\Model\TopicListEntry;
use Flow\Model\UUID;
use Flow\Model\Workflow;
use Flow\OccupationController;
use Flow\WorkflowLoaderFactory;
use IP;
use MWCryptRand;
use MWTimestamp;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use ReflectionProperty;
use SplQueue;
use Title;
use UIDGenerator;
use User;
/**
* The import system uses a TalkpageImportOperation class.
* This class is essentially a factory class that makes the
* dependency injection less inconvenient for callers.
*/
class Importer {
/** @var ManagerGroup **/
protected $storage;
/** @var WorkflowLoaderFactory **/
protected $workflowLoaderFactory;
/** @var LoggerInterface|null */
protected $logger;
/** @var BufferedCache */
protected $cache;
/** @var DbFactory */
protected $dbFactory;
/** @var bool */
protected $allowUnknownUsernames;
/** @var ProcessorGroup **/
protected $postprocessors;
/** @var SplQueue Callbacks for DeferredUpdate that are queue'd up by the commit process */
protected $deferredQueue;
/** @var OccupationController */
protected $occupationController;
public function __construct(
ManagerGroup $storage,
WorkflowLoaderFactory $workflowLoaderFactory,
BufferedCache $cache,
DbFactory $dbFactory,
SplQueue $deferredQueue,
OccupationController $occupationController
) {
$this->storage = $storage;
$this->workflowLoaderFactory = $workflowLoaderFactory;
$this->cache = $cache;
$this->dbFactory = $dbFactory;
$this->postprocessors = new ProcessorGroup;
$this->deferredQueue = $deferredQueue;
$this->occupationController = $occupationController;
}
public function addPostprocessor( Postprocessor $proc ) {
$this->postprocessors->add( $proc );
}
/**
* Returns the ProcessorGroup (calling this triggers all the postprocessors
*
* @return Postprocessor
*/
public function getPostprocessor() {
return $this->postprocessors;
}
/**
* @param LoggerInterface $logger
*/
public function setLogger( LoggerInterface $logger ) {
$this->logger = $logger;
}
/**
* @param bool $allowed When true allow usernames that do not exist on the wiki to be
* stored in the _ip field. *DO*NOT*USE* in any production setting, this is
* to allow for imports from production wiki api's to test machines for
* development purposes.
*/
public function setAllowUnknownUsernames( $allowed ) {
$this->allowUnknownUsernames = (bool)$allowed;
}
/**
* Imports topics from a data source to a given page.
*
* @param IImportSource $source
* @param Title $targetPage
* @param ImportSourceStore $sourceStore
* @return bool True When the import completes with no failures
*/
public function import( IImportSource $source, Title $targetPage, ImportSourceStore $sourceStore ) {
$operation = new TalkpageImportOperation( $source, $this->occupationController );
$pageImportState = new PageImportState(
$this->workflowLoaderFactory
->createWorkflowLoader( $targetPage )
->getWorkflow(),
$this->storage,
$sourceStore,
$this->logger ?: new NullLogger,
$this->cache,
$this->dbFactory,
$this->postprocessors,
$this->deferredQueue,
$this->allowUnknownUsernames
);
return $operation->import( $pageImportState );
}
}
/**
* Modified version of UIDGenerator generates historical timestamped
* uid's for use when importing older data.
*
* DO NOT USE for normal UID generation, this is likely to run into
* id collisions.
*
* The import process needs to identify collision failures reported by
* the database and re-try importing that item with another generated
* uid.
*/
class HistoricalUIDGenerator extends UIDGenerator {
public static function historicalTimestampedUID88( $timestamp, $base = 10 ) {
$COUNTER_MAX = 1023; // 2^10 - 1
static $counter = false;
if ( $counter === false ) {
$counter = mt_rand( 0, $COUNTER_MAX );
}
$time = array(
// seconds
wfTimestamp( TS_UNIX, $timestamp ),
// milliseconds
mt_rand( 0, 999 )
);
// The UIDGenerator is implemented very specifically to have
// a single instance, we have to reuse that instance.
$gen = self::singleton();
self::rotateNodeId( $gen );
$binaryUUID = $gen->getTimestampedID88(
array( $time, ++$counter % ( $COUNTER_MAX + 1) )
);
return \Wikimedia\base_convert( $binaryUUID, 2, $base );
}
/**
* Rotate the nodeId to a random one. The stable node is best for
* generating "now" uid's on a cluster of servers, but repeated
* creation of historical uid's with one or a smaller number of
* machines requires use of a random node id.
*
* @param UIDGenerator $gen
*/
protected static function rotateNodeId( UIDGenerator $gen ) {
// 4 bytes = 32 bits
$gen->nodeId32 = \Wikimedia\base_convert( MWCryptRand::generateHex( 8, true ), 16, 2, 32 );
// 6 bytes = 48 bits, used for 128bit uid's
//$gen->nodeId48 = \Wikimedia\base_convert( MWCryptRand::generateHex( 12, true ), 16, 2, 48 );
}
}
class PageImportState {
/**
* @var LoggerInterface
*/
public $logger;
/**
* @var Workflow
*/
public $boardWorkflow;
/**
* @var ManagerGroup
*/
protected $storage;
/**
* @var ReflectionProperty
*/
protected $workflowIdProperty;
/**
* @var ReflectionProperty[]
*/
protected $postIdProperty;
/**
* @var ReflectionProperty[]
*/
protected $revIdProperty;
/**
* @var ReflectionProperty[]
*/
protected $lastEditIdProperty;
/**
* @var bool
*/
protected $allowUnknownUsernames;
/**
* @var Postprocessor
*/
public $postprocessor;
/**
* @var SplQueue
*/
protected $deferredQueue;
public function __construct(
Workflow $boardWorkflow,
ManagerGroup $storage,
ImportSourceStore $sourceStore,
LoggerInterface $logger,
BufferedCache $cache,
DbFactory $dbFactory,
Postprocessor $postprocessor,
SplQueue $deferredQueue,
$allowUnknownUsernames = false
) {
$this->storage = $storage;;
$this->boardWorkflow = $boardWorkflow;
$this->sourceStore = $sourceStore;
$this->logger = $logger;
$this->cache = $cache;
$this->dbw = $dbFactory->getDB( DB_MASTER );
$this->postprocessor = $postprocessor;
$this->deferredQueue = $deferredQueue;
$this->allowUnknownUsernames = $allowUnknownUsernames;
// Get our workflow UUID property
$this->workflowIdProperty = new ReflectionProperty( 'Flow\\Model\\Workflow', 'id' );
$this->workflowIdProperty->setAccessible( true );
// Get our revision UUID properties
$this->postIdProperty = new ReflectionProperty( 'Flow\\Model\\PostRevision', 'postId' );
$this->postIdProperty->setAccessible( true );
$this->revIdProperty = new ReflectionProperty( 'Flow\\Model\\AbstractRevision', 'revId' );
$this->revIdProperty->setAccessible( true );
$this->lastEditIdProperty = new ReflectionProperty( 'Flow\\Model\\AbstractRevision', 'lastEditId' );
$this->lastEditIdProperty->setAccessible( true );
}
/**
* @param object|object[] $object
* @param array $metadata
*/
public function put( $object, array $metadata ) {
$metadata['imported'] = true;
if ( is_array( $object ) ) {
$this->storage->multiPut( $object, $metadata );
} else {
$this->storage->put( $object, $metadata );
}
}
/**
* Gets the given object from storage
*
* WARNING: Before calling this method, ensure that you follow the rule
* given in clearManagerGroup.
*
* @param string $type Class name to retrieve
* @param UUID $id ID of the object to retrieve
* @return Object|false
*/
public function get( $type, UUID $id ) {
return $this->storage->get( $type, $id );
}
/**
* Clears information about which objects are loaded, to avoid memory leaks.
* This will also:
* * Clear the mapper associated with each ObjectManager that has been used.
* * Trigger onAfterClear on any listeners.
*
* WARNING: You can *NOT* call ->get before calling clearManagerGroup, then ->put
* after calling clearManagerGroup, on the same object. This will cause a
* duplicate object to be inserted.
*/
public function clearManagerGroup() {
$this->storage->clear();
}
/**
* Gets the top revision of an item by ID
*
* @param string $type The type of the object to return (e.g. PostRevision).
* @param UUID $id The ID (e.g. post ID, topic ID, etc)
* @return object|false The top revision of the requested object, or false if not found.
*/
public function getTopRevision( $type, UUID $id ) {
$result = $this->storage->find(
$type,
array( 'rev_type_id' => $id ),
array( 'sort' => 'rev_id', 'order' => 'DESC', 'limit' => 1 )
);
if ( count( $result ) ) {
return reset( $result );
} else {
return false;
}
}
/**
* Creates a UUID object representing a given timestamp.
*
* @param string $timestamp The timestamp to represent, in a wfTimestamp compatible format.
* @return UUID
*/
public function getTimestampId( $timestamp ) {
return UUID::create( HistoricalUIDGenerator::historicalTimestampedUID88( $timestamp ) );
}
/**
* Update the id of the workflow to match the provided timestamp
*
* @param Workflow $workflow
* @param string $timestamp
*/
public function setWorkflowTimestamp( Workflow $workflow, $timestamp ) {
$uid = $this->getTimestampId( $timestamp );
$this->workflowIdProperty->setValue( $workflow, $uid );
}
/**
* @var AbstractRevision $summary
* @var string $timestamp
*/
public function setRevisionTimestamp( AbstractRevision $revision, $timestamp ) {
$uid = $this->getTimestampId( $timestamp );
// We don't set the topic title postId as it was inherited from the workflow. We only set the
// postId for first revisions because further revisions inherit it from the parent which was
// set appropriately.
if ( $revision instanceof PostRevision && $revision->isFirstRevision() && !$revision->isTopicTitle() ) {
$this->postIdProperty->setValue( $revision, $uid );
}
if ( $revision->getRevisionId()->equals( $revision->getLastContentEditId() ) ) {
$this->lastEditIdProperty->setValue( $revision, $uid );
}
$this->revIdProperty->setValue( $revision, $uid );
}
/**
* Records an association between a created object and its source.
*
* @param UUID $objectId UUID representing the object that was created.
* @param IImportObject $object Output from getObjectKey
*/
public function recordAssociation( UUID $objectId, IImportObject $object ) {
$this->sourceStore->setAssociation( $objectId, $object->getObjectKey() );
}
/**
* Gets the imported ID for a given object, if any.
*
* @param IImportObject $object
* @return UUID|false
*/
public function getImportedId( IImportObject $object ) {
return $this->sourceStore->getImportedId( $object );
}
public function createUser( $name ) {
if ( IP::isIPAddress( $name ) ) {
return User::newFromName( $name, false );
}
$user = User::newFromName( $name );
if ( !$user ) {
throw new ImportException( 'Unable to create user: ' . $name );
}
if ( $user->getId() == 0 && !$this->allowUnknownUsernames ) {
throw new ImportException( 'User does not exist: ' . $name );
}
return $user;
}
public function begin() {
$this->flushDeferredQueue();
$this->dbw->begin( __METHOD__ );
$this->cache->begin();
}
public function commit() {
$this->dbw->commit( __METHOD__ );
$this->cache->commit();
$this->sourceStore->save();
$this->flushDeferredQueue();
}
public function rollback() {
$this->dbw->rollback( __METHOD__ );
$this->cache->rollback();
$this->sourceStore->rollback();
$this->clearDeferredQueue();
$this->postprocessor->importAborted();
}
protected function flushDeferredQueue() {
while ( !$this->deferredQueue->isEmpty() ) {
DeferredUpdates::addCallableUpdate(
$this->deferredQueue->dequeue(),
DeferredUpdates::PRESEND
);
DeferredUpdates::tryOpportunisticExecute();
}
}
protected function clearDeferredQueue() {
while ( !$this->deferredQueue->isEmpty() ) {
$this->deferredQueue->dequeue();
}
}
}
class TopicImportState {
/**
* @var PageImportState
*/
public $parent;
/**
* @var Workflow
*/
public $topicWorkflow;
/**
* @var PostRevision
*/
public $topicTitle;
/**
* @var string
*/
protected $lastUpdated;
public function __construct(
PageImportState $parent,
Workflow $topicWorkflow,
PostRevision $topicTitle
) {
$this->parent = $parent;
$this->topicWorkflow = $topicWorkflow;
$this->topicTitle = $topicTitle;
$this->workflowUpdatedProperty = new ReflectionProperty( 'Flow\\Model\\Workflow', 'lastUpdated' );
$this->workflowUpdatedProperty->setAccessible( true );
$this->lastUpdated = '';
$this->recordUpdateTime( $topicWorkflow->getId() );
}
public function getMetadata() {
return array(
'workflow' => $this->topicWorkflow,
'board-workflow' => $this->parent->boardWorkflow,
'topic-title' => $this->topicTitle,
);
}
/**
* Notify the state about a modification action at a given time.
*
* @param UUID $uuid UUID of the modification revision.
*/
public function recordUpdateTime( UUID $uuid ) {
$timestamp = $uuid->getTimestamp();
$timestamp = wfTimestamp( TS_MW, $timestamp );
if ( $timestamp > $this->lastUpdated ) {
$this->lastUpdated = $timestamp;
}
}
/**
* Saves the last updated timestamp based on calls to recordUpdateTime
* XXX: Kind of icky; reaching through the parent and doing a second put().
*/
public function commitLastUpdated() {
$this->workflowUpdatedProperty->setValue(
$this->topicWorkflow,
$this->lastUpdated
);
$this->parent->put( $this->topicWorkflow, $this->getMetadata() );
}
}
class TalkpageImportOperation {
/**
* @var IImportSource
*/
protected $importSource;
/** @var OccupationController */
protected $occupationController;
/**
* @param IImportSource $source
*/
public function __construct( IImportSource $source, OccupationController $occupationController ) {
$this->importSource = $source;
$this->occupationController = $occupationController;
}
/**
* @param PageImportState $state
* @return bool True if import completed successfully
* @throws ImportSourceStoreException
* @throws \Exception
*/
public function import( PageImportState $state ) {
$destinationTitle = $state->boardWorkflow->getArticleTitle();
$state->logger->info( 'Importing to ' . $destinationTitle->getPrefixedText() );
$isNew = $state->boardWorkflow->isNew();
$state->logger->debug( 'Workflow isNew: ' . var_export( $isNew, true ) );
if ( $isNew ) {
// Explicitly allow creation of board
$creationStatus = $this->occupationController->safeAllowCreation(
$destinationTitle,
$this->occupationController->getTalkpageManager(),
/* $mustNotExist = */ true
);
if ( !$creationStatus->isGood() ) {
throw new ImportException( "safeAllowCreation failed to allow the import destination, with the following error:\n" . $creationStatus->getWikiText() );
}
// Makes sure the page exists and a Flow-specific revision has been inserted
$status = $this->occupationController->ensureFlowRevision(
new Article( $destinationTitle ),
$state->boardWorkflow
);
$state->logger->debug( 'ensureFlowRevision status isOK: ' . var_export( $status->isOK(), true ) );
$state->logger->debug( 'ensureFlowRevision status isGood: ' . var_export( $status->isGood(), true ) );
if ( $status->isOK() ) {
$ensureValue = $status->getValue();
$revision = $ensureValue['revision'];
$state->logger->debug( 'ensureFlowRevision already-existed: ' . var_export( $ensureValue['already-existed'], true ) );
$revisionId = $revision->getId();
$pageId = $revision->getTitle()->getArticleId( Title::GAID_FOR_UPDATE );
$state->logger->debug( "ensureFlowRevision revision ID: $revisionId, page ID: $pageId" );
$state->put( $state->boardWorkflow, array() );
} else {
throw new ImportException( "ensureFlowRevision failed to create the Flow board" );
}
}
$imported = $failed = 0;
$header = $this->importSource->getHeader();
try {
$state->begin();
$this->importHeader( $state, $header );
$state->commit();
$state->postprocessor->afterHeaderImported( $state, $header );
$imported++;
} catch ( ImportSourceStoreException $e ) {
// errors from the source store are more serious and should
// not just be logged and swallowed. This may indicate that
// we are not properly recording progress.
$state->rollback();
throw $e;
} catch ( \Exception $e ) {
$state->rollback();
\MWExceptionHandler::logException( $e );
$state->logger->error( 'Failed importing header: ' . $header->getObjectKey() );
$state->logger->error( (string)$e );
$failed++;
}
foreach( $this->importSource->getTopics() as $topic ) {
try {
// @todo this may be too large of a chunk for one commit, unsure
$state->begin();
$topicState = $this->getTopicState( $state, $topic );
$this->importTopic( $topicState, $topic );
$state->commit();
$state->postprocessor->afterTopicImported( $topicState, $topic );
$state->clearManagerGroup();
$imported++;
} catch ( ImportSourceStoreException $e ) {
// errors from the source store are more serious and shuld
// not juts be logged and swallowed. This may indicate that
// we are not properly recording progress.
$state->rollback();
throw $e;
} catch ( \Exception $e ) {
$state->rollback();
\MWExceptionHandler::logException( $e );
$state->logger->error( 'Failed importing topic: ' . $topic->getObjectKey() );
$state->logger->error( (string)$e );
$failed++;
}
}
$state->logger->info( "Imported $imported items, failed $failed" );
return $failed === 0;
}
/**
* @param PageImportState $pageState
* @param IImportHeader $importHeader
*/
public function importHeader( PageImportState $pageState, IImportHeader $importHeader ) {
$pageState->logger->info( 'Importing header' );
if ( ! $importHeader->getRevisions()->valid() ) {
$pageState->logger->info( 'no revisions located for header' );
// No revisions
return;
}
/*
* We don't need $pageState->getImportedId( $importHeader ) here, there
* can only be 1 header per workflow and we already know the workflow,
* might as well query it from the workflow instead of using the id from
* the source store.
* reason I prefer not to use source store is that a header import is
* incomplete (it doesn't import full history, just the last revision.
*/
$existingId = $pageState->boardWorkflow->getId();
if ( $existingId && $pageState->getTopRevision( 'Header', $existingId ) ) {
$pageState->logger->info( 'header previously imported' );
return;
}
$revisions = $this->importObjectWithHistory(
$importHeader,
function( IObjectRevision $rev ) use ( $pageState ) {
return Header::create(
$pageState->boardWorkflow,
$pageState->createUser( $rev->getAuthor() ),
$rev->getText(),
'wikitext',
'create-header'
);
},
'edit-header',
$pageState,
$pageState->boardWorkflow->getArticleTitle()
);
$pageState->put( $revisions, array(
'workflow' => $pageState->boardWorkflow,
) );
$pageState->recordAssociation(
reset( $revisions )->getCollectionId(),
$importHeader
);
$pageState->logger->info( 'Imported ' . count( $revisions ) . ' revisions for header' );
}
/**
* @param TopicImportState $topicState
* @param IImportTopic $importTopic
*/
public function importTopic( TopicImportState $topicState, IImportTopic $importTopic ) {
$summary = $importTopic->getTopicSummary();
if ( $summary ) {
$this->importSummary( $topicState, $summary );
}
foreach ( $importTopic->getReplies() as $post ) {
$this->importPost( $topicState, $post, $topicState->topicTitle );
}
$topicState->commitLastUpdated();
$topicState->parent->logger->info( "Finished importing topic" );
}
/**
* @param PageImportState $state
* @param IImportTopic $importTopic
* @return TopicImportState
*/
protected function getTopicState( PageImportState $state, IImportTopic $importTopic ) {
// Check if it's already been imported
$topicState = $this->getExistingTopicState( $state, $importTopic );
if ( $topicState ) {
$state->logger->info( 'Continuing import to ' . $topicState->topicWorkflow->getArticleTitle()->getPrefixedText() );
return $topicState;
} else {
return $this->createTopicState( $state, $importTopic );
}
}
protected function getFirstRevision( IRevisionableObject $obj ) {
$iterator = $obj->getRevisions();
$iterator->rewind();
return $iterator->current();
}
/**
* @param PageImportState $state
* @param IImportTopic $importTopic
* @return TopicImportState
*/
protected function createTopicState( PageImportState $state, IImportTopic $importTopic ) {
$state->logger->info( 'Importing new topic' );
$topicWorkflow = Workflow::create(
'topic',
$state->boardWorkflow->getArticleTitle()
);
$state->setWorkflowTimestamp(
$topicWorkflow,
$this->getFirstRevision( $importTopic )->getTimestamp()
);
$topicListEntry = TopicListEntry::create(
$state->boardWorkflow,
$topicWorkflow
);
$titleRevisions = $this->importObjectWithHistory(
$importTopic,
function( IObjectRevision $rev ) use ( $state, $topicWorkflow ) {
return PostRevision::createTopicPost(
$topicWorkflow,
$state->createUser( $rev->getAuthor() ),
$rev->getText()
);
},
'edit-title',
$state,
$topicWorkflow->getArticleTitle()
);
$topicState = new TopicImportState( $state, $topicWorkflow, end( $titleRevisions ) );
$topicMetadata = $topicState->getMetadata();
// This should all match the order in TopicListBlock->commit (board/
// discussion workflow is inserted before this method is called).
$state->put( $topicWorkflow, $topicMetadata );
// TLE must be before topic title, otherwise you get an error importing the Topic Title
// Flow/includes/Data/Index/BoardHistoryIndex.php:
// No topic list contains topic XXX, called for revision YYY
$state->put( $topicListEntry, $topicMetadata );
$state->put( $titleRevisions, $topicMetadata );
$state->recordAssociation( $topicWorkflow->getId(), $importTopic );
$state->logger->info( 'Finished importing topic title with ' . count( $titleRevisions ) . ' revisions' );
return $topicState;
}
/**
* @param PageImportState $state
* @param IImportTopic $importTopic
* @return TopicImportState|null
*/
protected function getExistingTopicState( PageImportState $state, IImportTopic $importTopic ) {
$topicId = $state->getImportedId( $importTopic );
if ( $topicId ) {
$topicWorkflow = $state->get( 'Workflow', $topicId );
$topicTitle = $state->getTopRevision( 'PostRevision', $topicId );
if ( $topicWorkflow instanceof Workflow && $topicTitle instanceof PostRevision ) {
return new TopicImportState( $state, $topicWorkflow, $topicTitle );
}
}
return null;
}
/**
* @param TopicImportState $state
* @param IImportSummary $importSummary
*/
public function importSummary( TopicImportState $state, IImportSummary $importSummary ) {
$state->parent->logger->info( "Importing summary" );
$existingId = $state->parent->getImportedId( $importSummary );
if ( $existingId ) {
$summary = $state->parent->getTopRevision( 'PostSummary', $existingId );
if ( $summary ) {
$state->recordUpdateTime( $summary->getRevisionId() );
$state->parent->logger->info( "Summary previously imported" );
return;
}
}
$revisions = $this->importObjectWithHistory(
$importSummary,
function( IObjectRevision $rev ) use ( $state ) {
return PostSummary::create(
$state->topicWorkflow->getArticleTitle(),
$state->topicTitle,
$state->parent->createUser( $rev->getAuthor() ),
$rev->getText(),
'wikitext',
'create-topic-summary'
);
},
'edit-topic-summary',
$state->parent,
$state->topicWorkflow->getArticleTitle()
);
$metadata = array(
'workflow' => $state->topicWorkflow,
);
$state->parent->put( $revisions, $metadata );
$state->parent->recordAssociation(
reset( $revisions )->getCollectionId(), // Summary ID
$importSummary
);
$state->recordUpdateTime( end( $revisions )->getRevisionId() );
$state->parent->logger->info( "Finished importing summary with " . count( $revisions ) . " revisions" );
}
/**
* @param TopicImportState $state
* @param IImportPost $post
* @param PostRevision $replyTo
* @param string $logPrefix
*/
public function importPost(
TopicImportState $state,
IImportPost $post,
PostRevision $replyTo,
$logPrefix = ''
) {
$state->parent->logger->info( $logPrefix . "Importing post" );
$postId = $state->parent->getImportedId( $post );
$topRevision = false;
if ( $postId ) {
$topRevision = $state->parent->getTopRevision( 'PostRevision', $postId );
}
if ( $topRevision ) {
$state->parent->logger->info( $logPrefix . "Post previously imported" );
} else {
$replyRevisions = $this->importObjectWithHistory(
$post,
function( IObjectRevision $rev ) use ( $replyTo, $state ) {
return $replyTo->reply(
$state->topicWorkflow,
$state->parent->createUser( $rev->getAuthor() ),
$rev->getText(),
'wikitext'
);
},
'edit-post',
$state->parent,
$state->topicWorkflow->getArticleTitle()
);
$topRevision = end( $replyRevisions );
$metadata = array(
'workflow' => $state->topicWorkflow,
'board-workflow' => $state->parent->boardWorkflow,
'topic-title' => $state->topicTitle,
'reply-to' => $replyTo,
);
$state->parent->put( $replyRevisions, $metadata );
$state->parent->recordAssociation(
$topRevision->getPostId(),
$post
);
$state->parent->logger->info( $logPrefix . "Finished importing post with " . count( $replyRevisions ) . " revisions" );
$state->parent->postprocessor->afterPostImported( $state, $post, $topRevision );
}
$state->recordUpdateTime( $topRevision->getRevisionId() );
foreach ( $post->getReplies() as $subReply ) {
$this->importPost( $state, $subReply, $topRevision, $logPrefix . ' ' );
}
}
/**
* Imports an object with all its revisions
*
* @param IRevisionableObject $object Object to import.
* @param callable $importFirstRevision Function which, given the appropriate import revision, creates the Flow revision.
* @param string $editChangeType The Flow change type (from FlowActions.php) for each new operation.
* @param PageImportState $state State of the import operation.
* @param Title $title Title content is rendered against
* @return AbstractRevision[] Objects to insert into the database.
* @throws ImportException
*/
public function importObjectWithHistory(
IRevisionableObject $object,
$importFirstRevision,
$editChangeType,
PageImportState $state,
Title $title
) {
$insertObjects = array();
$revisions = $object->getRevisions();
$revisions->rewind();
if ( !$revisions->valid() ) {
throw new ImportException( "Attempted to import empty history" );
}
$importRevision = $revisions->current();
/** @var AbstractRevision $lastRevision */
$insertObjects[] = $lastRevision = $importFirstRevision( $importRevision );
$lastTimestamp = $importRevision->getTimestamp();
$state->setRevisionTimestamp( $lastRevision, $lastTimestamp );
$state->recordAssociation( $lastRevision->getRevisionId(), $importRevision );
$state->recordAssociation( $lastRevision->getCollectionId(), $importRevision );
$revisions->next();
while( $revisions->valid() ) {
$importRevision = $revisions->current();
$insertObjects[] = $lastRevision =
$lastRevision->newNextRevision(
$state->createUser( $importRevision->getAuthor() ),
$importRevision->getText(),
'wikitext',
$editChangeType,
$title
);
$importTimestampObj = new MWTimestamp( $importRevision->getTimestamp() );
$lastTimestampObj = new MWTimestamp( $lastTimestamp );
$timeDiff = $lastTimestampObj->diff( $importTimestampObj );
// If $import - last < 0
if ( $timeDiff->invert ) {
throw new ImportException( "Revision listing is not sorted from oldest to newest" );
}
$lastTimestamp = $importRevision->getTimestamp();
$state->setRevisionTimestamp( $lastRevision, $lastTimestamp );
$state->recordAssociation( $lastRevision->getRevisionId(), $importRevision );
$revisions->next();
}
return $insertObjects;
}
}