MediaWiki master
XmlDumpWriter.php
Go to the documentation of this file.
1<?php
46use Wikimedia\Assert\Assert;
47use Wikimedia\IPUtils;
48
53
55 public const WRITE_CONTENT = 0;
56
58 public const WRITE_STUB = 1;
59
64 private const WRITE_STUB_DELETED = 2;
65
70 public static $supportedSchemas = [
73 ];
74
80 private $schemaVersion;
81
87 private $currentTitle = null;
88
92 private $contentMode;
93
95 private $hookRunner;
96
98 private $commentStore;
99
108 public function __construct(
109 $contentMode = self::WRITE_CONTENT,
110 $schemaVersion = XML_DUMP_SCHEMA_VERSION_11,
111 ?HookContainer $hookContainer = null,
112 ?CommentStore $commentStore = null
113 ) {
114 Assert::parameter(
115 in_array( $contentMode, [ self::WRITE_CONTENT, self::WRITE_STUB ], true ),
116 '$contentMode',
117 'must be one of the following constants: WRITE_CONTENT or WRITE_STUB.'
118 );
119
120 Assert::parameter(
121 in_array( $schemaVersion, self::$supportedSchemas, true ),
122 '$schemaVersion',
123 'must be one of the following schema versions: '
124 . implode( ',', self::$supportedSchemas )
125 );
126
127 $this->contentMode = $contentMode;
128 $this->schemaVersion = $schemaVersion;
129 $this->hookRunner = new HookRunner(
130 $hookContainer ?? MediaWikiServices::getInstance()->getHookContainer()
131 );
132 $this->commentStore = $commentStore ?? MediaWikiServices::getInstance()->getCommentStore();
133 }
134
145 public function openStream() {
146 $ver = $this->schemaVersion;
147 return Xml::element( 'mediawiki', [
148 'xmlns' => "http://www.mediawiki.org/xml/export-$ver/",
149 'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance",
150 /*
151 * When a new version of the schema is created, it needs staging on mediawiki.org.
152 * This requires a change in the operations/mediawiki-config git repo.
153 *
154 * Create a changeset like https://gerrit.wikimedia.org/r/#/c/149643/ in which
155 * you copy in the new xsd file.
156 *
157 * After it is reviewed, merged and deployed (sync-docroot), the index.html needs purging.
158 * echo "https://www.mediawiki.org/xml/index.html" | mwscript purgeList.php --wiki=aawiki
159 */
160 'xsi:schemaLocation' => "http://www.mediawiki.org/xml/export-$ver/ " .
161 "http://www.mediawiki.org/xml/export-$ver.xsd",
162 'version' => $ver,
163 'xml:lang' => MediaWikiServices::getInstance()->getContentLanguage()->getHtmlCode() ],
164 null ) .
165 "\n" .
166 $this->siteInfo();
167 }
168
172 private function siteInfo() {
173 $info = [
174 $this->sitename(),
175 $this->dbname(),
176 $this->homelink(),
177 $this->generator(),
178 $this->caseSetting(),
179 $this->namespaces() ];
180 return " <siteinfo>\n " .
181 implode( "\n ", $info ) .
182 "\n </siteinfo>\n";
183 }
184
188 private function sitename() {
189 $sitename = MediaWikiServices::getInstance()->getMainConfig()->get(
190 MainConfigNames::Sitename );
191 return Xml::element( 'sitename', [], $sitename );
192 }
193
197 private function dbname() {
198 $dbname = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::DBname );
199 return Xml::element( 'dbname', [], $dbname );
200 }
201
205 private function generator() {
206 return Xml::element( 'generator', [], 'MediaWiki ' . MW_VERSION );
207 }
208
212 private function homelink() {
213 return Xml::element( 'base', [], Title::newMainPage()->getCanonicalURL() );
214 }
215
219 private function caseSetting() {
220 $capitalLinks = MediaWikiServices::getInstance()->getMainConfig()->get(
221 MainConfigNames::CapitalLinks );
222 // "case-insensitive" option is reserved for future
223 $sensitivity = $capitalLinks ? 'first-letter' : 'case-sensitive';
224 return Xml::element( 'case', [], $sensitivity );
225 }
226
230 private function namespaces() {
231 $spaces = "<namespaces>\n";
232 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
233 foreach (
234 MediaWikiServices::getInstance()->getContentLanguage()->getFormattedNamespaces()
235 as $ns => $title
236 ) {
237 $spaces .= ' ' .
238 Xml::element( 'namespace',
239 [
240 'key' => $ns,
241 'case' => $nsInfo->isCapitalized( $ns )
242 ? 'first-letter' : 'case-sensitive',
243 ], $title ) . "\n";
244 }
245 $spaces .= " </namespaces>";
246 return $spaces;
247 }
248
255 public function closeStream() {
256 return "</mediawiki>\n";
257 }
258
266 public function openPage( $row ) {
267 $out = " <page>\n";
268 $this->currentTitle = Title::newFromRow( $row );
269 $canonicalTitle = self::canonicalTitle( $this->currentTitle );
270 $out .= ' ' . Xml::elementClean( 'title', [], $canonicalTitle ) . "\n";
271 $out .= ' ' . Xml::element( 'ns', [], strval( $row->page_namespace ) ) . "\n";
272 $out .= ' ' . Xml::element( 'id', [], strval( $row->page_id ) ) . "\n";
273 if ( $row->page_is_redirect ) {
274 $services = MediaWikiServices::getInstance();
275 $page = $services->getWikiPageFactory()->newFromTitle( $this->currentTitle );
276 $redirectStore = $services->getRedirectStore();
277 $redirect = $this->invokeLenient(
278 static function () use ( $page, $redirectStore ) {
279 return $redirectStore->getRedirectTarget( $page );
280 },
281 'Failed to get redirect target of page ' . $page->getId()
282 );
283 $redirect = Title::castFromLinkTarget( $redirect );
284 if ( $redirect instanceof Title && $redirect->isValidRedirectTarget() ) {
285 $out .= ' ';
286 $out .= Xml::element( 'redirect', [ 'title' => self::canonicalTitle( $redirect ) ] );
287 $out .= "\n";
288 }
289 }
290 $this->hookRunner->onXmlDumpWriterOpenPage( $this, $out, $row, $this->currentTitle );
291
292 return $out;
293 }
294
301 public function closePage() {
302 if ( $this->currentTitle !== null ) {
303 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
304 // In rare cases, link cache has the same key for some pages which
305 // might be read as part of the same batch. T220424 and T220316
306 $linkCache->clearLink( $this->currentTitle );
307 }
308 return " </page>\n";
309 }
310
314 private function getRevisionStore() {
315 return MediaWikiServices::getInstance()->getRevisionStore();
316 }
317
321 private function getBlobStore() {
322 // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
323 return MediaWikiServices::getInstance()->getBlobStore();
324 }
325
335 private function invokeLenient( $callback, $warning ) {
336 try {
337 return $callback();
338 } catch ( SuppressedDataException ) {
339 return null;
340 } catch ( MWException | RuntimeException | InvalidArgumentException | ErrorException $ex ) {
341 MWDebug::warning( $warning . ': ' . $ex->getMessage() );
342 return null;
343 }
344 }
345
356 public function writeRevision( $row, $slotRows = null ) {
357 $rev = $this->getRevisionStore()->newRevisionFromRowAndSlots(
358 $row,
359 $slotRows,
360 0,
361 $this->currentTitle
362 );
363
364 $out = " <revision>\n";
365 $out .= " " . Xml::element( 'id', null, strval( $rev->getId() ) ) . "\n";
366
367 if ( $rev->getParentId() ) {
368 $out .= " " . Xml::element( 'parentid', null, strval( $rev->getParentId() ) ) . "\n";
369 }
370
371 $out .= $this->writeTimestamp( $rev->getTimestamp() );
372
373 if ( $rev->isDeleted( RevisionRecord::DELETED_USER ) ) {
374 $out .= " " . Xml::element( 'contributor', [ 'deleted' => 'deleted' ] ) . "\n";
375 } else {
376 // empty values get written out as uid 0, see T224221
377 $user = $rev->getUser();
378 $out .= $this->writeContributor(
379 $user ? $user->getId() : 0,
380 $user ? $user->getName() : ''
381 );
382 }
383
384 if ( $rev->isMinor() ) {
385 $out .= " <minor/>\n";
386 }
387 if ( $rev->isDeleted( RevisionRecord::DELETED_COMMENT ) ) {
388 $out .= " " . Xml::element( 'comment', [ 'deleted' => 'deleted' ] ) . "\n";
389 } else {
390 if ( $rev->getComment()->text != '' ) {
391 $out .= " "
392 . Xml::elementClean( 'comment', [], strval( $rev->getComment()->text ) )
393 . "\n";
394 }
395 }
396
397 $contentMode = $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ? self::WRITE_STUB_DELETED
398 : $this->contentMode;
399
400 $slots = $rev->getSlots()->getSlots();
401
402 // use predictable order, put main slot first
403 ksort( $slots );
404 $out .= $this->writeSlot( $slots[SlotRecord::MAIN], $contentMode );
405
406 foreach ( $slots as $role => $slot ) {
407 if ( $role === SlotRecord::MAIN ) {
408 continue;
409 }
410 $out .= $this->writeSlot( $slot, $contentMode );
411 }
412
413 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
414 $out .= " <sha1/>\n";
415 } else {
416 $sha1 = $this->invokeLenient(
417 static function () use ( $rev ) {
418 return $rev->getSha1();
419 },
420 'failed to determine sha1 for revision ' . $rev->getId()
421 );
422 $out .= " " . Xml::element( 'sha1', null, strval( $sha1 ) ) . "\n";
423 }
424
425 $text = '';
426 if ( $contentMode === self::WRITE_CONTENT ) {
428 $content = $this->invokeLenient(
429 static function () use ( $rev ) {
430 return $rev->getMainContentRaw();
431 },
432 'Failed to load main slot content of revision ' . $rev->getId()
433 );
434
435 $text = $content ? $content->serialize() : '';
436 }
437 $this->hookRunner->onXmlDumpWriterWriteRevision( $this, $out, $row, $text, $rev );
438
439 $out .= " </revision>\n";
440
441 return $out;
442 }
443
450 private function writeSlot( SlotRecord $slot, $contentMode ) {
451 $isMain = $slot->getRole() === SlotRecord::MAIN;
452 $isV11 = $this->schemaVersion >= XML_DUMP_SCHEMA_VERSION_11;
453
454 if ( !$isV11 && !$isMain ) {
455 // ignore extra slots
456 return '';
457 }
458
459 $out = '';
460 $indent = ' ';
461
462 if ( !$isMain ) {
463 // non-main slots are wrapped into an additional element.
464 $out .= ' ' . Xml::openElement( 'content' ) . "\n";
465 $indent .= ' ';
466 $out .= $indent . Xml::element( 'role', null, strval( $slot->getRole() ) ) . "\n";
467 }
468
469 if ( $isV11 ) {
470 $out .= $indent . Xml::element( 'origin', null, strval( $slot->getOrigin() ) ) . "\n";
471 }
472
473 $contentModel = $slot->getModel();
474 $contentHandler = MediaWikiServices::getInstance()
475 ->getContentHandlerFactory()
476 ->getContentHandler( $contentModel );
477 $contentFormat = $contentHandler->getDefaultFormat();
478
479 // XXX: The content format is only relevant when actually outputting serialized content.
480 // It should probably be an attribute on the text tag.
481 $out .= $indent . Xml::element( 'model', null, strval( $contentModel ) ) . "\n";
482 $out .= $indent . Xml::element( 'format', null, strval( $contentFormat ) ) . "\n";
483
484 $textAttributes = [
485 'bytes' => $this->invokeLenient(
486 static function () use ( $slot ) {
487 return $slot->getSize();
488 },
489 'failed to determine size for slot ' . $slot->getRole() . ' of revision '
490 . $slot->getRevision()
491 ) ?: '0'
492 ];
493
494 if ( $isV11 ) {
495 $textAttributes['sha1'] = $this->invokeLenient(
496 static function () use ( $slot ) {
497 return $slot->getSha1();
498 },
499 'failed to determine sha1 for slot ' . $slot->getRole() . ' of revision '
500 . $slot->getRevision()
501 ) ?: '';
502 }
503
504 if ( $contentMode === self::WRITE_CONTENT ) {
505 $content = $this->invokeLenient(
506 static function () use ( $slot ) {
507 return $slot->getContent();
508 },
509 'failed to load content for slot ' . $slot->getRole() . ' of revision '
510 . $slot->getRevision()
511 );
512
513 if ( $content === null ) {
514 $out .= $indent . Xml::element( 'text', $textAttributes ) . "\n";
515 } else {
516 $out .= $this->writeText( $content, $textAttributes, $indent );
517 }
518 } elseif ( $contentMode === self::WRITE_STUB_DELETED ) {
519 // write <text> placeholder tag
520 $textAttributes['deleted'] = 'deleted';
521 $out .= $indent . Xml::element( 'text', $textAttributes ) . "\n";
522 } else {
523 // write <text> stub tag
524 if ( $isV11 ) {
525 $textAttributes['location'] = $slot->getAddress();
526 }
527 $schema = null;
528
529 if ( $isMain ) {
530 // Output the numerical text ID if possible, for backwards compatibility.
531 // Note that this is currently the ONLY reason we have a BlobStore here at all.
532 // When removing this line, check whether the BlobStore has become unused.
533 try {
534 // NOTE: this will only work for addresses of the form "tt:12345" or "es:DB://cluster1/1234".
535 // If we want to support other kinds of addresses in the future,
536 // we will have to silently ignore failures here.
537 // For now, this fails for "tt:0", which is present in the WMF production
538 // database as of July 2019, due to data corruption.
539 [ $schema, $textId ] = $this->getBlobStore()->splitBlobAddress( $slot->getAddress() );
540 } catch ( InvalidArgumentException $ex ) {
541 MWDebug::warning( 'Bad content address for slot ' . $slot->getRole()
542 . ' of revision ' . $slot->getRevision() . ': ' . $ex->getMessage() );
543 $textId = 0;
544 }
545
546 if ( $schema === 'tt' ) {
547 $textAttributes['id'] = $textId;
548 } elseif ( $schema === 'es' ) {
549 $textAttributes['id'] = bin2hex( $textId );
550 }
551 }
552
553 $out .= $indent . Xml::element( 'text', $textAttributes ) . "\n";
554 }
555
556 if ( !$isMain ) {
557 $out .= ' ' . Xml::closeElement( 'content' ) . "\n";
558 }
559
560 return $out;
561 }
562
570 private function writeText( Content $content, $textAttributes, $indent ) {
571 $contentHandler = $content->getContentHandler();
572 $contentFormat = $contentHandler->getDefaultFormat();
573
574 if ( $content instanceof TextContent ) {
575 // HACK: For text based models, bypass the serialization step. This allows extensions (like Flow)
576 // that use incompatible combinations of serialization format and content model.
577 $data = $content->getText();
578 } else {
579 $data = $content->serialize( $contentFormat );
580 }
581
582 $data = $contentHandler->exportTransform( $data, $contentFormat );
583 // make sure to use the actual size
584 $textAttributes['bytes'] = strlen( $data );
585 $textAttributes['xml:space'] = 'preserve';
586 return $indent . Xml::elementClean( 'text', $textAttributes, strval( $data ) ) . "\n";
587 }
588
596 public function writeLogItem( $row ) {
597 $out = " <logitem>\n";
598 $out .= " " . Xml::element( 'id', null, strval( $row->log_id ) ) . "\n";
599
600 $out .= $this->writeTimestamp( $row->log_timestamp, " " );
601
602 if ( $row->log_deleted & LogPage::DELETED_USER ) {
603 $out .= " " . Xml::element( 'contributor', [ 'deleted' => 'deleted' ] ) . "\n";
604 } else {
605 $out .= $this->writeContributor( $row->actor_user, $row->actor_name, " " );
606 }
607
608 if ( $row->log_deleted & LogPage::DELETED_COMMENT ) {
609 $out .= " " . Xml::element( 'comment', [ 'deleted' => 'deleted' ] ) . "\n";
610 } else {
611 $comment = $this->commentStore->getComment( 'log_comment', $row )->text;
612 if ( $comment != '' ) {
613 $out .= " " . Xml::elementClean( 'comment', null, strval( $comment ) ) . "\n";
614 }
615 }
616
617 $out .= " " . Xml::element( 'type', null, strval( $row->log_type ) ) . "\n";
618 $out .= " " . Xml::element( 'action', null, strval( $row->log_action ) ) . "\n";
619
620 if ( $row->log_deleted & LogPage::DELETED_ACTION ) {
621 $out .= " " . Xml::element( 'text', [ 'deleted' => 'deleted' ] ) . "\n";
622 } else {
623 $title = Title::makeTitle( $row->log_namespace, $row->log_title );
624 $out .= " " . Xml::elementClean( 'logtitle', null, self::canonicalTitle( $title ) ) . "\n";
625 $out .= " " . Xml::elementClean( 'params',
626 [ 'xml:space' => 'preserve' ],
627 strval( $row->log_params ) ) . "\n";
628 }
629
630 $out .= " </logitem>\n";
631
632 return $out;
633 }
634
640 public function writeTimestamp( $timestamp, $indent = " " ) {
641 $ts = wfTimestamp( TS_ISO_8601, $timestamp );
642 return $indent . Xml::element( 'timestamp', null, $ts ) . "\n";
643 }
644
651 public function writeContributor( $id, $text, $indent = " " ) {
652 $out = $indent . "<contributor>\n";
653 if ( $id || !IPUtils::isValid( $text ) ) {
654 $out .= $indent . " " . Xml::elementClean( 'username', null, strval( $text ) ) . "\n";
655 $out .= $indent . " " . Xml::element( 'id', null, strval( $id ) ) . "\n";
656 } else {
657 $out .= $indent . " " . Xml::elementClean( 'ip', null, strval( $text ) ) . "\n";
658 }
659 $out .= $indent . "</contributor>\n";
660 return $out;
661 }
662
669 public function writeUploads( $row, $dumpContents = false ) {
670 if ( $row->page_namespace == NS_FILE ) {
671 $img = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
672 ->newFile( $row->page_title );
673 if ( $img && $img->exists() ) {
674 $out = '';
675 foreach ( array_reverse( $img->getHistory() ) as $ver ) {
676 $out .= $this->writeUpload( $ver, $dumpContents );
677 }
678 $out .= $this->writeUpload( $img, $dumpContents );
679 return $out;
680 }
681 }
682 return '';
683 }
684
690 private function writeUpload( $file, $dumpContents = false ) {
691 if ( $file->isOld() ) {
693 '@phan-var OldLocalFile $file';
694 $archiveName = " " .
695 Xml::element( 'archivename', null, $file->getArchiveName() ) . "\n";
696 } else {
697 $archiveName = '';
698 }
699 if ( $dumpContents ) {
700 $be = $file->getRepo()->getBackend();
701 # Dump file as base64
702 # Uses only XML-safe characters, so does not need escaping
703 # @todo Too bad this loads the contents into memory (script might swap)
704 $contents = ' <contents encoding="base64">' .
705 chunk_split( base64_encode(
706 $be->getFileContents( [ 'src' => $file->getPath() ] ) ) ) .
707 " </contents>\n";
708 } else {
709 $contents = '';
710 }
711 $uploader = $file->getUploader( File::FOR_PUBLIC );
712 if ( $uploader ) {
713 $uploader = $this->writeContributor( $uploader->getId(), $uploader->getName() );
714 } else {
715 $uploader = Xml::element( 'contributor', [ 'deleted' => 'deleted' ] ) . "\n";
716 }
717 $comment = $file->getDescription( File::FOR_PUBLIC );
718 if ( ( $comment ?? '' ) !== '' ) {
719 $comment = Xml::elementClean( 'comment', null, $comment );
720 } else {
721 $comment = Xml::element( 'comment', [ 'deleted' => 'deleted' ] );
722 }
723 return " <upload>\n" .
724 $this->writeTimestamp( $file->getTimestamp() ) .
725 $uploader .
726 " " . $comment . "\n" .
727 " " . Xml::element( 'filename', null, $file->getName() ) . "\n" .
728 $archiveName .
729 " " . Xml::element( 'src', null, $file->getCanonicalUrl() ) . "\n" .
730 " " . Xml::element( 'size', null, (string)( $file->getSize() ?: 0 ) ) . "\n" .
731 " " . Xml::element( 'sha1base36', null, $file->getSha1() ) . "\n" .
732 " " . Xml::element( 'rel', null, $file->getRel() ) . "\n" .
733 $contents .
734 " </upload>\n";
735 }
736
747 public static function canonicalTitle( Title $title ) {
748 if ( $title->isExternal() ) {
749 return $title->getPrefixedText();
750 }
751
752 $prefix = MediaWikiServices::getInstance()->getContentLanguage()->
753 getFormattedNsText( $title->getNamespace() );
754
755 // @todo Emit some kind of warning to the user if $title->getNamespace() !==
756 // NS_MAIN and $prefix === '' (viz. pages in an unregistered namespace)
757
758 if ( $prefix !== '' ) {
759 $prefix .= ':';
760 }
761
762 return $prefix . $title->getText();
763 }
764}
const NS_FILE
Definition Defines.php:71
const MW_VERSION
The running version of MediaWiki.
Definition Defines.php:37
const XML_DUMP_SCHEMA_VERSION_11
Definition Defines.php:359
const XML_DUMP_SCHEMA_VERSION_10
Definition Defines.php:358
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Handle database storage of comments such as edit summaries and log reasons.
Content object implementation for representing flat text.
Debug toolbar.
Definition MWDebug.php:49
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:93
Old file in the oldimage table.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Class to simplify the use of log pages.
Definition LogPage.php:50
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Exception representing a failure to look up a revision.
Page revision base class.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
getContent()
Returns the Content of the given slot.
getRole()
Returns the role of the slot.
getSha1()
Returns the content size.
getSize()
Returns the content size.
getAddress()
Returns the address of this slot's content.
getModel()
Returns the content model.
getOrigin()
Returns the revision ID of the revision that originated the slot's content.
getRevision()
Returns the ID of the revision this slot is associated with.
Exception raised in response to an audience check when attempting to access suppressed information wi...
Service for storing and loading Content objects representing revision data blobs.
Represents a title within MediaWiki.
Definition Title.php:78
isValidRedirectTarget()
Check if this Title is a valid redirect target.
Definition Title.php:3472
getNamespace()
Get the namespace index, i.e.
Definition Title.php:1046
getText()
Get the text form (spaces not underscores) of the main part.
Definition Title.php:1019
getPrefixedText()
Get the prefixed title with spaces.
Definition Title.php:1859
Module of static functions for generating XML.
Definition Xml.php:37
closeStream()
Closes the output stream with the closing root element.
static string[] $supportedSchemas
the schema versions supported for output @final
static canonicalTitle(Title $title)
Return prefixed text form of title, but using the content language's canonical namespace.
const WRITE_STUB
Only output subs for revision content.
writeLogItem( $row)
Dumps a "<logitem>" section on the output stream, with data filled in from the given database row.
writeTimestamp( $timestamp, $indent=" ")
const WRITE_CONTENT
Output serialized revision content.
writeUploads( $row, $dumpContents=false)
Warning! This data is potentially inconsistent.
closePage()
Closes a "<page>" section on the output stream.
openStream()
Opens the XML output stream's root "<mediawiki>" element.
writeRevision( $row, $slotRows=null)
Dumps a "<revision>" section on the output stream, with data filled in from the given database row.
openPage( $row)
Opens a "<page>" section on the output stream, with data from the given database row.
__construct( $contentMode=self::WRITE_CONTENT, $schemaVersion=XML_DUMP_SCHEMA_VERSION_11, ?HookContainer $hookContainer=null, ?CommentStore $commentStore=null)
writeContributor( $id, $text, $indent=" ")
Content objects represent page content, e.g.
Definition Content.php:42
serialize( $format=null)
Serialize this Content object.
getContentHandler()
Convenience method that returns the ContentHandler singleton for handling the content model that this...
isExternal()
Whether this LinkTarget has an interwiki component.
element(SerializerNode $parent, SerializerNode $node, $contents)