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