From ca3be1e13dbd3de7c866f512fd984211f78e3f50 Mon Sep 17 00:00:00 2001 From: daniel Date: Mon, 2 Feb 2015 11:58:14 +0100 Subject: [PATCH] Maintenance script for exporting site definitions Bug: T87178 Change-Id: I40fd6aaa8f47bad3d595d5c190036bf04d13c12a --- autoload.php | 1 + includes/site/SiteExporter.php | 114 ++++++++++++++ maintenance/exportSites.php | 54 +++++++ .../includes/site/SiteExporterTest.php | 148 ++++++++++++++++++ 4 files changed, 317 insertions(+) create mode 100644 includes/site/SiteExporter.php create mode 100644 maintenance/exportSites.php create mode 100644 tests/phpunit/includes/site/SiteExporterTest.php diff --git a/autoload.php b/autoload.php index 552566a528..4b28966d07 100644 --- a/autoload.php +++ b/autoload.php @@ -1046,6 +1046,7 @@ $wgAutoloadLocalClasses = array( 'Site' => __DIR__ . '/includes/site/Site.php', 'SiteArray' => __DIR__ . '/includes/site/SiteList.php', 'SiteConfiguration' => __DIR__ . '/includes/SiteConfiguration.php', + 'SiteExporter' => __DIR__ . '/includes/site/SiteExporter.php', 'SiteImporter' => __DIR__ . '/includes/site/SiteImporter.php', 'SiteList' => __DIR__ . '/includes/site/SiteList.php', 'SiteListFileCache' => __DIR__ . '/includes/site/SiteListFileCache.php', diff --git a/includes/site/SiteExporter.php b/includes/site/SiteExporter.php new file mode 100644 index 0000000000..62f6ca3cb2 --- /dev/null +++ b/includes/site/SiteExporter.php @@ -0,0 +1,114 @@ +sink = $sink; + } + + /** + * Writes a tag for each Site object in $sites, and encloses the entire list + * between tags. + * + * @param Site[]|SiteList $sites + */ + public function exportSites( $sites ) { + $attributes = array( + 'version' => '1.0', + 'xmlns' => 'http://www.mediawiki.org/xml/sitelist-1.0/', + ); + + fwrite( $this->sink, XML::openElement( 'sites', $attributes ) . "\n" ); + + foreach ( $sites as $site ) { + $this->exportSite( $site ); + } + + fwrite( $this->sink, XML::closeElement( 'sites' ) . "\n" ); + fflush( $this->sink ); + } + + /** + * Writes a tag representing the given Site object. + * + * @param Site $site + */ + private function exportSite( Site $site ) { + if ( $site->getType() !== Site::TYPE_UNKNOWN ) { + $siteAttr = array( 'type' => $site->getType() ); + } else { + $siteAttr = null; + } + + fwrite( $this->sink, "\t" . XML::openElement( 'site', $siteAttr ) . "\n" ); + + fwrite( $this->sink, "\t\t" . XML::element( 'globalid', null, $site->getGlobalId() ) . "\n" ); + + if ( $site->getGroup() !== Site::GROUP_NONE ) { + fwrite( $this->sink, "\t\t" . XML::element( 'group', null, $site->getGroup() ) . "\n" ); + } + + if ( $site->getSource() !== Site::SOURCE_LOCAL ) { + fwrite( $this->sink, "\t\t" . XML::element( 'source', null, $site->getSource() ) . "\n" ); + } + + if ( $site->shouldForward() ) { + fwrite( $this->sink, "\t\t" . XML::element( 'forward', null, '' ) . "\n" ); + } + + foreach ( $site->getAllPaths() as $type => $path ) { + fwrite( $this->sink, "\t\t" . XML::element( 'path', array( 'type' => $type ), $path ) . "\n" ); + } + + foreach ( $site->getLocalIds() as $type => $ids ) { + foreach ( $ids as $id ) { + fwrite( $this->sink, "\t\t" . XML::element( 'localid', array( 'type' => $type ), $id ) . "\n" ); + } + } + + //@todo: export + //@todo: export + + fwrite( $this->sink, "\t" . XML::closeElement( 'site' ) . "\n" ); + } + +} diff --git a/maintenance/exportSites.php b/maintenance/exportSites.php new file mode 100644 index 0000000000..1c71dc0ed0 --- /dev/null +++ b/maintenance/exportSites.php @@ -0,0 +1,54 @@ +mDescription = 'Exports site definitions the sites table to XML file'; + + $this->addArg( 'file', 'A file to write the XML to (see docs/sitelist.txt). Use "php://stdout" to write to stdout.', true ); + + parent::__construct(); + } + + /** + * Do the actual work. All child classes will need to implement this + */ + public function execute() { + $file = $this->getArg( 0 ); + + if ( $file === 'php://output' || $file === 'php://stdout' ) { + $this->mQuiet = true; + } + + $handle = fopen( $file, 'w' ); + + if ( !$handle ) { + $this->error( "Failed to open $file for writing.\n", 1 ); + } + + $exporter = new SiteExporter( $handle ); + + $sites = SiteSQLStore::newInstance()->getSites( 'recache' ); + $exporter->exportSites( $sites ); + + fclose( $handle ); + + $this->output( "Exported sites to " . realpath( $file ) . ".\n" ); + } + +} + +$maintClass = 'ExportSites'; +require_once( RUN_MAINTENANCE_IF_MAIN ); diff --git a/tests/phpunit/includes/site/SiteExporterTest.php b/tests/phpunit/includes/site/SiteExporterTest.php new file mode 100644 index 0000000000..a3ef4bebcb --- /dev/null +++ b/tests/phpunit/includes/site/SiteExporterTest.php @@ -0,0 +1,148 @@ +setExpectedException( 'InvalidArgumentException' ); + + new SiteExporter( 'Foo' ); + } + + public function testExportSites() { + $foo = Site::newForType( Site::TYPE_UNKNOWN ); + $foo->setGlobalId( 'Foo' ); + + $acme = Site::newForType( Site::TYPE_UNKNOWN ); + $acme->setGlobalId( 'acme.com' ); + $acme->setGroup( 'Test' ); + $acme->addLocalId( Site::ID_INTERWIKI, 'acme' ); + $acme->setPath( Site::PATH_LINK, 'http://acme.com/' ); + + $tmp = tmpfile(); + $exporter = new SiteExporter( $tmp ); + + $exporter->exportSites( array( $foo, $acme ) ); + + fseek( $tmp, 0 ); + $xml = fread( $tmp, 16*1024 ); + + $this->assertContains( 'assertContains( '', $xml ); + $this->assertContains( 'Foo', $xml ); + $this->assertContains( '', $xml ); + $this->assertContains( 'acme.com', $xml ); + $this->assertContains( 'Test', $xml ); + $this->assertContains( 'acme', $xml ); + $this->assertContains( 'http://acme.com/', $xml ); + $this->assertContains( '', $xml ); + + // NOTE: HHVM (at least on wmf Jenkins) doesn't like file URLs. + $xsdFile = __DIR__ . '/../../../../docs/sitelist-1.0.xsd'; + $xsdData = file_get_contents( $xsdFile ); + + $document = new DOMDocument(); + $document->loadXML( $xml, LIBXML_NONET ); + $document->schemaValidateSource( $xsdData ); + } + + private function newSiteStore( SiteList $sites ) { + $store = $this->getMock( 'SiteStore' ); + + $store->expects( $this->once() ) + ->method( 'saveSites' ) + ->will( $this->returnCallback( function ( $moreSites ) use ( $sites ) { + foreach ( $moreSites as $site ) { + $sites->setSite( $site ); + } + } ) ); + + $store->expects( $this->any() ) + ->method( 'getSites' ) + ->will( $this->returnValue( new SiteList() ) ); + + return $store; + } + + public function provideRoundTrip() { + $foo = Site::newForType( Site::TYPE_UNKNOWN ); + $foo->setGlobalId( 'Foo' ); + + $acme = Site::newForType( Site::TYPE_UNKNOWN ); + $acme->setGlobalId( 'acme.com' ); + $acme->setGroup( 'Test' ); + $acme->addLocalId( Site::ID_INTERWIKI, 'acme' ); + $acme->setPath( Site::PATH_LINK, 'http://acme.com/' ); + + $dewiki = Site::newForType( Site::TYPE_MEDIAWIKI ); + $dewiki->setGlobalId( 'dewiki' ); + $dewiki->setGroup( 'wikipedia' ); + $dewiki->setForward( true ); + $dewiki->addLocalId( Site::ID_INTERWIKI, 'wikipedia' ); + $dewiki->addLocalId( Site::ID_EQUIVALENT, 'de' ); + $dewiki->setPath( Site::PATH_LINK, 'http://de.wikipedia.org/w/' ); + $dewiki->setPath( MediaWikiSite::PATH_PAGE, 'http://de.wikipedia.org/wiki/' ); + $dewiki->setSource( 'meta.wikimedia.org' ); + + return array( + 'empty' => array( + new SiteList() + ), + + 'some' => array( + new SiteList( array( $foo, $acme, $dewiki ) ), + ), + ); + } + + /** + * @dataProvider provideRoundTrip() + */ + public function testRoundTrip( SiteList $sites ) { + $tmp = tmpfile(); + $exporter = new SiteExporter( $tmp ); + + $exporter->exportSites( $sites ); + + fseek( $tmp, 0 ); + $xml = fread( $tmp, 16*1024 ); + + $actualSites = new SiteList(); + $store = $this->newSiteStore( $actualSites ); + + $importer = new SiteImporter( $store ); + $importer->importFromXML( $xml ); + + $this->assertEquals( $sites, $actualSites ); + } + +} -- 2.20.1