60 private const WRITE_STUB_DELETED = 2;
66 public static $supportedSchemas = [
76 private $schemaVersion;
83 private $currentTitle =
null;
94 private $commentStore;
105 $contentMode = self::WRITE_CONTENT,
111 in_array( $contentMode, [ self::WRITE_CONTENT, self::WRITE_STUB ],
true ),
113 'must be one of the following constants: WRITE_CONTENT or WRITE_STUB.'
117 in_array( $schemaVersion, self::$supportedSchemas,
true ),
119 'must be one of the following schema versions: '
120 . implode(
',', self::$supportedSchemas )
123 $this->contentMode = $contentMode;
124 $this->schemaVersion = $schemaVersion;
126 $hookContainer ?? MediaWikiServices::getInstance()->getHookContainer()
128 $this->commentStore = $commentStore ?? MediaWikiServices::getInstance()->getCommentStore();
142 $ver = $this->schemaVersion;
143 return Xml::element(
'mediawiki', [
144 'xmlns' =>
"http://www.mediawiki.org/xml/export-$ver/",
145 'xmlns:xsi' =>
"http://www.w3.org/2001/XMLSchema-instance",
156 'xsi:schemaLocation' =>
"http://www.mediawiki.org/xml/export-$ver/ " .
157 "http://www.mediawiki.org/xml/export-$ver.xsd",
159 'xml:lang' => MediaWikiServices::getInstance()->getContentLanguage()->getHtmlCode() ],
168 private function siteInfo() {
174 $this->caseSetting(),
175 $this->namespaces() ];
176 return " <siteinfo>\n " .
177 implode(
"\n ", $info ) .
184 private function sitename() {
185 $sitename = MediaWikiServices::getInstance()->getMainConfig()->get(
186 MainConfigNames::Sitename );
187 return Xml::element(
'sitename', [], $sitename );
193 private function dbname() {
194 $dbname = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::DBname );
195 return Xml::element(
'dbname', [], $dbname );
201 private function generator() {
202 return Xml::element(
'generator', [],
'MediaWiki ' .
MW_VERSION );
208 private function homelink() {
209 return Xml::element(
'base', [], Title::newMainPage()->getCanonicalURL() );
215 private function caseSetting() {
216 $capitalLinks = MediaWikiServices::getInstance()->getMainConfig()->get(
217 MainConfigNames::CapitalLinks );
219 $sensitivity = $capitalLinks ?
'first-letter' :
'case-sensitive';
220 return Xml::element(
'case', [], $sensitivity );
226 private function namespaces() {
227 $spaces =
"<namespaces>\n";
228 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
230 MediaWikiServices::getInstance()->getContentLanguage()->getFormattedNamespaces()
234 Xml::element(
'namespace',
237 'case' => $nsInfo->isCapitalized( $ns )
238 ?
'first-letter' :
'case-sensitive',
241 $spaces .=
" </namespaces>";
252 return "</mediawiki>\n";
264 $this->currentTitle = Title::newFromRow( $row );
265 $canonicalTitle = self::canonicalTitle( $this->currentTitle );
266 $out .=
' ' . Xml::elementClean(
'title', [], $canonicalTitle ) .
"\n";
267 $out .=
' ' . Xml::element(
'ns', [], strval( $row->page_namespace ) ) .
"\n";
268 $out .=
' ' . Xml::element(
'id', [], strval( $row->page_id ) ) .
"\n";
269 if ( $row->page_is_redirect ) {
270 $services = MediaWikiServices::getInstance();
271 $page = $services->getWikiPageFactory()->newFromTitle( $this->currentTitle );
272 $redirectStore = $services->getRedirectStore();
273 $redirect = $this->invokeLenient(
274 static function () use ( $page, $redirectStore ) {
275 return $redirectStore->getRedirectTarget( $page );
277 'Failed to get redirect target of page ' . $page->getId()
279 $redirect = Title::castFromLinkTarget( $redirect );
282 $out .= Xml::element(
'redirect', [
'title' => self::canonicalTitle( $redirect ) ] );
286 $this->hookRunner->onXmlDumpWriterOpenPage( $this, $out, $row, $this->currentTitle );
298 if ( $this->currentTitle !==
null ) {
299 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
302 $linkCache->clearLink( $this->currentTitle );
310 private function getRevisionStore() {
311 return MediaWikiServices::getInstance()->getRevisionStore();
317 private function getBlobStore() {
319 return MediaWikiServices::getInstance()->getBlobStore();
331 private function invokeLenient( $callback, $warning ) {
336 }
catch (
MWException | RuntimeException | InvalidArgumentException | ErrorException $ex ) {
337 MWDebug::warning( $warning .
': ' . $ex->getMessage() );
353 $rev = $this->getRevisionStore()->newRevisionFromRowAndSlots(
360 $out =
" <revision>\n";
361 $out .=
" " . Xml::element(
'id',
null, strval( $rev->getId() ) ) .
"\n";
363 if ( $rev->getParentId() ) {
364 $out .=
" " . Xml::element(
'parentid',
null, strval( $rev->getParentId() ) ) .
"\n";
369 if ( $rev->isDeleted( RevisionRecord::DELETED_USER ) ) {
370 $out .=
" " . Xml::element(
'contributor', [
'deleted' =>
'deleted' ] ) .
"\n";
373 $user = $rev->getUser();
375 $user ? $user->getId() : 0,
376 $user ? $user->getName() :
''
380 if ( $rev->isMinor() ) {
381 $out .=
" <minor/>\n";
383 if ( $rev->isDeleted( RevisionRecord::DELETED_COMMENT ) ) {
384 $out .=
" " . Xml::element(
'comment', [
'deleted' =>
'deleted' ] ) .
"\n";
386 if ( $rev->getComment()->text !=
'' ) {
388 . Xml::elementClean(
'comment', [], strval( $rev->getComment()->text ) )
393 $contentMode = $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ? self::WRITE_STUB_DELETED
394 : $this->contentMode;
396 $slots = $rev->getSlots()->getSlots();
400 $out .= $this->writeSlot( $slots[SlotRecord::MAIN], $contentMode );
402 foreach ( $slots as $role => $slot ) {
403 if ( $role === SlotRecord::MAIN ) {
406 $out .= $this->writeSlot( $slot, $contentMode );
409 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
410 $out .=
" <sha1/>\n";
412 $sha1 = $this->invokeLenient(
413 static function () use ( $rev ) {
414 return $rev->getSha1();
416 'failed to determine sha1 for revision ' . $rev->getId()
418 $out .=
" " . Xml::element(
'sha1',
null, strval( $sha1 ) ) .
"\n";
422 if ( $contentMode === self::WRITE_CONTENT ) {
424 $content = $this->invokeLenient(
425 static function () use ( $rev ) {
426 return $rev->getMainContentRaw();
428 'Failed to load main slot content of revision ' . $rev->getId()
431 $text = $content ? $content->
serialize() :
'';
433 $this->hookRunner->onXmlDumpWriterWriteRevision( $this, $out, $row, $text, $rev );
435 $out .=
" </revision>\n";
446 private function writeSlot(
SlotRecord $slot, $contentMode ) {
447 $isMain = $slot->
getRole() === SlotRecord::MAIN;
450 if ( !$isV11 && !$isMain ) {
460 $out .=
' ' . Xml::openElement(
'content' ) .
"\n";
462 $out .= $indent . Xml::element(
'role',
null, strval( $slot->
getRole() ) ) .
"\n";
466 $out .= $indent . Xml::element(
'origin',
null, strval( $slot->
getOrigin() ) ) .
"\n";
470 $contentHandler = MediaWikiServices::getInstance()
471 ->getContentHandlerFactory()
472 ->getContentHandler( $contentModel );
473 $contentFormat = $contentHandler->getDefaultFormat();
477 $out .= $indent . Xml::element(
'model',
null, strval( $contentModel ) ) .
"\n";
478 $out .= $indent . Xml::element(
'format',
null, strval( $contentFormat ) ) .
"\n";
481 'bytes' => $this->invokeLenient(
482 static function () use ( $slot ) {
485 'failed to determine size for slot ' . $slot->
getRole() .
' of revision '
491 $textAttributes[
'sha1'] = $this->invokeLenient(
492 static function () use ( $slot ) {
495 'failed to determine sha1 for slot ' . $slot->
getRole() .
' of revision '
500 if ( $contentMode === self::WRITE_CONTENT ) {
501 $content = $this->invokeLenient(
502 static function () use ( $slot ) {
505 'failed to load content for slot ' . $slot->
getRole() .
' of revision '
509 if ( $content ===
null ) {
510 $out .= $indent . Xml::element(
'text', $textAttributes ) .
"\n";
512 $out .= $this->writeText( $content, $textAttributes, $indent );
514 } elseif ( $contentMode === self::WRITE_STUB_DELETED ) {
516 $textAttributes[
'deleted'] =
'deleted';
517 $out .= $indent . Xml::element(
'text', $textAttributes ) .
"\n";
521 $textAttributes[
'location'] = $slot->
getAddress();
535 [ $schema, $textId ] = $this->getBlobStore()->splitBlobAddress( $slot->
getAddress() );
536 }
catch ( InvalidArgumentException $ex ) {
537 MWDebug::warning(
'Bad content address for slot ' . $slot->
getRole()
538 .
' of revision ' . $slot->
getRevision() .
': ' . $ex->getMessage() );
542 if ( $schema ===
'tt' ) {
543 $textAttributes[
'id'] = $textId;
544 } elseif ( $schema ===
'es' ) {
545 $textAttributes[
'id'] = bin2hex( $textId );
549 $out .= $indent . Xml::element(
'text', $textAttributes ) .
"\n";
553 $out .=
' ' . Xml::closeElement(
'content' ) .
"\n";
566 private function writeText(
Content $content, $textAttributes, $indent ) {
568 $contentFormat = $contentHandler->getDefaultFormat();
573 $data = $content->getText();
575 $data = $content->
serialize( $contentFormat );
578 $data = $contentHandler->exportTransform( $data, $contentFormat );
580 $textAttributes[
'bytes'] = strlen( $data );
581 $textAttributes[
'xml:space'] =
'preserve';
582 return $indent . Xml::elementClean(
'text', $textAttributes, strval( $data ) ) .
"\n";
593 $out =
" <logitem>\n";
594 $out .=
" " . Xml::element(
'id',
null, strval( $row->log_id ) ) .
"\n";
598 if ( $row->log_deleted & LogPage::DELETED_USER ) {
599 $out .=
" " . Xml::element(
'contributor', [
'deleted' =>
'deleted' ] ) .
"\n";
604 if ( $row->log_deleted & LogPage::DELETED_COMMENT ) {
605 $out .=
" " . Xml::element(
'comment', [
'deleted' =>
'deleted' ] ) .
"\n";
607 $comment = $this->commentStore->getComment(
'log_comment', $row )->text;
608 if ( $comment !=
'' ) {
609 $out .=
" " . Xml::elementClean(
'comment',
null, strval( $comment ) ) .
"\n";
613 $out .=
" " . Xml::element(
'type',
null, strval( $row->log_type ) ) .
"\n";
614 $out .=
" " . Xml::element(
'action',
null, strval( $row->log_action ) ) .
"\n";
616 if ( $row->log_deleted & LogPage::DELETED_ACTION ) {
617 $out .=
" " . Xml::element(
'text', [
'deleted' =>
'deleted' ] ) .
"\n";
619 $title = Title::makeTitle( $row->log_namespace, $row->log_title );
620 $out .=
" " . Xml::elementClean(
'logtitle',
null, self::canonicalTitle( $title ) ) .
"\n";
621 $out .=
" " . Xml::elementClean(
'params',
622 [
'xml:space' =>
'preserve' ],
623 strval( $row->log_params ) ) .
"\n";
626 $out .=
" </logitem>\n";
638 return $indent . Xml::element(
'timestamp',
null, $ts ) .
"\n";
648 $out = $indent .
"<contributor>\n";
649 if ( $id || !IPUtils::isValid( $text ) ) {
650 $out .= $indent .
" " . Xml::elementClean(
'username',
null, strval( $text ) ) .
"\n";
651 $out .= $indent .
" " . Xml::element(
'id',
null, strval( $id ) ) .
"\n";
653 $out .= $indent .
" " . Xml::elementClean(
'ip',
null, strval( $text ) ) .
"\n";
655 $out .= $indent .
"</contributor>\n";
666 if ( $row->page_namespace ==
NS_FILE ) {
667 $img = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
668 ->newFile( $row->page_title );
669 if ( $img && $img->exists() ) {
671 foreach ( array_reverse( $img->getHistory() ) as $ver ) {
672 $out .= $this->writeUpload( $ver, $dumpContents );
674 $out .= $this->writeUpload( $img, $dumpContents );
686 private function writeUpload( $file, $dumpContents =
false ) {
687 if ( $file->isOld() ) {
689 '@phan-var OldLocalFile $file';
691 Xml::element(
'archivename',
null, $file->getArchiveName() ) .
"\n";
695 if ( $dumpContents ) {
696 $be = $file->getRepo()->getBackend();
697 # Dump file as base64
698 # Uses only XML-safe characters, so does not need escaping
699 # @todo Too bad this loads the contents into memory (script might swap)
700 $contents =
' <contents encoding="base64">' .
701 chunk_split( base64_encode(
702 $be->getFileContents( [
'src' => $file->getPath() ] ) ) ) .
707 $uploader = $file->getUploader( File::FOR_PUBLIC );
709 $uploader = $this->
writeContributor( $uploader->getId(), $uploader->getName() );
711 $uploader = Xml::element(
'contributor', [
'deleted' =>
'deleted' ] ) .
"\n";
713 $comment = $file->getDescription( File::FOR_PUBLIC );
714 if ( ( $comment ??
'' ) !==
'' ) {
715 $comment = Xml::elementClean(
'comment',
null, $comment );
717 $comment = Xml::element(
'comment', [
'deleted' =>
'deleted' ] );
719 return " <upload>\n" .
722 " " . $comment .
"\n" .
723 " " . Xml::element(
'filename',
null, $file->getName() ) .
"\n" .
725 " " . Xml::element(
'src',
null, $file->getCanonicalUrl() ) .
"\n" .
726 " " . Xml::element(
'size',
null, (
string)( $file->getSize() ?: 0 ) ) .
"\n" .
727 " " .
Xml::
element(
'sha1base36', null, $file->getSha1() ) .
"\n" .
728 " " .
Xml::
element(
'rel', null, $file->getRel() ) .
"\n" .
748 $prefix = MediaWikiServices::getInstance()->getContentLanguage()->
754 if ( $prefix !==
'' ) {
758 return $prefix . $title->
getText();