MediaWiki master
XmlDumpWriter.php
Go to the documentation of this file.
1<?php
38use Wikimedia\Assert\Assert;
39use Wikimedia\IPUtils;
40
45
47 public const WRITE_CONTENT = 0;
48
50 public const WRITE_STUB = 1;
51
56 private const WRITE_STUB_DELETED = 2;
57
62 public static $supportedSchemas = [
65 ];
66
72 private $schemaVersion;
73
79 private $currentTitle = null;
80
84 private $contentMode;
85
87 private $hookRunner;
88
90 private $commentStore;
91
100 public function __construct(
101 $contentMode = self::WRITE_CONTENT,
102 $schemaVersion = XML_DUMP_SCHEMA_VERSION_11,
103 ?HookContainer $hookContainer = null,
104 ?CommentStore $commentStore = null
105 ) {
106 Assert::parameter(
107 in_array( $contentMode, [ self::WRITE_CONTENT, self::WRITE_STUB ], true ),
108 '$contentMode',
109 'must be one of the following constants: WRITE_CONTENT or WRITE_STUB.'
110 );
111
112 Assert::parameter(
113 in_array( $schemaVersion, self::$supportedSchemas, true ),
114 '$schemaVersion',
115 'must be one of the following schema versions: '
116 . implode( ',', self::$supportedSchemas )
117 );
118
119 $this->contentMode = $contentMode;
120 $this->schemaVersion = $schemaVersion;
121 $this->hookRunner = new HookRunner(
122 $hookContainer ?? MediaWikiServices::getInstance()->getHookContainer()
123 );
124 $this->commentStore = $commentStore ?? MediaWikiServices::getInstance()->getCommentStore();
125 }
126
137 public function openStream() {
138 $ver = $this->schemaVersion;
139 return Xml::element( 'mediawiki', [
140 'xmlns' => "http://www.mediawiki.org/xml/export-$ver/",
141 'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance",
142 /*
143 * When a new version of the schema is created, it needs staging on mediawiki.org.
144 * This requires a change in the operations/mediawiki-config git repo.
145 *
146 * Create a changeset like https://gerrit.wikimedia.org/r/#/c/149643/ in which
147 * you copy in the new xsd file.
148 *
149 * After it is reviewed, merged and deployed (sync-docroot), the index.html needs purging.
150 * echo "https://www.mediawiki.org/xml/index.html" | mwscript purgeList.php --wiki=aawiki
151 */
152 'xsi:schemaLocation' => "http://www.mediawiki.org/xml/export-$ver/ " .
153 "http://www.mediawiki.org/xml/export-$ver.xsd",
154 'version' => $ver,
155 'xml:lang' => MediaWikiServices::getInstance()->getContentLanguage()->getHtmlCode() ],
156 null ) .
157 "\n" .
158 $this->siteInfo();
159 }
160
164 private function siteInfo() {
165 $info = [
166 $this->sitename(),
167 $this->dbname(),
168 $this->homelink(),
169 $this->generator(),
170 $this->caseSetting(),
171 $this->namespaces() ];
172 return " <siteinfo>\n " .
173 implode( "\n ", $info ) .
174 "\n </siteinfo>\n";
175 }
176
180 private function sitename() {
181 $sitename = MediaWikiServices::getInstance()->getMainConfig()->get(
182 MainConfigNames::Sitename );
183 return Xml::element( 'sitename', [], $sitename );
184 }
185
189 private function dbname() {
190 $dbname = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::DBname );
191 return Xml::element( 'dbname', [], $dbname );
192 }
193
197 private function generator() {
198 return Xml::element( 'generator', [], 'MediaWiki ' . MW_VERSION );
199 }
200
204 private function homelink() {
205 return Xml::element( 'base', [], Title::newMainPage()->getCanonicalURL() );
206 }
207
211 private function caseSetting() {
212 $capitalLinks = MediaWikiServices::getInstance()->getMainConfig()->get(
213 MainConfigNames::CapitalLinks );
214 // "case-insensitive" option is reserved for future
215 $sensitivity = $capitalLinks ? 'first-letter' : 'case-sensitive';
216 return Xml::element( 'case', [], $sensitivity );
217 }
218
222 private function namespaces() {
223 $spaces = "<namespaces>\n";
224 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
225 foreach (
226 MediaWikiServices::getInstance()->getContentLanguage()->getFormattedNamespaces()
227 as $ns => $title
228 ) {
229 $spaces .= ' ' .
230 Xml::element( 'namespace',
231 [
232 'key' => $ns,
233 'case' => $nsInfo->isCapitalized( $ns )
234 ? 'first-letter' : 'case-sensitive',
235 ], $title ) . "\n";
236 }
237 $spaces .= " </namespaces>";
238 return $spaces;
239 }
240
247 public function closeStream() {
248 return "</mediawiki>\n";
249 }
250
258 public function openPage( $row ) {
259 $out = " <page>\n";
260 $this->currentTitle = Title::newFromRow( $row );
261 $canonicalTitle = self::canonicalTitle( $this->currentTitle );
262 $out .= ' ' . Xml::elementClean( 'title', [], $canonicalTitle ) . "\n";
263 $out .= ' ' . Xml::element( 'ns', [], strval( $row->page_namespace ) ) . "\n";
264 $out .= ' ' . Xml::element( 'id', [], strval( $row->page_id ) ) . "\n";
265 if ( $row->page_is_redirect ) {
266 $services = MediaWikiServices::getInstance();
267 $page = $services->getWikiPageFactory()->newFromTitle( $this->currentTitle );
268 $redirectStore = $services->getRedirectStore();
269 $redirect = $this->invokeLenient(
270 static function () use ( $page, $redirectStore ) {
271 return $redirectStore->getRedirectTarget( $page );
272 },
273 'Failed to get redirect target of page ' . $page->getId()
274 );
275 if ( $redirect instanceof Title && $redirect->isValidRedirectTarget() ) {
276 $out .= ' ';
277 $out .= Xml::element( 'redirect', [ 'title' => self::canonicalTitle( $redirect ) ] );
278 $out .= "\n";
279 }
280 }
281 $this->hookRunner->onXmlDumpWriterOpenPage( $this, $out, $row, $this->currentTitle );
282
283 return $out;
284 }
285
292 public function closePage() {
293 if ( $this->currentTitle !== null ) {
294 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
295 // In rare cases, link cache has the same key for some pages which
296 // might be read as part of the same batch. T220424 and T220316
297 $linkCache->clearLink( $this->currentTitle );
298 }
299 return " </page>\n";
300 }
301
305 private function getRevisionStore() {
306 return MediaWikiServices::getInstance()->getRevisionStore();
307 }
308
312 private function getBlobStore() {
313 // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
314 return MediaWikiServices::getInstance()->getBlobStore();
315 }
316
326 private function invokeLenient( $callback, $warning ) {
327 try {
328 return $callback();
329 } catch ( SuppressedDataException $ex ) {
330 return null;
331 } catch ( MWException | RuntimeException | InvalidArgumentException | ErrorException $ex ) {
332 MWDebug::warning( $warning . ': ' . $ex->getMessage() );
333 return null;
334 }
335 }
336
347 public function writeRevision( $row, $slotRows = null ) {
348 $rev = $this->getRevisionStore()->newRevisionFromRowAndSlots(
349 $row,
350 $slotRows,
351 0,
352 $this->currentTitle
353 );
354
355 $out = " <revision>\n";
356 $out .= " " . Xml::element( 'id', null, strval( $rev->getId() ) ) . "\n";
357
358 if ( $rev->getParentId() ) {
359 $out .= " " . Xml::element( 'parentid', null, strval( $rev->getParentId() ) ) . "\n";
360 }
361
362 $out .= $this->writeTimestamp( $rev->getTimestamp() );
363
364 if ( $rev->isDeleted( RevisionRecord::DELETED_USER ) ) {
365 $out .= " " . Xml::element( 'contributor', [ 'deleted' => 'deleted' ] ) . "\n";
366 } else {
367 // empty values get written out as uid 0, see T224221
368 $user = $rev->getUser();
369 $out .= $this->writeContributor(
370 $user ? $user->getId() : 0,
371 $user ? $user->getName() : ''
372 );
373 }
374
375 if ( $rev->isMinor() ) {
376 $out .= " <minor/>\n";
377 }
378 if ( $rev->isDeleted( RevisionRecord::DELETED_COMMENT ) ) {
379 $out .= " " . Xml::element( 'comment', [ 'deleted' => 'deleted' ] ) . "\n";
380 } else {
381 if ( $rev->getComment()->text != '' ) {
382 $out .= " "
383 . Xml::elementClean( 'comment', [], strval( $rev->getComment()->text ) )
384 . "\n";
385 }
386 }
387
388 $contentMode = $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ? self::WRITE_STUB_DELETED
389 : $this->contentMode;
390
391 $slots = $rev->getSlots()->getSlots();
392
393 // use predictable order, put main slot first
394 ksort( $slots );
395 $out .= $this->writeSlot( $slots[SlotRecord::MAIN], $contentMode );
396
397 foreach ( $slots as $role => $slot ) {
398 if ( $role === SlotRecord::MAIN ) {
399 continue;
400 }
401 $out .= $this->writeSlot( $slot, $contentMode );
402 }
403
404 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
405 $out .= " <sha1/>\n";
406 } else {
407 $sha1 = $this->invokeLenient(
408 static function () use ( $rev ) {
409 return $rev->getSha1();
410 },
411 'failed to determine sha1 for revision ' . $rev->getId()
412 );
413 $out .= " " . Xml::element( 'sha1', null, strval( $sha1 ) ) . "\n";
414 }
415
416 $text = '';
417 if ( $contentMode === self::WRITE_CONTENT ) {
419 $content = $this->invokeLenient(
420 static function () use ( $rev ) {
421 return $rev->getContent( SlotRecord::MAIN, RevisionRecord::RAW );
422 },
423 'Failed to load main slot content of revision ' . $rev->getId()
424 );
425
426 $text = $content ? $content->serialize() : '';
427 }
428 $this->hookRunner->onXmlDumpWriterWriteRevision( $this, $out, $row, $text, $rev );
429
430 $out .= " </revision>\n";
431
432 return $out;
433 }
434
441 private function writeSlot( SlotRecord $slot, $contentMode ) {
442 $isMain = $slot->getRole() === SlotRecord::MAIN;
443 $isV11 = $this->schemaVersion >= XML_DUMP_SCHEMA_VERSION_11;
444
445 if ( !$isV11 && !$isMain ) {
446 // ignore extra slots
447 return '';
448 }
449
450 $out = '';
451 $indent = ' ';
452
453 if ( !$isMain ) {
454 // non-main slots are wrapped into an additional element.
455 $out .= ' ' . Xml::openElement( 'content' ) . "\n";
456 $indent .= ' ';
457 $out .= $indent . Xml::element( 'role', null, strval( $slot->getRole() ) ) . "\n";
458 }
459
460 if ( $isV11 ) {
461 $out .= $indent . Xml::element( 'origin', null, strval( $slot->getOrigin() ) ) . "\n";
462 }
463
464 $contentModel = $slot->getModel();
465 $contentHandler = MediaWikiServices::getInstance()
466 ->getContentHandlerFactory()
467 ->getContentHandler( $contentModel );
468 $contentFormat = $contentHandler->getDefaultFormat();
469
470 // XXX: The content format is only relevant when actually outputting serialized content.
471 // It should probably be an attribute on the text tag.
472 $out .= $indent . Xml::element( 'model', null, strval( $contentModel ) ) . "\n";
473 $out .= $indent . Xml::element( 'format', null, strval( $contentFormat ) ) . "\n";
474
475 $textAttributes = [
476 'bytes' => $this->invokeLenient(
477 static function () use ( $slot ) {
478 return $slot->getSize();
479 },
480 'failed to determine size for slot ' . $slot->getRole() . ' of revision '
481 . $slot->getRevision()
482 ) ?: '0'
483 ];
484
485 if ( $isV11 ) {
486 $textAttributes['sha1'] = $this->invokeLenient(
487 static function () use ( $slot ) {
488 return $slot->getSha1();
489 },
490 'failed to determine sha1 for slot ' . $slot->getRole() . ' of revision '
491 . $slot->getRevision()
492 ) ?: '';
493 }
494
495 if ( $contentMode === self::WRITE_CONTENT ) {
496 $content = $this->invokeLenient(
497 static function () use ( $slot ) {
498 return $slot->getContent();
499 },
500 'failed to load content for slot ' . $slot->getRole() . ' of revision '
501 . $slot->getRevision()
502 );
503
504 if ( $content === null ) {
505 $out .= $indent . Xml::element( 'text', $textAttributes ) . "\n";
506 } else {
507 $out .= $this->writeText( $content, $textAttributes, $indent );
508 }
509 } elseif ( $contentMode === self::WRITE_STUB_DELETED ) {
510 // write <text> placeholder tag
511 $textAttributes['deleted'] = 'deleted';
512 $out .= $indent . Xml::element( 'text', $textAttributes ) . "\n";
513 } else {
514 // write <text> stub tag
515 if ( $isV11 ) {
516 $textAttributes['location'] = $slot->getAddress();
517 }
518
519 if ( $isMain ) {
520 // Output the numerical text ID if possible, for backwards compatibility.
521 // Note that this is currently the ONLY reason we have a BlobStore here at all.
522 // When removing this line, check whether the BlobStore has become unused.
523 try {
524 // NOTE: this will only work for addresses of the form "tt:12345".
525 // If we want to support other kinds of addresses in the future,
526 // we will have to silently ignore failures here.
527 // For now, this fails for "tt:0", which is present in the WMF production
528 // database as of July 2019, due to data corruption.
529 $textId = $this->getBlobStore()->getTextIdFromAddress( $slot->getAddress() );
530 } catch ( InvalidArgumentException $ex ) {
531 MWDebug::warning( 'Bad content address for slot ' . $slot->getRole()
532 . ' of revision ' . $slot->getRevision() . ': ' . $ex->getMessage() );
533 $textId = 0;
534 }
535
536 if ( is_int( $textId ) ) {
537 $textAttributes['id'] = $textId;
538 }
539 }
540
541 $out .= $indent . Xml::element( 'text', $textAttributes ) . "\n";
542 }
543
544 if ( !$isMain ) {
545 $out .= ' ' . Xml::closeElement( 'content' ) . "\n";
546 }
547
548 return $out;
549 }
550
558 private function writeText( Content $content, $textAttributes, $indent ) {
559 $contentHandler = $content->getContentHandler();
560 $contentFormat = $contentHandler->getDefaultFormat();
561
562 if ( $content instanceof TextContent ) {
563 // HACK: For text based models, bypass the serialization step. This allows extensions (like Flow)
564 // that use incompatible combinations of serialization format and content model.
565 $data = $content->getText();
566 } else {
567 $data = $content->serialize( $contentFormat );
568 }
569
570 $data = $contentHandler->exportTransform( $data, $contentFormat );
571 // make sure to use the actual size
572 $textAttributes['bytes'] = strlen( $data );
573 $textAttributes['xml:space'] = 'preserve';
574 return $indent . Xml::elementClean( 'text', $textAttributes, strval( $data ) ) . "\n";
575 }
576
584 public function writeLogItem( $row ) {
585 $out = " <logitem>\n";
586 $out .= " " . Xml::element( 'id', null, strval( $row->log_id ) ) . "\n";
587
588 $out .= $this->writeTimestamp( $row->log_timestamp, " " );
589
590 if ( $row->log_deleted & LogPage::DELETED_USER ) {
591 $out .= " " . Xml::element( 'contributor', [ 'deleted' => 'deleted' ] ) . "\n";
592 } else {
593 $out .= $this->writeContributor( $row->actor_user, $row->actor_name, " " );
594 }
595
596 if ( $row->log_deleted & LogPage::DELETED_COMMENT ) {
597 $out .= " " . Xml::element( 'comment', [ 'deleted' => 'deleted' ] ) . "\n";
598 } else {
599 $comment = $this->commentStore->getComment( 'log_comment', $row )->text;
600 if ( $comment != '' ) {
601 $out .= " " . Xml::elementClean( 'comment', null, strval( $comment ) ) . "\n";
602 }
603 }
604
605 $out .= " " . Xml::element( 'type', null, strval( $row->log_type ) ) . "\n";
606 $out .= " " . Xml::element( 'action', null, strval( $row->log_action ) ) . "\n";
607
608 if ( $row->log_deleted & LogPage::DELETED_ACTION ) {
609 $out .= " " . Xml::element( 'text', [ 'deleted' => 'deleted' ] ) . "\n";
610 } else {
611 $title = Title::makeTitle( $row->log_namespace, $row->log_title );
612 $out .= " " . Xml::elementClean( 'logtitle', null, self::canonicalTitle( $title ) ) . "\n";
613 $out .= " " . Xml::elementClean( 'params',
614 [ 'xml:space' => 'preserve' ],
615 strval( $row->log_params ) ) . "\n";
616 }
617
618 $out .= " </logitem>\n";
619
620 return $out;
621 }
622
628 public function writeTimestamp( $timestamp, $indent = " " ) {
629 $ts = wfTimestamp( TS_ISO_8601, $timestamp );
630 return $indent . Xml::element( 'timestamp', null, $ts ) . "\n";
631 }
632
639 public function writeContributor( $id, $text, $indent = " " ) {
640 $out = $indent . "<contributor>\n";
641 if ( $id || !IPUtils::isValid( $text ) ) {
642 $out .= $indent . " " . Xml::elementClean( 'username', null, strval( $text ) ) . "\n";
643 $out .= $indent . " " . Xml::element( 'id', null, strval( $id ) ) . "\n";
644 } else {
645 $out .= $indent . " " . Xml::elementClean( 'ip', null, strval( $text ) ) . "\n";
646 }
647 $out .= $indent . "</contributor>\n";
648 return $out;
649 }
650
657 public function writeUploads( $row, $dumpContents = false ) {
658 if ( $row->page_namespace == NS_FILE ) {
659 $img = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
660 ->newFile( $row->page_title );
661 if ( $img && $img->exists() ) {
662 $out = '';
663 foreach ( array_reverse( $img->getHistory() ) as $ver ) {
664 $out .= $this->writeUpload( $ver, $dumpContents );
665 }
666 $out .= $this->writeUpload( $img, $dumpContents );
667 return $out;
668 }
669 }
670 return '';
671 }
672
678 private function writeUpload( $file, $dumpContents = false ) {
679 if ( $file->isOld() ) {
681 '@phan-var OldLocalFile $file';
682 $archiveName = " " .
683 Xml::element( 'archivename', null, $file->getArchiveName() ) . "\n";
684 } else {
685 $archiveName = '';
686 }
687 if ( $dumpContents ) {
688 $be = $file->getRepo()->getBackend();
689 # Dump file as base64
690 # Uses only XML-safe characters, so does not need escaping
691 # @todo Too bad this loads the contents into memory (script might swap)
692 $contents = ' <contents encoding="base64">' .
693 chunk_split( base64_encode(
694 $be->getFileContents( [ 'src' => $file->getPath() ] ) ) ) .
695 " </contents>\n";
696 } else {
697 $contents = '';
698 }
699 $uploader = $file->getUploader( File::FOR_PUBLIC );
700 if ( $uploader ) {
701 $uploader = $this->writeContributor( $uploader->getId(), $uploader->getName() );
702 } else {
703 $uploader = Xml::element( 'contributor', [ 'deleted' => 'deleted' ] ) . "\n";
704 }
705 $comment = $file->getDescription( File::FOR_PUBLIC );
706 if ( ( $comment ?? '' ) !== '' ) {
707 $comment = Xml::elementClean( 'comment', null, $comment );
708 } else {
709 $comment = Xml::element( 'comment', [ 'deleted' => 'deleted' ] );
710 }
711 return " <upload>\n" .
712 $this->writeTimestamp( $file->getTimestamp() ) .
713 $uploader .
714 " " . $comment . "\n" .
715 " " . Xml::element( 'filename', null, $file->getName() ) . "\n" .
716 $archiveName .
717 " " . Xml::element( 'src', null, $file->getCanonicalUrl() ) . "\n" .
718 " " . Xml::element( 'size', null, (string)( $file->getSize() ?: 0 ) ) . "\n" .
719 " " . Xml::element( 'sha1base36', null, $file->getSha1() ) . "\n" .
720 " " . Xml::element( 'rel', null, $file->getRel() ) . "\n" .
721 $contents .
722 " </upload>\n";
723 }
724
735 public static function canonicalTitle( Title $title ) {
736 if ( $title->isExternal() ) {
737 return $title->getPrefixedText();
738 }
739
740 $prefix = MediaWikiServices::getInstance()->getContentLanguage()->
741 getFormattedNsText( $title->getNamespace() );
742
743 // @todo Emit some kind of warning to the user if $title->getNamespace() !==
744 // NS_MAIN and $prefix === '' (viz. pages in an unregistered namespace)
745
746 if ( $prefix !== '' ) {
747 $prefix .= ':';
748 }
749
750 return $prefix . $title->getText();
751 }
752}
const NS_FILE
Definition Defines.php:70
const MW_VERSION
The running version of MediaWiki.
Definition Defines.php:36
const XML_DUMP_SCHEMA_VERSION_11
Definition Defines.php:324
const XML_DUMP_SCHEMA_VERSION_10
Definition Defines.php:323
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
static warning( $msg, $callerOffset=1, $level=E_USER_NOTICE, $log='auto')
Adds a warning entry to the log.
Definition MWDebug.php:189
MediaWiki exception.
Handle database storage of comments such as edit summaries and log reasons.
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:78
isValidRedirectTarget()
Check if this Title is a valid redirect target.
Definition Title.php:3506
getNamespace()
Get the namespace index, i.e.
Definition Title.php:1044
getText()
Get the text form (spaces not underscores) of the main part.
Definition Title.php:1017
getPrefixedText()
Get the prefixed title with spaces.
Definition Title.php:1861
Content object implementation for representing flat text.
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=" ")
Module of static functions for generating XML.
Definition Xml.php:33
static closeElement( $element)
Shortcut to close an XML element.
Definition Xml.php:124
static openElement( $element, $attribs=null)
This opens an XML element.
Definition Xml.php:115
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition Xml.php:50
static elementClean( $element, $attribs=[], $contents='')
Format an XML element as with self::element(), but run text through the content language's normalize(...
Definition Xml.php:97
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)