jenkins-bot has submitted this change and it was merged.
Change subject: Import Flow data dump
......................................................................
Import Flow data dump
Meanwhile also changes some Models to allow empty values as well
for nullable columns.
Bug: T114703
Change-Id: Ieae618d4aaa4b21a4eb8fe8d1f0627ee8bd1ff8d
---
M Flow.php
M Hooks.php
M autoload.php
M includes/Data/Listener/ReferenceRecorder.php
A includes/Dump/Importer.php
M includes/Model/AbstractRevision.php
M includes/Model/PostRevision.php
7 files changed, 303 insertions(+), 6 deletions(-)
Approvals:
Mattflaschen: Looks good to me, approved
jenkins-bot: Verified
diff --git a/Flow.php b/Flow.php
index 925b372..000d7be 100644
--- a/Flow.php
+++ b/Flow.php
@@ -154,6 +154,7 @@
$wgHooks['ArticleDeleteComplete'][] = 'FlowHooks::onArticleDeleteComplete';
$wgHooks['ArticleUndelete'][] = 'FlowHooks::onArticleUndelete';
$wgHooks['SearchableNamespaces'][] = 'FlowHooks::onSearchableNamespaces';
+$wgHooks['ImportHandleToplevelXMLTag'][] =
'FlowHooks::onImportHandleToplevelXMLTag';
// Extension:UserMerge support
$wgHooks['UserMergeAccountFields'][] = 'FlowHooks::onUserMergeAccountFields';
diff --git a/Hooks.php b/Hooks.php
index 7fbca28..4aa7289 100644
--- a/Hooks.php
+++ b/Hooks.php
@@ -1768,4 +1768,61 @@
return true;
}
+ /**
+ * @param WikiImporter $importer
+ * @return bool
+ */
+ public static function onImportHandleToplevelXMLTag( WikiImporter
$importer ) {
+ // only init Flow's importer once, then re-use it
+ static $flowImporter = null;
+ if ( $flowImporter === null ) {
+ // importer can be dry-run (= parse, but don't store),
but we can only
+ // derive that from mPageOutCallback. I'll set a new
value (which will
+ // return the existing value) to see if it's in dry-run
mode (= null)
+ $callback = $importer->setPageOutCallback( null );
+ // restore previous mPageOutCallback value
+ $importer->setPageOutCallback( $callback );
+
+ $flowImporter = new \Flow\Dump\Importer( $importer );
+ if ( $callback !== null ) {
+ // not in dry-run mode
+ $flowImporter->setStorage( Container::get(
'storage' ) );
+ }
+ }
+
+ $reader = $importer->getReader();
+ $tag = $reader->localName;
+ $type = $reader->nodeType;
+
+ if ( $tag == 'board' ) {
+ if ( $type === XMLReader::ELEMENT ) {
+ $flowImporter->handleBoard();
+ }
+ return false;
+ } elseif ( $tag == 'description' ) {
+ if ( $type === XMLReader::ELEMENT ) {
+ $flowImporter->handleHeader();
+ }
+ return false;
+ } elseif ( $tag == 'topic' ) {
+ if ( $type === XMLReader::ELEMENT ) {
+ $flowImporter->handleTopic();
+ }
+ return false;
+ } elseif ( $tag == 'post' ) {
+ if ( $type === XMLReader::ELEMENT ) {
+ $flowImporter->handlePost();
+ }
+ return false;
+ } elseif ( $tag == 'summary' ) {
+ if ( $type === XMLReader::ELEMENT ) {
+ $flowImporter->handleSummary();
+ }
+ return false;
+ } elseif ( $tag == 'children' ) {
+ return false;
+ }
+
+ return true;
+ }
}
diff --git a/autoload.php b/autoload.php
index 5dfa95f..583baf3 100644
--- a/autoload.php
+++ b/autoload.php
@@ -115,6 +115,7 @@
'Flow\\Data\\Utils\\UserMerger' => __DIR__ .
'/includes/Data/Utils/UserMerger.php',
'Flow\\DbFactory' => __DIR__ . '/includes/DbFactory.php',
'Flow\\Dump\\Exporter' => __DIR__ . '/includes/Dump/Exporter.php',
+ 'Flow\\Dump\\Importer' => __DIR__ . '/includes/Dump/Importer.php',
'Flow\\Exception\\CatchableFatalErrorException' => __DIR__ .
'/includes/Exception/CatchableFatalErrorException.php',
'Flow\\Exception\\CrossWikiException' => __DIR__ .
'/includes/Exception/ExceptionHandling.php',
'Flow\\Exception\\DataModelException' => __DIR__ .
'/includes/Exception/ExceptionHandling.php',
diff --git a/includes/Data/Listener/ReferenceRecorder.php
b/includes/Data/Listener/ReferenceRecorder.php
index 358ec64..95baa62 100644
--- a/includes/Data/Listener/ReferenceRecorder.php
+++ b/includes/Data/Listener/ReferenceRecorder.php
@@ -137,7 +137,7 @@
}
$previous = $this->storage->get( 'PostRevision',
$current->getPrevRevisionId() );
if ( !$previous ) {
- throw new FlowException( 'Expcted previous revision of
' . $current->getPrevRevisionId()->getAlphadecimal() );
+ throw new FlowException( 'Expected previous revision of
' . $current->getPrevRevisionId()->getAlphadecimal() );
}
$isHidden = self::isHidden( $current );
diff --git a/includes/Dump/Importer.php b/includes/Dump/Importer.php
new file mode 100644
index 0000000..614e999
--- /dev/null
+++ b/includes/Dump/Importer.php
@@ -0,0 +1,238 @@
+<?php
+
+namespace Flow\Dump;
+
+use Flow\Container;
+use Flow\Data\ManagerGroup;
+use Flow\Model\AbstractRevision;
+use Flow\Model\Header;
+use Flow\Model\PostRevision;
+use Flow\Model\TopicListEntry;
+use Flow\Model\UUID;
+use Flow\Model\Workflow;
+use Flow\OccupationController;
+use WikiImporter;
+use XMLReader;
+
+class Importer {
+ /**
+ * @var WikiImporter
+ */
+ protected $importer;
+
+ /**
+ * @var ManagerGroup|null
+ */
+ protected $storage;
+
+ /**
+ * The most recently imported board workflow (if any).
+ *
+ * @var Workflow|null
+ */
+ protected $boardWorkflow;
+
+ /**
+ * The most recently imported topic workflow (if any).
+ *
+ * @var Workflow|null
+ */
+ protected $topicWorkflow;
+
+ /**
+ * @param WikiImporter $importer
+ */
+ public function __construct( WikiImporter $importer ) {
+ $this->importer = $importer;
+ }
+
+ /**
+ * @param ManagerGroup $storage
+ */
+ public function setStorage( ManagerGroup $storage ) {
+ $this->storage = $storage;
+ }
+
+ /**
+ * @param object $object
+ * @param array $metadata
+ */
+ protected function put( $object, array $metadata = array() ) {
+ if ( $this->storage ) {
+ $this->storage->put( $object, array( 'imported' => true
) + $metadata );
+
+ // prevent memory from being filled up
+ $this->storage->clear();
+
+ // keep workflow objects around, so follow-up `put`s
(e.g. to update
+ // last_update_timestamp) don't confuse it for a new
object
+ foreach ( array( $this->boardWorkflow,
$this->topicWorkflow ) as $object ) {
+ if ( $object ) {
+ $this->storage->getStorage( get_class(
$object ) )->merge( $object );
+ }
+ }
+ }
+ }
+
+ public function handleBoard() {
+ $id = $this->importer->nodeAttribute( 'id' );
+ $this->importer->debug( 'Enter board handler for ' . $id );
+
+ $uuid = UUID::create( $id );
+ $title = \Title::newFromDBkey( $this->importer->nodeAttribute(
'title' ) );
+
+ $this->boardWorkflow = Workflow::fromStorageRow( array(
+ 'workflow_id' => $uuid->getAlphadecimal(),
+ 'workflow_type' => 'discussion',
+ 'workflow_wiki' => wfWikiID(),
+ 'workflow_page_id' => $title->getArticleID(),
+ 'workflow_namespace' => $title->getNamespace(),
+ 'workflow_title_text' => $title->getDBkey(),
+ 'workflow_last_update_timestamp' =>
$uuid->getTimestamp( TS_MW ),
+ ) );
+
+ // create page if it does not yet exist
+ /** @var OccupationController $occupationController */
+ $occupationController = Container::get( 'occupation_controller'
);
+ $occupationController->allowCreation( $title,
$occupationController->getTalkpageManager() );
+ $occupationController->ensureFlowRevision( new \Article( $title
), $this->boardWorkflow );
+
+ $this->put( $this->boardWorkflow, array() );
+ }
+
+ public function handleHeader() {
+ $id = $this->importer->nodeAttribute( 'id' );
+ $this->importer->debug( 'Enter description handler for ' . $id
);
+
+ $metadata = array( 'workflow' => $this->boardWorkflow );
+
+ $revisions = $this->getRevisions( array( 'Flow\\Model\\Header',
'fromStorageRow' ) );
+ foreach ( $revisions as $revision ) {
+ $this->put( $revision, $metadata );
+ }
+
+ /** @var Header $revision */
+ $revision = end( $revisions );
+ $this->boardWorkflow->updateLastUpdated(
$revision->getRevisionId() );
+ $this->put( $this->boardWorkflow, array() );
+ }
+
+ public function handleTopic() {
+ $id = $this->importer->nodeAttribute( 'id' );
+ $this->importer->debug( 'Enter topic handler for ' . $id );
+
+ $uuid = UUID::create( $this->importer->nodeAttribute( 'id' ) );
+ $title = $this->boardWorkflow->getArticleTitle();
+
+ $this->topicWorkflow = Workflow::fromStorageRow( array(
+ 'workflow_id' => $uuid->getAlphadecimal(),
+ 'workflow_type' => 'topic',
+ 'workflow_wiki' => wfWikiID(),
+ 'workflow_page_id' => $title->getArticleID(),
+ 'workflow_namespace' => $title->getNamespace(),
+ 'workflow_title_text' => $title->getDBkey(),
+ 'workflow_last_update_timestamp' =>
$uuid->getTimestamp( TS_MW ),
+ ) );
+ $topicListEntry = TopicListEntry::create( $this->boardWorkflow,
$this->topicWorkflow );
+
+ $metadata = array(
+ 'board-workflow' => $this->boardWorkflow,
+ 'workflow' => $this->topicWorkflow,
+ // @todo: topic-title & first-post? (used only in
NotificationListener)
+ );
+
+ $this->put( $this->topicWorkflow, $metadata );
+ $this->put( $topicListEntry, $metadata );
+ }
+
+ public function handlePost() {
+ $id = $this->importer->nodeAttribute( 'id' );
+ $this->importer->debug( 'Enter post handler for ' . $id );
+
+ $metadata = array(
+ 'workflow' => $this->topicWorkflow
+ // @todo: topic-title? (used only in
NotificationListener)
+ );
+
+ $revisions = $this->getRevisions( array(
'Flow\\Model\\PostRevision', 'fromStorageRow' ) );
+ foreach ( $revisions as $revision ) {
+ $this->put( $revision, $metadata );
+ }
+
+ /** @var PostRevision $revision */
+ $revision = end( $revisions );
+ $this->topicWorkflow->updateLastUpdated(
$revision->getRevisionId() );
+ $this->put( $this->topicWorkflow, $metadata );
+ }
+
+ public function handleSummary() {
+ $id = $this->importer->nodeAttribute( 'id' );
+ $this->importer->debug( 'Enter summary handler for ' . $id );
+
+ $metadata = array( 'workflow' => $this->topicWorkflow );
+
+ $revisions = $this->getRevisions( array(
'Flow\\Model\\PostSummary', 'fromStorageRow' ) );
+ foreach ( $revisions as $revision ) {
+ $this->put( $revision, $metadata );
+ }
+
+ /** @var PostSummary $revision */
+ $revision = end( $revisions );
+ $this->topicWorkflow->updateLastUpdated(
$revision->getRevisionId() );
+ $this->put( $this->topicWorkflow, $metadata );
+ }
+
+ /**
+ * @param callable $callback The relevant fromStorageRow callback
+ * @return AbstractRevision[]
+ */
+ protected function getRevisions( $callback ) {
+ $revisions = array();
+
+ // keep processing <revision> nodes until </revisions>
+ while ( $this->importer->getReader()->localName !== 'revisions'
|| $this->importer->getReader()->nodeType !== XMLReader::END_ELEMENT ) {
+ if ( $this->importer->getReader()->localName ===
'revision' ) {
+ $revisions[] = $this->getRevision( $callback );
+ }
+ $this->importer->getReader()->read();
+ }
+
+ return $revisions;
+ }
+
+ /**
+ * @param callable $callback The relevant fromStorageRow callback
+ * @return AbstractRevision
+ */
+ protected function getRevision( $callback ) {
+ $id = $this->importer->nodeAttribute( 'id' );
+ $this->importer->debug( 'Enter revision handler for ' . $id );
+
+ // isEmptyElement will no longer be valid after we've started
iterating
+ // the attributes
+ $empty = $this->importer->getReader()->isEmptyElement;
+
+ $attribs = array();
+
+ $this->importer->getReader()->moveToFirstAttribute();
+ do {
+ $attribs[$this->importer->getReader()->name] =
$this->importer->getReader()->value;
+ } while ( $this->importer->getReader()->moveToNextAttribute() );
+
+ // now that we've moved inside the node (to fetch attributes),
+ // nodeContents() is no longer reliable: is uses isEmptyContent
(which
+ // will now no longer respond with 'true') to see if the node
should be
+ // skipped - use the value we've fetched earlier!
+ $attribs['content'] = $empty ? '' :
$this->importer->nodeContents();
+
+ // make sure there are no leftover key columns (unknown to
$attribs)
+ $keys = array_intersect_key( array_flip( Exporter::$map ),
$attribs );
+ // now make sure $values columns are in the same order as $keys
are
+ // (array_merge) and there are no leftover columns
(array_intersect_key)
+ $values = array_intersect_key( array_merge( $keys, $attribs ),
$keys );
+ // combine them
+ $attribs = array_combine( $keys, $values );
+
+ return call_user_func( $callback, $attribs );
+ }
+}
diff --git a/includes/Model/AbstractRevision.php
b/includes/Model/AbstractRevision.php
index 857d329..15a78ac 100644
--- a/includes/Model/AbstractRevision.php
+++ b/includes/Model/AbstractRevision.php
@@ -171,7 +171,7 @@
if ( $obj->user === null ) {
throw new DataModelException( 'Could not load UserTuple
for rev_user_' );
}
- $obj->prevRevision = UUID::create( $row['rev_parent_id'] );
+ $obj->prevRevision = $row['rev_parent_id'] ? UUID::create(
$row['rev_parent_id'] ) : null;
$obj->changeType = $row['rev_change_type'];
$obj->flags = array_filter( explode( ',', $row['rev_flags'] ) );
$obj->content = $row['rev_content'];
@@ -181,8 +181,8 @@
$obj->moderationState = $row['rev_mod_state'];
$obj->moderatedBy = UserTuple::newFromArray( $row,
'rev_mod_user_' );
- $obj->moderationTimestamp = $row['rev_mod_timestamp'];
- $obj->moderatedReason = isset( $row['rev_mod_reason'] ) ?
$row['rev_mod_reason'] : null;
+ $obj->moderationTimestamp = $row['rev_mod_timestamp'] ?: null;
+ $obj->moderatedReason = isset( $row['rev_mod_reason'] ) &&
$row['rev_mod_reason'] ? $row['rev_mod_reason'] : null;
// BC: 'suppress' used to be called 'censor' & 'lock' was
'close'
$bc = array(
@@ -192,7 +192,7 @@
$obj->moderationState = str_replace( array_keys( $bc ),
array_values( $bc ), $obj->moderationState );
// isset required because there is a possible db migration,
cached data will not have it
- $obj->lastEditId = isset( $row['rev_last_edit_id'] ) ?
UUID::create( $row['rev_last_edit_id'] ) : null;
+ $obj->lastEditId = isset( $row['rev_last_edit_id'] ) &&
$row['rev_last_edit_id'] ? UUID::create( $row['rev_last_edit_id'] ) : null;
$obj->lastEditUser = UserTuple::newFromArray( $row,
'rev_edit_user_' );
$obj->contentLength = isset( $row['rev_content_length'] ) ?
$row['rev_content_length'] : 0;
diff --git a/includes/Model/PostRevision.php b/includes/Model/PostRevision.php
index bae47ba..e555b4a 100644
--- a/includes/Model/PostRevision.php
+++ b/includes/Model/PostRevision.php
@@ -133,7 +133,7 @@
'process-data'
);
}
- $obj->replyToId = UUID::create( $row['tree_parent_id'] );
+ $obj->replyToId = $row['tree_parent_id'] ? UUID::create(
$row['tree_parent_id'] ) : null;
$obj->postId = UUID::create( $row['rev_type_id'] );
$obj->origUser = UserTuple::newFromArray( $row,
'tree_orig_user_' );
if ( !$obj->origUser ) {
--
To view, visit https://gerrit.wikimedia.org/r/245501
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: Ieae618d4aaa4b21a4eb8fe8d1f0627ee8bd1ff8d
Gerrit-PatchSet: 22
Gerrit-Project: mediawiki/extensions/Flow
Gerrit-Branch: master
Gerrit-Owner: Matthias Mullie <[email protected]>
Gerrit-Reviewer: Daniel Kinzler <[email protected]>
Gerrit-Reviewer: Mattflaschen <[email protected]>
Gerrit-Reviewer: Matthias Mullie <[email protected]>
Gerrit-Reviewer: Sbisson <[email protected]>
Gerrit-Reviewer: TTO <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits