MediaWiki master
XmlDumpWriter.php
Go to the documentation of this file.
1<?php
41use Wikimedia\Assert\Assert;
42use Wikimedia\IPUtils;
43
48
50 public const WRITE_CONTENT = 0;
51
53 public const WRITE_STUB = 1;
54
59 private const WRITE_STUB_DELETED = 2;
60
65 public static $supportedSchemas = [
68 ];
69
75 private $schemaVersion;
76
82 private $currentTitle = null;
83
87 private $contentMode;
88
90 private $hookRunner;
91
93 private $commentStore;
94
103 public function __construct(
104 $contentMode = self::WRITE_CONTENT,
105 $schemaVersion = XML_DUMP_SCHEMA_VERSION_11,
106 ?HookContainer $hookContainer = null,
107 ?CommentStore $commentStore = null
108 ) {
109 Assert::parameter(
110 in_array( $contentMode, [ self::WRITE_CONTENT, self::WRITE_STUB ], true ),
111 '$contentMode',
112 'must be one of the following constants: WRITE_CONTENT or WRITE_STUB.'
113 );
114
115 Assert::parameter(
116 in_array( $schemaVersion, self::$supportedSchemas, true ),
117 '$schemaVersion',
118 'must be one of the following schema versions: '
119 . implode( ',', self::$supportedSchemas )
120 );
121
122 $this->contentMode = $contentMode;
123 $this->schemaVersion = $schemaVersion;
124 $this->hookRunner = new HookRunner(
125 $hookContainer ?? MediaWikiServices::getInstance()->getHookContainer()
126 );
127 $this->commentStore = $commentStore ?? MediaWikiServices::getInstance()->getCommentStore();
128 }
129
140 public function openStream() {
141 $ver = $this->schemaVersion;
142 return Xml::element( 'mediawiki', [
143 'xmlns' => "http://www.mediawiki.org/xml/export-$ver/",
144 'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance",
145 /*
146 * When a new version of the schema is created, it needs staging on mediawiki.org.
147 * This requires a change in the operations/mediawiki-config git repo.
148 *
149 * Create a changeset like https://gerrit.wikimedia.org/r/#/c/149643/ in which
150 * you copy in the new xsd file.
151 *
152 * After it is reviewed, merged and deployed (sync-docroot), the index.html needs purging.
153 * echo "https://www.mediawiki.org/xml/index.html" | mwscript purgeList.php --wiki=aawiki
154 */
155 'xsi:schemaLocation' => "http://www.mediawiki.org/xml/export-$ver/ " .
156 "http://www.mediawiki.org/xml/export-$ver.xsd",
157 'version' => $ver,
158 'xml:lang' => MediaWikiServices::getInstance()->getContentLanguage()->getHtmlCode() ],
159 null ) .
160 "\n" .
161 $this->siteInfo();
162 }
163
167 private function siteInfo() {
168 $info = [
169 $this->sitename(),
170 $this->dbname(),
171 $this->homelink(),
172 $this->generator(),
173 $this->caseSetting(),
174 $this->namespaces() ];
175 return " <siteinfo>\n " .
176 implode( "\n ", $info ) .
177 "\n </siteinfo>\n";
178 }
179
183 private function sitename() {
184 $sitename = MediaWikiServices::getInstance()->getMainConfig()->get(
185 MainConfigNames::Sitename );
186 return Xml::element( 'sitename', [], $sitename );
187 }
188
192 private function dbname() {
193 $dbname = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::DBname );
194 return Xml::element( 'dbname', [], $dbname );
195 }
196
200 private function generator() {
201 return Xml::element( 'generator', [], 'MediaWiki ' . MW_VERSION );
202 }
203
207 private function homelink() {
208 return Xml::element( 'base', [], Title::newMainPage()->getCanonicalURL() );
209 }
210
214 private function caseSetting() {
215 $capitalLinks = MediaWikiServices::getInstance()->getMainConfig()->get(
216 MainConfigNames::CapitalLinks );
217 // "case-insensitive" option is reserved for future
218 $sensitivity = $capitalLinks ? 'first-letter' : 'case-sensitive';
219 return Xml::element( 'case', [], $sensitivity );
220 }
221
225 private function namespaces() {
226 $spaces = "<namespaces>\n";
227 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
228 foreach (
229 MediaWikiServices::getInstance()->getContentLanguage()->getFormattedNamespaces()
230 as $ns => $title
231 ) {
232 $spaces .= ' ' .
233 Xml::element( 'namespace',
234 [
235 'key' => $ns,
236 'case' => $nsInfo->isCapitalized( $ns )
237 ? 'first-letter' : 'case-sensitive',
238 ], $title ) . "\n";
239 }
240 $spaces .= " </namespaces>";
241 return $spaces;
242 }
243
250 public function closeStream() {
251 return "</mediawiki>\n";
252 }
253
261 public function openPage( $row ) {
262 $out = " <page>\n";
263 $this->currentTitle = Title::newFromRow( $row );
264 $canonicalTitle = self::canonicalTitle( $this->currentTitle );
265 $out .= ' ' . Xml::elementClean( 'title', [], $canonicalTitle ) . "\n";
266 $out .= ' ' . Xml::element( 'ns', [], strval( $row->page_namespace ) ) . "\n";
267 $out .= ' ' . Xml::element( 'id', [], strval( $row->page_id ) ) . "\n";
268 if ( $row->page_is_redirect ) {
269 $services = MediaWikiServices::getInstance();
270 $page = $services->getWikiPageFactory()->newFromTitle( $this->currentTitle );
271 $redirectStore = $services->getRedirectStore();
272 $redirect = $this->invokeLenient(
273 static function () use ( $page, $redirectStore ) {
274 return $redirectStore->getRedirectTarget( $page );
275 },
276 'Failed to get redirect target of page ' . $page->getId()
277 );
278 $redirect = Title::castFromLinkTarget( $redirect );
279 if ( $redirect instanceof Title && $redirect->isValidRedirectTarget() ) {
280 $out .= ' ';
281 $out .= Xml::element( 'redirect', [ 'title' => self::canonicalTitle( $redirect ) ] );
282 $out .= "\n";
283 }
284 }
285 $this->hookRunner->onXmlDumpWriterOpenPage( $this, $out, $row, $this->currentTitle );
286
287 return $out;
288 }
289
296 public function closePage() {
297 if ( $this->currentTitle !== null ) {
298 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
299 // In rare cases, link cache has the same key for some pages which
300 // might be read as part of the same batch. T220424 and T220316
301 $linkCache->clearLink( $this->currentTitle );
302 }
303 return " </page>\n";
304 }
305
309 private function getRevisionStore() {
310 return MediaWikiServices::getInstance()->getRevisionStore();
311 }
312
316 private function getBlobStore() {
317 // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
318 return MediaWikiServices::getInstance()->getBlobStore();
319 }
320
330 private function invokeLenient( $callback, $warning ) {
331 try {
332 return $callback();
333 } catch ( SuppressedDataException $ex ) {
334 return null;
335 } catch ( MWException | RuntimeException | InvalidArgumentException | ErrorException $ex ) {
336 MWDebug::warning( $warning . ': ' . $ex->getMessage() );
337 return null;
338 }
339 }
340
351 public function writeRevision( $row, $slotRows = null ) {
352 $rev = $this->getRevisionStore()->newRevisionFromRowAndSlots(
353 $row,
354 $slotRows,
355 0,
356 $this->currentTitle
357 );
358
359 $out = " <revision>\n";
360 $out .= " " . Xml::element( 'id', null, strval( $rev->getId() ) ) . "\n";
361
362 if ( $rev->getParentId() ) {
363 $out .= " " . Xml::element( 'parentid', null, strval( $rev->getParentId() ) ) . "\n";
364 }
365
366 $out .= $this->writeTimestamp( $rev->getTimestamp() );
367
368 if ( $rev->isDeleted( RevisionRecord::DELETED_USER ) ) {
369 $out .= " " . Xml::element( 'contributor', [ 'deleted' => 'deleted' ] ) . "\n";
370 } else {
371 // empty values get written out as uid 0, see T224221
372 $user = $rev->getUser();
373 $out .= $this->writeContributor(
374 $user ? $user->getId() : 0,
375 $user ? $user->getName() : ''
376 );
377 }
378
379 if ( $rev->isMinor() ) {
380 $out .= " <minor/>\n";
381 }
382 if ( $rev->isDeleted( RevisionRecord::DELETED_COMMENT ) ) {
383 $out .= " " . Xml::element( 'comment', [ 'deleted' => 'deleted' ] ) . "\n";
384 } else {
385 if ( $rev->getComment()->text != '' ) {
386 $out .= " "
387 . Xml::elementClean( 'comment', [], strval( $rev->getComment()->text ) )
388 . "\n";
389 }
390 }
391
392 $contentMode = $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ? self::WRITE_STUB_DELETED
393 : $this->contentMode;
394
395 $slots = $rev->getSlots()->getSlots();
396
397 // use predictable order, put main slot first
398 ksort( $slots );
399 $out .= $this->writeSlot( $slots[SlotRecord::MAIN], $contentMode );
400
401 foreach ( $slots as $role => $slot ) {
402 if ( $role === SlotRecord::MAIN ) {
403 continue;
404 }
405 $out .= $this->writeSlot( $slot, $contentMode );
406 }
407
408 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
409 $out .= " <sha1/>\n";
410 } else {
411 $sha1 = $this->invokeLenient(
412 static function () use ( $rev ) {
413 return $rev->getSha1();
414 },
415 'failed to determine sha1 for revision ' . $rev->getId()
416 );
417 $out .= " " . Xml::element( 'sha1', null, strval( $sha1 ) ) . "\n";
418 }
419
420 $text = '';
421 if ( $contentMode === self::WRITE_CONTENT ) {
423 $content = $this->invokeLenient(
424 static function () use ( $rev ) {
425 return $rev->getContent( SlotRecord::MAIN, RevisionRecord::RAW );
426 },
427 'Failed to load main slot content of revision ' . $rev->getId()
428 );
429
430 $text = $content ? $content->serialize() : '';
431 }
432 $this->hookRunner->onXmlDumpWriterWriteRevision( $this, $out, $row, $text, $rev );
433
434 $out .= " </revision>\n";
435
436 return $out;
437 }
438
445 private function writeSlot( SlotRecord $slot, $contentMode ) {
446 $isMain = $slot->getRole() === SlotRecord::MAIN;
447 $isV11 = $this->schemaVersion >= XML_DUMP_SCHEMA_VERSION_11;
448
449 if ( !$isV11 && !$isMain ) {
450 // ignore extra slots
451 return '';
452 }
453
454 $out = '';
455 $indent = ' ';
456
457 if ( !$isMain ) {
458 // non-main slots are wrapped into an additional element.
459 $out .= ' ' . Xml::openElement( 'content' ) . "\n";
460 $indent .= ' ';
461 $out .= $indent . Xml::element( 'role', null, strval( $slot->getRole() ) ) . "\n";
462 }
463
464 if ( $isV11 ) {
465 $out .= $indent . Xml::element( 'origin', null, strval( $slot->getOrigin() ) ) . "\n";
466 }
467
468 $contentModel = $slot->getModel();
469 $contentHandler = MediaWikiServices::getInstance()
470 ->getContentHandlerFactory()
471 ->getContentHandler( $contentModel );
472 $contentFormat = $contentHandler->getDefaultFormat();
473
474 // XXX: The content format is only relevant when actually outputting serialized content.
475 // It should probably be an attribute on the text tag.
476 $out .= $indent . Xml::element( 'model', null, strval( $contentModel ) ) . "\n";
477 $out .= $indent . Xml::element( 'format', null, strval( $contentFormat ) ) . "\n";
478
479 $textAttributes = [
480 'bytes' => $this->invokeLenient(
481 static function () use ( $slot ) {
482 return $slot->getSize();
483 },
484 'failed to determine size for slot ' . $slot->getRole() . ' of revision '
485 . $slot->getRevision()
486 ) ?: '0'
487 ];
488
489 if ( $isV11 ) {
490 $textAttributes['sha1'] = $this->invokeLenient(
491 static function () use ( $slot ) {
492 return $slot->getSha1();
493 },
494 'failed to determine sha1 for slot ' . $slot->getRole() . ' of revision '
495 . $slot->getRevision()
496 ) ?: '';
497 }
498
499 if ( $contentMode === self::WRITE_CONTENT ) {
500 $content = $this->invokeLenient(
501 static function () use ( $slot ) {
502 return $slot->getContent();
503 },
504 'failed to load content for slot ' . $slot->getRole() . ' of revision '
505 . $slot->getRevision()
506 );
507
508 if ( $content === null ) {
509 $out .= $indent . Xml::element( 'text', $textAttributes ) . "\n";
510 } else {
511 $out .= $this->writeText( $content, $textAttributes, $indent );
512 }
513 } elseif ( $contentMode === self::WRITE_STUB_DELETED ) {
514 // write <text> placeholder tag
515 $textAttributes['deleted'] = 'deleted';
516 $out .= $indent . Xml::element( 'text', $textAttributes ) . "\n";
517 } else {
518 // write <text> stub tag
519 if ( $isV11 ) {
520 $textAttributes['location'] = $slot->getAddress();
521 }
522 $schema = null;
523
524 if ( $isMain ) {
525 // Output the numerical text ID if possible, for backwards compatibility.
526 // Note that this is currently the ONLY reason we have a BlobStore here at all.
527 // When removing this line, check whether the BlobStore has become unused.
528 try {
529 // NOTE: this will only work for addresses of the form "tt:12345" or "es:DB://cluster1/1234".
530 // If we want to support other kinds of addresses in the future,
531 // we will have to silently ignore failures here.
532 // For now, this fails for "tt:0", which is present in the WMF production
533 // database as of July 2019, due to data corruption.
534 [ $schema, $textId ] = $this->getBlobStore()->splitBlobAddress( $slot->getAddress() );
535 } catch ( InvalidArgumentException $ex ) {
536 MWDebug::warning( 'Bad content address for slot ' . $slot->getRole()
537 . ' of revision ' . $slot->getRevision() . ': ' . $ex->getMessage() );
538 $textId = 0;
539 }
540
541 if ( $schema === 'tt' ) {
542 $textAttributes['id'] = $textId;
543 } elseif ( $schema === 'es' ) {
544 $textAttributes['id'] = bin2hex( $textId );
545 }
546 }
547
548 $out .= $indent . Xml::element( 'text', $textAttributes ) . "\n";
549 }
550
551 if ( !$isMain ) {
552 $out .= ' ' . Xml::closeElement( 'content' ) . "\n";
553 }
554
555 return $out;
556 }
557
565 private function writeText( Content $content, $textAttributes, $indent ) {
566 $contentHandler = $content->getContentHandler();
567 $contentFormat = $contentHandler->getDefaultFormat();
568
569 if ( $content instanceof TextContent ) {
570 // HACK: For text based models, bypass the serialization step. This allows extensions (like Flow)
571 // that use incompatible combinations of serialization format and content model.
572 $data = $content->getText();
573 } else {
574 $data = $content->serialize( $contentFormat );
575 }
576
577 $data = $contentHandler->exportTransform( $data, $contentFormat );
578 // make sure to use the actual size
579 $textAttributes['bytes'] = strlen( $data );
580 $textAttributes['xml:space'] = 'preserve';
581 return $indent . Xml::elementClean( 'text', $textAttributes, strval( $data ) ) . "\n";
582 }
583
591 public function writeLogItem( $row ) {
592 $out = " <logitem>\n";
593 $out .= " " . Xml::element( 'id', null, strval( $row->log_id ) ) . "\n";
594
595 $out .= $this->writeTimestamp( $row->log_timestamp, " " );
596
597 if ( $row->log_deleted & LogPage::DELETED_USER ) {
598 $out .= " " . Xml::element( 'contributor', [ 'deleted' => 'deleted' ] ) . "\n";
599 } else {
600 $out .= $this->writeContributor( $row->actor_user, $row->actor_name, " " );
601 }
602
603 if ( $row->log_deleted & LogPage::DELETED_COMMENT ) {
604 $out .= " " . Xml::element( 'comment', [ 'deleted' => 'deleted' ] ) . "\n";
605 } else {
606 $comment = $this->commentStore->getComment( 'log_comment', $row )->text;
607 if ( $comment != '' ) {
608 $out .= " " . Xml::elementClean( 'comment', null, strval( $comment ) ) . "\n";
609 }
610 }
611
612 $out .= " " . Xml::element( 'type', null, strval( $row->log_type ) ) . "\n";
613 $out .= " " . Xml::element( 'action', null, strval( $row->log_action ) ) . "\n";
614
615 if ( $row->log_deleted & LogPage::DELETED_ACTION ) {
616 $out .= " " . Xml::element( 'text', [ 'deleted' => 'deleted' ] ) . "\n";
617 } else {
618 $title = Title::makeTitle( $row->log_namespace, $row->log_title );
619 $out .= " " . Xml::elementClean( 'logtitle', null, self::canonicalTitle( $title ) ) . "\n";
620 $out .= " " . Xml::elementClean( 'params',
621 [ 'xml:space' => 'preserve' ],
622 strval( $row->log_params ) ) . "\n";
623 }
624
625 $out .= " </logitem>\n";
626
627 return $out;
628 }
629
635 public function writeTimestamp( $timestamp, $indent = " " ) {
636 $ts = wfTimestamp( TS_ISO_8601, $timestamp );
637 return $indent . Xml::element( 'timestamp', null, $ts ) . "\n";
638 }
639
646 public function writeContributor( $id, $text, $indent = " " ) {
647 $out = $indent . "<contributor>\n";
648 if ( $id || !IPUtils::isValid( $text ) ) {
649 $out .= $indent . " " . Xml::elementClean( 'username', null, strval( $text ) ) . "\n";
650 $out .= $indent . " " . Xml::element( 'id', null, strval( $id ) ) . "\n";
651 } else {
652 $out .= $indent . " " . Xml::elementClean( 'ip', null, strval( $text ) ) . "\n";
653 }
654 $out .= $indent . "</contributor>\n";
655 return $out;
656 }
657
664 public function writeUploads( $row, $dumpContents = false ) {
665 if ( $row->page_namespace == NS_FILE ) {
666 $img = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
667 ->newFile( $row->page_title );
668 if ( $img && $img->exists() ) {
669 $out = '';
670 foreach ( array_reverse( $img->getHistory() ) as $ver ) {
671 $out .= $this->writeUpload( $ver, $dumpContents );
672 }
673 $out .= $this->writeUpload( $img, $dumpContents );
674 return $out;
675 }
676 }
677 return '';
678 }
679
685 private function writeUpload( $file, $dumpContents = false ) {
686 if ( $file->isOld() ) {
688 '@phan-var OldLocalFile $file';
689 $archiveName = " " .
690 Xml::element( 'archivename', null, $file->getArchiveName() ) . "\n";
691 } else {
692 $archiveName = '';
693 }
694 if ( $dumpContents ) {
695 $be = $file->getRepo()->getBackend();
696 # Dump file as base64
697 # Uses only XML-safe characters, so does not need escaping
698 # @todo Too bad this loads the contents into memory (script might swap)
699 $contents = ' <contents encoding="base64">' .
700 chunk_split( base64_encode(
701 $be->getFileContents( [ 'src' => $file->getPath() ] ) ) ) .
702 " </contents>\n";
703 } else {
704 $contents = '';
705 }
706 $uploader = $file->getUploader( File::FOR_PUBLIC );
707 if ( $uploader ) {
708 $uploader = $this->writeContributor( $uploader->getId(), $uploader->getName() );
709 } else {
710 $uploader = Xml::element( 'contributor', [ 'deleted' => 'deleted' ] ) . "\n";
711 }
712 $comment = $file->getDescription( File::FOR_PUBLIC );
713 if ( ( $comment ?? '' ) !== '' ) {
714 $comment = Xml::elementClean( 'comment', null, $comment );
715 } else {
716 $comment = Xml::element( 'comment', [ 'deleted' => 'deleted' ] );
717 }
718 return " <upload>\n" .
719 $this->writeTimestamp( $file->getTimestamp() ) .
720 $uploader .
721 " " . $comment . "\n" .
722 " " . Xml::element( 'filename', null, $file->getName() ) . "\n" .
723 $archiveName .
724 " " . Xml::element( 'src', null, $file->getCanonicalUrl() ) . "\n" .
725 " " . Xml::element( 'size', null, (string)( $file->getSize() ?: 0 ) ) . "\n" .
726 " " . Xml::element( 'sha1base36', null, $file->getSha1() ) . "\n" .
727 " " . Xml::element( 'rel', null, $file->getRel() ) . "\n" .
728 $contents .
729 " </upload>\n";
730 }
731
742 public static function canonicalTitle( Title $title ) {
743 if ( $title->isExternal() ) {
744 return $title->getPrefixedText();
745 }
746
747 $prefix = MediaWikiServices::getInstance()->getContentLanguage()->
748 getFormattedNsText( $title->getNamespace() );
749
750 // @todo Emit some kind of warning to the user if $title->getNamespace() !==
751 // NS_MAIN and $prefix === '' (viz. pages in an unregistered namespace)
752
753 if ( $prefix !== '' ) {
754 $prefix .= ':';
755 }
756
757 return $prefix . $title->getText();
758 }
759}
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:328
const XML_DUMP_SCHEMA_VERSION_10
Definition Defines.php:327
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
MediaWiki exception.
Handle database storage of comments such as edit summaries and log reasons.
Content object implementation for representing flat text.
Debug toolbar.
Definition MWDebug.php:48
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
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:79
isValidRedirectTarget()
Check if this Title is a valid redirect target.
Definition Title.php:3465
getNamespace()
Get the namespace index, i.e.
Definition Title.php:1045
getText()
Get the text form (spaces not underscores) of the main part.
Definition Title.php:1018
getPrefixedText()
Get the prefixed title with spaces.
Definition Title.php:1862
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=" ")
Base interface for representing page content.
Definition Content.php:37
getContentHandler()
Convenience method that returns the ContentHandler singleton for handling the content model that this...
serialize( $format=null)
Convenience method for serializing this Content object.
isExternal()
Whether this LinkTarget has an interwiki component.
element(SerializerNode $parent, SerializerNode $node, $contents)