1<?php
2
3use MediaWiki\MediaWikiServices;
4use MediaWiki\Revision\RevisionRecord;
5use MediaWiki\Tests\Maintenance\DumpAsserter;
6
7/**
8 * Import/export round trip test.
9 *
10 * @group Database
11 * @covers WikiExporter
12 * @covers WikiImporter
13 */
14class ImportExportTest extends MediaWikiLangTestCase {
15
16	public function setUp(): void {
17		parent::setUp();
18
19		$slotRoleRegistry = MediaWikiServices::getInstance()->getSlotRoleRegistry();
20
21		if ( !$slotRoleRegistry->isDefinedRole( 'ImportExportTest' ) ) {
22			$slotRoleRegistry->defineRoleWithModel( 'ImportExportTest', CONTENT_MODEL_WIKITEXT );
23		}
24	}
25
26	/**
27	 * @param string $schemaVersion
28	 *
29	 * @return WikiExporter
30	 */
31	private function getExporter( string $schemaVersion ) {
32		$exporter = new WikiExporter( $this->db, WikiExporter::FULL );
33		$exporter->setSchemaVersion( $schemaVersion );
34		return $exporter;
35	}
36
37	/**
38	 * @param ImportSource $source
39	 *
40	 * @return WikiImporter
41	 */
42	private function getImporter( ImportSource $source ) {
43		$config = new HashConfig( [
44			'CommandLineMode' => true,
45		] );
46		$importer = new WikiImporter( $source, $config );
47		return $importer;
48	}
49
50	/**
51	 * @param string $testName
52	 *
53	 * @return string[]
54	 */
55	private function getFilesToImport( string $testName ) {
56		return glob( __DIR__ . "/../../data/import/$testName.import-*.xml" );
57	}
58
59	/**
60	 * @param string $name
61	 * @param string $schemaVersion
62	 *
63	 * @return string path of the dump file
64	 */
65	protected function getDumpTemplatePath( $name, $schemaVersion ) {
66		return __DIR__ . "/../../data/import/$name.$schemaVersion.xml";
67	}
68
69	/**
70	 * @param string $prefix
71	 * @param string[] $keys
72	 *
73	 * @return string[]
74	 */
75	private function getUniqueNames( string $prefix, array $keys ) {
76		$names = [];
77
78		foreach ( $keys as $k ) {
79			$names[$k] = "$prefix-$k-" . wfRandomString( 6 );
80		}
81
82		return $names;
83	}
84
85	/**
86	 * @param string $xmlData
87	 * @param string[] $pageTitles
88	 *
89	 * @return string
90	 */
91	private function injectPageTitles( string $xmlData, array $pageTitles ) {
92		$keys = array_map( static function ( $name ) {
93			return "{{{$name}_title}}";
94		}, array_keys( $pageTitles ) );
95
96		return str_replace(
97			$keys,
98			array_values( $pageTitles ),
99			$xmlData
100		);
101	}
102
103	/**
104	 * @param Title $title
105	 *
106	 * @return RevisionRecord[]
107	 */
108	private function getRevisions( Title $title ) {
109		$store = MediaWikiServices::getInstance()->getRevisionStore();
110		$qi = $store->getQueryInfo();
111
112		$conds = [ 'rev_page' => $title->getArticleID() ];
113		$opt = [ 'ORDER BY' => 'rev_id ASC' ];
114
115		$rows = $this->db->select(
116			$qi['tables'],
117			$qi['fields'],
118			$conds,
119			__METHOD__,
120			$opt,
121			$qi['joins']
122		);
123
124		$status = $store->newRevisionsFromBatch( $rows );
125		return $status->getValue();
126	}
127
128	/**
129	 * @param string[] $pageTitles
130	 *
131	 * @return string[]
132	 */
133	private function getPageInfoVars( array $pageTitles ) {
134		$vars = [];
135		foreach ( $pageTitles as $name => $page ) {
136			$title = Title::newFromText( $page );
137
138			if ( !$title->exists( Title::READ_LATEST ) ) {
139				// map only existing pages, since only they can appear in a dump
140				continue;
141			}
142
143			$vars[ $name . '_pageid' ] = $title->getArticleID();
144			$vars[ $name . '_title' ] = $title->getPrefixedDBkey();
145			$vars[ $name . '_namespace' ] = $title->getNamespace();
146
147			$n = 1;
148			$revisions = $this->getRevisions( $title );
149			foreach ( $revisions as $i => $rev ) {
150				$revkey = "{$name}_rev" . $n++;
151
152				$vars[ $revkey . '_id' ] = $rev->getId();
153				$vars[ $revkey . '_userid' ] = $rev->getUser()->getId();
154			}
155		}
156
157		return $vars;
158	}
159
160	/**
161	 * @param string $schemaVersion
162	 * @return string[]
163	 */
164	private function getSiteVars( $schemaVersion ) {
165		global $wgSitename, $wgDBname, $wgVersion, $wgCapitalLinks;
166
167		$vars = [];
168		$vars['mw_version'] = $wgVersion;
169		$vars['schema_version'] = $schemaVersion;
170
171		$vars['site_name'] = $wgSitename;
172		$vars['project_namespace'] =
173			MediaWikiServices::getInstance()->getTitleFormatter()->getNamespaceName(
174				NS_PROJECT,
175				'Dummy'
176			);
177		$vars['site_db'] = $wgDBname;
178		$vars['site_case'] = $wgCapitalLinks ? 'first-letter' : 'case-sensitive';
179		$vars['site_base'] = Title::newMainPage()->getCanonicalURL();
180		$vars['site_language'] = MediaWikiServices::getInstance()->getContentLanguage()->getHtmlCode();
181
182		return $vars;
183	}
184
185	public function provideImportExport() {
186		foreach ( XmlDumpWriter::$supportedSchemas as $schemaVersion ) {
187			yield [ 'Basic', $schemaVersion ];
188			yield [ 'Dupes', $schemaVersion ];
189			yield [ 'Slots', $schemaVersion ];
190			yield [ 'Interleaved', $schemaVersion ];
191			yield [ 'InterleavedMulti', $schemaVersion ];
192			yield [ 'MissingMainContentModel', $schemaVersion ];
193			yield [ 'MissingSlotContentModel', $schemaVersion ];
194		}
195	}
196
197	/**
198	 * @dataProvider provideImportExport
199	 */
200	public function testImportExport( $testName, $schemaVersion ) {
201		$asserter = new DumpAsserter( $schemaVersion );
202
203		$filesToImport = $this->getFilesToImport( $testName );
204		$fileToExpect = $this->getDumpTemplatePath( "$testName.expected", $schemaVersion );
205		$siteInfoExpect = $this->getDumpTemplatePath( 'SiteInfo', $schemaVersion );
206
207		$pageKeys = [ 'page1', 'page2', 'page3', 'page4', ];
208		$pageTitles = $this->getUniqueNames( $testName, $pageKeys );
209
210		// import each file
211		foreach ( $filesToImport as $fileName ) {
212			$xmlData = file_get_contents( $fileName );
213			$xmlData = $this->injectPageTitles( $xmlData, $pageTitles );
214
215			$source = new ImportStringSource( $xmlData );
216			$importer = $this->getImporter( $source );
217			$importer->doImport();
218		}
219
220		// write dump
221		$exporter = $this->getExporter( $schemaVersion );
222
223		$tmpFile = $this->getNewTempFile();
224		$buffer = new DumpFileOutput( $tmpFile );
225
226		$exporter->setOutputSink( $buffer );
227		$exporter->openStream();
228		$exporter->pagesByName( $pageTitles );
229		$exporter->closeStream();
230
231		// determine expected variable values
232		$vars = array_merge(
233			$this->getSiteVars( $schemaVersion ),
234			$this->getPageInfoVars( $pageTitles )
235		);
236
237		foreach ( $vars as $key => $value ) {
238			$asserter->setVarMapping( $key, $value );
239		}
240
241		// sanity check
242		$dumpData = file_get_contents( $tmpFile );
243		$this->assertNotEmpty( $dumpData, 'Dump XML' );
244
245		// check dump content
246		$asserter->assertDumpStart( $tmpFile, $siteInfoExpect );
247		$asserter->assertDOM( $fileToExpect );
248		$asserter->assertDumpEnd();
249	}
250
251}
252