Matthias Mullie has uploaded a new change for review.
https://gerrit.wikimedia.org/r/245501
Change subject: [WIP] Import Flow data dump
......................................................................
[WIP] Import Flow data dump
Meanwhile also changes some Models to allow empty values as well
for nullable columns.
Bug: T114703
Change-Id: Ieae618d4aaa4b21a4eb8fe8d1f0627ee8bd1ff8d
---
M autoload.php
A includes/Dump/Importer.php
M includes/Model/AbstractRevision.php
M includes/Model/PostRevision.php
A maintenance/importDump.php
5 files changed, 351 insertions(+), 5 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Flow
refs/changes/01/245501/1
diff --git a/autoload.php b/autoload.php
index 9094e3c..988dc93 100644
--- a/autoload.php
+++ b/autoload.php
@@ -114,6 +114,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/Dump/Importer.php b/includes/Dump/Importer.php
new file mode 100644
index 0000000..8c3d30e
--- /dev/null
+++ b/includes/Dump/Importer.php
@@ -0,0 +1,243 @@
+<?php
+
+namespace Flow\Dump;
+
+use Exception;
+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 MWException;
+use WikiImporter;
+use XMLReader;
+
+class Importer extends WikiImporter {
+ /**
+ * @var ManagerGroup|null
+ */
+ protected $storage;
+
+ /**
+ * @var array
+ */
+ protected $metadata = array();
+
+ /**
+ * Copied from parent::doImport, but with different handle* calls
+ * for the different nodes we deal with.
+ *
+ * @return bool
+ * @throws Exception
+ * @throws MWException
+ * @throws null
+ */
+ public function doImport() {
+ // Calls to reader->read need to be wrapped in calls to
+ // libxml_disable_entity_loader() to avoid local file
+ // inclusion attacks (bug 46932).
+ $oldDisable = libxml_disable_entity_loader( true );
+ $this->reader->read();
+
+ if ( $this->reader->localName != 'mediawiki' ) {
+ libxml_disable_entity_loader( $oldDisable );
+ throw new MWException( "Expected <mediawiki> tag, got "
.
+ $this->reader->localName );
+ }
+ $this->debug( "<mediawiki> tag is correct." );
+
+ $this->debug( "Starting primary dump processing loop." );
+
+ $keepReading = $this->reader->read();
+ $skip = false;
+ $rethrow = null;
+ try {
+ while ( $keepReading ) {
+ $tag = $this->reader->localName;
+ $type = $this->reader->nodeType;
+
+ if ( $tag == 'mediawiki' && $type ===
XMLReader::END_ELEMENT ) {
+ break;
+ } elseif ( $tag == 'board' && $type ===
XMLReader::ELEMENT ) {
+ // prevent memory from being filled up
+ /** @var ManagerGroup $storage */
+ $storage = Container::get( 'storage' );
+ $storage->clear();
+
+ $this->handleBoard();
+ } elseif ( $tag == 'description' && $type ===
XMLReader::ELEMENT ) {
+ $this->handleHeader();
+ } elseif ( $tag == 'topic' && $type ===
XMLReader::ELEMENT ) {
+ $this->handleTopic();
+ } elseif ( $tag == 'post' && $type ===
XMLReader::ELEMENT ) {
+ $this->handlePost();
+ } elseif ( $tag == 'summary' && $type ===
XMLReader::ELEMENT ) {
+ $this->handleSummary();
+ } elseif ( $tag != '#text' ) {
+ $this->warn( "Unhandled top-level XML
tag $tag" );
+
+ $skip = true;
+ }
+
+ if ( $skip ) {
+ $keepReading = $this->reader->next();
+ $skip = false;
+ $this->debug( "Skip" );
+ } else {
+ $keepReading = $this->reader->read();
+ }
+ }
+ } catch ( Exception $ex ) {
+ $rethrow = $ex;
+ }
+
+ // finally
+ libxml_disable_entity_loader( $oldDisable );
+ $this->reader->close();
+
+ if ( $rethrow ) {
+ throw $rethrow;
+ }
+
+ return true;
+ }
+
+ /**
+ * @param ManagerGroup $storage
+ */
+ public function setStorage( ManagerGroup $storage ) {
+ $this->storage = $storage;
+ }
+
+ /**
+ * @param object $object
+ */
+ protected function put( $object ) {
+ if ( $this->storage ) {
+ $this->storage->put( $object, $this->metadata );
+ }
+ }
+
+ protected function handleBoard() {
+ $this->debug( 'Enter board handler.' );
+
+ global $wgFlowDefaultWorkflow;
+
+ $uuid = UUID::create( $this->nodeAttribute( 'id' ) );
+ $title = \Title::newFromDBkey( $this->nodeAttribute( 'title' )
);
+
+ $workflow = Workflow::fromStorageRow( array(
+ 'workflow_id' => $uuid->getAlphadecimal(),
+ 'workflow_type' => $wgFlowDefaultWorkflow,
+ '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
), $workflow );
+
+ $this->metadata = array();
+ $this->metadata['board-workflow'] = $workflow;
+
+ $this->put( $workflow );
+ }
+
+ protected function handleHeader() {
+ $this->debug( 'Enter description handler.' );
+
+ $revisions = $this->getRevisions( array( 'Flow\\Model\\Header',
'fromStorageRow' ) );
+ foreach ( $revisions as $revision ) {
+ $this->put( $revision );
+ }
+ }
+
+ protected function handleTopic() {
+ $this->debug( 'Enter topic handler.' );
+
+ $uuid = UUID::create( $this->nodeAttribute( 'id' ) );
+ /** @var Workflow $boardWorkflow */
+ $boardWorkflow = $this->metadata['board-workflow'];
+ $title = $boardWorkflow->getArticleTitle();
+
+ $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( $boardWorkflow,
$topicWorkflow );
+
+ $this->metadata['workflow'] = $topicWorkflow;
+
+ $this->put( $topicWorkflow );
+ $this->put( $topicListEntry );
+ }
+
+ protected function handlePost() {
+ $this->debug( 'Enter post handler.' );
+
+ $revisions = $this->getRevisions( array(
'Flow\\Model\\PostRevision', 'fromStorageRow' ) );
+ foreach ( $revisions as $revision ) {
+ $this->put( $revision );
+ }
+ }
+
+ protected function handleSummary() {
+ $this->debug( 'Enter summary handler.' );
+
+ $revisions = $this->getRevisions( array(
'Flow\\Model\\PostSummary', 'fromStorageRow' ) );
+ foreach ( $revisions as $revision ) {
+ $this->put( $revision );
+ }
+ }
+
+ /**
+ * @param callable $callback The relevant fromStorageRow callback
+ * @return AbstractRevision[]
+ */
+ protected function getRevisions( $callback ) {
+ $revisions = array();
+
+ // keep processing <revision> nodes until </revisions>
+ while ( $this->reader->localName !== 'revisions' ||
$this->reader->nodeType !== XMLReader::END_ELEMENT ) {
+ if ( $this->reader->localName === 'revision' ) {
+ $revisions[] = $this->getRevision( $callback );
+ }
+ $this->reader->read();
+ }
+
+ return $revisions;
+ }
+
+ /**
+ * @param callable $callback The relevant fromStorageRow callback
+ * @return AbstractRevision
+ */
+ protected function getRevision( $callback ) {
+ $this->debug( 'Enter revision handler.' );
+
+ $data = array();
+
+ $this->reader->moveToFirstAttribute();
+ do {
+ $data[$this->reader->name] = $this->reader->value;
+ } while ( $this->reader->moveToNextAttribute() );
+
+ $data['rev_content'] = $this->nodeContents();
+
+ return call_user_func( $callback, $data );
+ }
+}
diff --git a/includes/Model/AbstractRevision.php
b/includes/Model/AbstractRevision.php
index fdb7ea2..6583a14 100644
--- a/includes/Model/AbstractRevision.php
+++ b/includes/Model/AbstractRevision.php
@@ -164,7 +164,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'];
@@ -174,8 +174,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(
@@ -185,7 +185,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 cac5c9c..f078192 100644
--- a/includes/Model/PostRevision.php
+++ b/includes/Model/PostRevision.php
@@ -132,7 +132,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 ) {
diff --git a/maintenance/importDump.php b/maintenance/importDump.php
new file mode 100644
index 0000000..831d65c
--- /dev/null
+++ b/maintenance/importDump.php
@@ -0,0 +1,102 @@
+<?php
+
+use Flow\Container;
+use Flow\Dump\Importer;
+
+require_once ( getenv( 'MW_INSTALL_PATH' ) !== false
+ ? getenv( 'MW_INSTALL_PATH' ) . '/maintenance/Maintenance.php'
+ : dirname( __FILE__ ) . '/../../../maintenance/Maintenance.php' );
+
+/**
+ * FlowBackupReader is mostly copied from core's importDump.php.
+ * importFromHandle will call a different Importer, but other than that,
+ * this class is mostly the same - it just has some options stripped.
+ * I just couldn't extend the original BackupReader class: including that
+ * file automatically launches the script.
+ */
+class FlowBackupReader extends Maintenance {
+ protected $dryRun = false;
+
+ public function __construct() {
+ parent::__construct();
+ $gz = in_array( 'compress.zlib', stream_get_wrappers() )
+ ? 'ok'
+ : '(disabled; requires PHP zlib module)';
+ $bz2 = in_array( 'compress.bzip2', stream_get_wrappers() )
+ ? 'ok'
+ : '(disabled; requires PHP bzip2 module)';
+
+ $this->mDescription = <<<TEXT
+This script reads pages from an XML file as produced from Flow's
+dumpBackup.php, and saves them into the current wiki.
+
+Compressed XML files may be read directly:
+ .gz $gz
+ .bz2 $bz2
+ .7z (if 7za executable is in PATH)
+
+Note that for very large data sets, importDump.php may be slow.
+TEXT;
+ $this->stderr = fopen( 'php://stderr', 'wt' );
+ $this->addOption( 'dry-run', 'Parse dump without actually
importing pages' );
+ $this->addOption( 'debug', 'Output extra verbose debug
information' );
+ $this->addArg( 'file', 'Dump file to import [else use stdin]',
false );
+ }
+
+ public function execute() {
+ if ( wfReadOnly() ) {
+ $this->error( "Wiki is in read-only mode; you'll need
to disable it for import to work.", true );
+ }
+
+ $this->dryRun = $this->hasOption( 'dry-run' );
+
+ if ( $this->hasArg() ) {
+ $this->importFromFile( $this->getArg() );
+ } else {
+ $this->importFromStdin();
+ }
+
+ $this->output( "Done!\n" );
+ }
+
+ protected function importFromFile( $filename ) {
+ if ( preg_match( '/\.gz$/', $filename ) ) {
+ $filename = 'compress.zlib://' . $filename;
+ } elseif ( preg_match( '/\.bz2$/', $filename ) ) {
+ $filename = 'compress.bzip2://' . $filename;
+ } elseif ( preg_match( '/\.7z$/', $filename ) ) {
+ $filename = 'mediawiki.compress.7z://' . $filename;
+ }
+
+ $file = fopen( $filename, 'rt' );
+
+ return $this->importFromHandle( $file );
+ }
+
+ protected function importFromStdin() {
+ $file = fopen( 'php://stdin', 'rt' );
+ if ( self::posix_isatty( $file ) ) {
+ $this->maybeHelp( true );
+ }
+
+ return $this->importFromHandle( $file );
+ }
+
+ protected function importFromHandle( $handle ) {
+ $source = new ImportStreamSource( $handle );
+ $importer = new Importer( $source, $this->getConfig() );
+
+ if ( $this->hasOption( 'debug' ) ) {
+ $importer->setDebug( true );
+ }
+
+ if ( !$this->dryRun ) {
+ $importer->setStorage( Container::get( 'storage' ) );
+ }
+
+ return $importer->doImport();
+ }
+}
+
+$maintClass = 'FlowBackupReader';
+require_once RUN_MAINTENANCE_IF_MAIN;
--
To view, visit https://gerrit.wikimedia.org/r/245501
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: Ieae618d4aaa4b21a4eb8fe8d1f0627ee8bd1ff8d
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Flow
Gerrit-Branch: master
Gerrit-Owner: Matthias Mullie <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits