1<?php 2 3namespace MediaWiki\Tests\Maintenance; 4 5use PHPUnit\Framework\Assert; 6use XMLReader; 7 8/** 9 * Helper for asserting the structure of an XML dump stream. 10 */ 11class DumpAsserter { 12 13 /** 14 * Holds the XMLReader used for analyzing an XML dump 15 * 16 * @var XMLReader|null 17 */ 18 protected $xml = null; 19 20 /** 21 * XML dump schema version 22 * 23 * @var string 24 */ 25 protected $schemaVersion; 26 27 /** 28 * @var array 29 */ 30 private $varMapping = []; 31 32 /** 33 * @param string $schemaVersion see XML_DUMP_SCHEMA_VERSION_XX 34 */ 35 public function __construct( $schemaVersion ) { 36 $this->schemaVersion = $schemaVersion; 37 } 38 39 /** 40 * Step the current XML reader until node start of given name is found. 41 * 42 * @param string $name Name of the element to look for 43 * (e.g.: "text" when looking for <text>) 44 * 45 * @param bool $allowAscend Whether the search should continue in parent 46 * nodes of the current position. If false (the default), the search will be aborted 47 * on the next closing element. 48 * 49 * @return bool True if the node could be found. false otherwise. 50 */ 51 public function skipToNode( $name, $allowAscend = false ) { 52 $depth = 0; 53 while ( true ) { 54 $current = $this->xml->name; 55 if ( $this->xml->nodeType == XMLReader::ELEMENT ) { 56 if ( $current == $name ) { 57 return true; 58 } 59 60 if ( !$this->xml->isEmptyElement ) { 61 $depth++; 62 } 63 } 64 65 if ( $this->xml->nodeType == XMLReader::END_ELEMENT ) { 66 $depth--; 67 if ( $depth < 0 && !$allowAscend ) 68 return false; 69 } 70 71 if ( !$this->xml->read() ) { 72 break; 73 } 74 } 75 76 return false; 77 } 78 79 /** 80 * Step the current XML reader until node start of given name is found, 81 * and advance to the first child node. 82 * 83 * @param string $name Name of the element to look for 84 * (e.g.: "text" when looking for <text>) 85 * 86 * @param bool $allowAscend Whether the search should continue in parent 87 * nodes of the current position. If false (the default), the search will be aborted 88 * on the next closing element. 89 */ 90 public function skipIntoNode( $name, $allowAscend = false ) { 91 Assert::assertTrue( $this->skipToNode( $name, $allowAscend ), 92 "Skipping to $name" ); 93 94 Assert::assertTrue( !$this->xml->isEmptyElement, 95 "Skipping into $name" ); 96 97 $this->xml->read(); 98 } 99 100 /** 101 * Step the current XML reader until node end of given name is found. 102 * 103 * @param string $name Name of the closing element to look for 104 * (e.g.: "mediawiki" when looking for </mediawiki>) 105 * 106 * @return bool True if the end node could be found. false otherwise. 107 */ 108 public function skipToNodeEnd( $name ) { 109 while ( $this->xml->read() ) { 110 if ( $this->xml->nodeType == XMLReader::END_ELEMENT && 111 $this->xml->name == $name 112 ) { 113 return true; 114 } 115 } 116 117 return false; 118 } 119 120 /** 121 * Step the current XML reader to the first element start after the node 122 * end of a given name. 123 * 124 * @param string $name Name of the closing element to look for 125 * (e.g.: "mediawiki" when looking for </mediawiki>) 126 * 127 * @return bool True if new element after the closing of $name could be 128 * found. false otherwise. 129 */ 130 public function skipPastNodeEnd( $name ) { 131 Assert::assertTrue( $this->skipToNodeEnd( $name ), 132 "Skipping to end of $name" ); 133 while ( $this->xml->read() ) { 134 if ( $this->xml->nodeType == XMLReader::ELEMENT ) { 135 return true; 136 } 137 } 138 139 return false; 140 } 141 142 /** 143 * Opens an XML file to analyze. 144 * 145 * @param string $fname Name of file to analyze 146 */ 147 public function open( $fname ) { 148 $this->xml = new XMLReader(); 149 150 Assert::assertTrue( $this->xml->open( $fname ), 151 "Opening temporary file $fname via XMLReader failed" ); 152 } 153 154 /** 155 * Opens an XML file to analyze, verifies the top level tags, 156 * and skips past <siteinfo>. 157 * 158 * The contents of the <siteinfo> tag can be checked if $siteInfoTemplate 159 * is given. See assertDumpHead(). 160 * 161 * @param string $fname Name of file to analyze 162 * 163 * @param string|null $siteInfoTemplate 164 * @param string $language 165 */ 166 public function assertDumpStart( $fname, $siteInfoTemplate = null, $language = 'en' ) { 167 $this->open( $fname ); 168 $this->assertDumpHead( $siteInfoTemplate, $language ); 169 } 170 171 /** 172 * Asserts that the head of a dump is valid. 173 * This checks the attributes of the top level <mediawiki> tag. 174 * 175 * If $siteInfoTemplate is given, it is interpreted as the file name 176 * of an XML template that will be used with assertDOM() to check the contents 177 * of the <siteinfo> tag, which is expected to be the first child of 178 * the top level <mediawiki>. Variable substitution applies as defined by 179 * calling setVarMapping(). 180 * 181 * After this method returns, the XML reader's position will be after 182 * the closing </siteinfo> tag, before the next tag. 183 * 184 * @param string|null $siteInfoTemplate 185 * @param string $language 186 */ 187 public function assertDumpHead( $siteInfoTemplate = null, $language = 'en' ) { 188 $this->assertNodeStart( 'mediawiki', false ); 189 $this->assertAttributes( [ 190 "xmlns" => "http://www.mediawiki.org/xml/export-{$this->schemaVersion}/", 191 "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance", 192 "xsi:schemaLocation" => "http://www.mediawiki.org/xml/export-{$this->schemaVersion}/ " 193 . "http://www.mediawiki.org/xml/export-{$this->schemaVersion}.xsd", 194 "version" => "{$this->schemaVersion}", 195 "xml:lang" => "{$language}" 196 ] ); 197 198 $this->assertNodeStart( 'siteinfo', false ); 199 200 if ( $siteInfoTemplate ) { 201 // Checking site info 202 $this->assertDOM( $siteInfoTemplate ); 203 } 204 205 // skip past extra namespaces 206 $this->skipPastNodeEnd( 'siteinfo' ); 207 } 208 209 /** 210 * Asserts that the xml reader is at the final closing tag of an xml file and 211 * closes the reader. 212 * 213 * @param string $name (optional) the name of the final tag 214 * (e.g.: "mediawiki" for </mediawiki>) 215 */ 216 public function assertDumpEnd( $name = "mediawiki" ) { 217 $this->assertNodeEnd( $name, false ); 218 if ( $this->xml->read() ) { 219 $this->skipWhitespace(); 220 } 221 Assert::assertEquals( $this->xml->nodeType, XMLReader::NONE, 222 "No proper entity left to parse" ); 223 $this->close(); 224 } 225 226 public function close() { 227 $this->xml->close(); 228 } 229 230 /** 231 * Steps the xml reader over white space 232 */ 233 public function skipWhitespace() { 234 $cont = true; 235 while ( $cont && ( ( $this->xml->nodeType == XMLReader::NONE ) 236 || ( $this->xml->nodeType == XMLReader::WHITESPACE ) 237 || ( $this->xml->nodeType == XMLReader::SIGNIFICANT_WHITESPACE ) ) ) { 238 $cont = $this->xml->read(); 239 } 240 } 241 242 /** 243 * Asserts that the xml reader is at an element of given name, and optionally 244 * skips past it. If the reader is at a whitespace element, the whitespace is 245 * skipped first. 246 * 247 * @param string $name The name of the element to check for 248 * (e.g.: "mediawiki" for <mediawiki>) 249 * @param bool $skip (optional) if true, skip past the found element 250 */ 251 public function assertNodeStart( $name, $skip = true ) { 252 $this->skipWhitespace(); 253 Assert::assertEquals( $name, $this->xml->name, "Node name" ); 254 Assert::assertEquals( XMLReader::ELEMENT, $this->xml->nodeType, "Node type" ); 255 if ( $skip ) { 256 Assert::assertTrue( $this->xml->read(), "Skipping past start tag" ); 257 } 258 } 259 260 /** 261 * Asserts that the XML reader is at an element start, and that the element 262 * has the given attributes with the given values. 263 * Variable substitution applies for variables set via setVarMapping(). 264 * 265 * @param array $attributes 266 * @param bool $skip (optional) if true, skip past the found element 267 */ 268 public function assertAttributes( $attributes, $skip = true ) { 269 Assert::assertEquals( XMLReader::ELEMENT, $this->xml->nodeType, "Node type" ); 270 $actualAttributes = $this->getAttributeArray( $this->xml ); 271 272 $attributes = array_map( 273 function ( $v ) { 274 return $this->resolveVars( $v ); 275 }, 276 $attributes 277 ); 278 $actualAttributes = array_intersect_key( $actualAttributes, $attributes ); 279 280 Assert::assertEquals( $attributes, $actualAttributes, "Attributes" ); 281 282 if ( $skip ) { 283 Assert::assertTrue( $this->xml->read(), "Skipping past start tag" ); 284 } 285 } 286 287 /** 288 * Asserts that the xml reader is at an element of given name, and that element 289 * is an empty tag. 290 * 291 * @param string $name The name of the element to check for 292 * (e.g.: "text" for <text/>) 293 * @param bool $skip (optional) if true, skip past the found element 294 * @param bool $skip_ws (optional) if true, also skip past white spaces that trail the 295 * closing element. 296 */ 297 public function assertEmptyNode( $name, $skip = true, $skip_ws = true ) { 298 $this->assertNodeStart( $name, false ); 299 Assert::assertFalse( !$this->xml->isEmptyElement, "$name tag has content" ); 300 301 if ( $skip ) { 302 Assert::assertTrue( $this->xml->read(), "Skipping $name tag" ); 303 if ( ( $this->xml->nodeType == XMLReader::END_ELEMENT ) 304 && ( $this->xml->name == $name ) 305 ) { 306 $this->xml->read(); 307 } 308 309 if ( $skip_ws ) { 310 $this->skipWhitespace(); 311 } 312 } 313 } 314 315 /** 316 * Asserts that the xml reader is at an closing element of given name, and optionally 317 * skips past it. If the reader is at a whitespace element, the whitespace is 318 * skipped first. 319 * 320 * @param string $name The name of the closing element to check for 321 * (e.g.: "mediawiki" for </mediawiki>) 322 * @param bool $skip (optional) if true, skip past the found element 323 */ 324 public function assertNodeEnd( $name, $skip = true ) { 325 $this->skipWhitespace(); 326 Assert::assertEquals( $name, $this->xml->name, "Node name" ); 327 Assert::assertEquals( XMLReader::END_ELEMENT, $this->xml->nodeType, "Node type" ); 328 if ( $skip ) { 329 // note: if there is no more content after the tag and read() returns false, 330 // that's fine. 331 $this->xml->read(); 332 } 333 } 334 335 /** 336 * Asserts that the xml reader is at an element of given tag that contains a given text, 337 * and skips over the element. 338 * 339 * @param string $name The name of the element to check for 340 * (e.g.: "mediawiki" for <mediawiki>...</mediawiki>) 341 * @param string|bool $text If string, check if it equals the elements text. 342 * Variable substitution applies. If false, ignore the element's text. 343 * @param bool $skip_ws (optional) if true, skip past white spaces that trail the 344 * closing element. 345 */ 346 public function assertTextNode( $name, $text, $skip_ws = true ) { 347 $this->assertNodeStart( $name ); 348 349 if ( $text !== false ) { 350 $text = $this->resolveVars( $text ); 351 $actual = $this->resolveVars( $this->xml->value ); 352 Assert::assertEquals( $text, $actual, "Text of node " . $name ); 353 } 354 Assert::assertTrue( $this->xml->read(), "Skipping past processed text of " . $name ); 355 $this->assertNodeEnd( $name ); 356 357 if ( $skip_ws ) { 358 $this->skipWhitespace(); 359 } 360 } 361 362 /** 363 * Asserts that the xml reader is at the start of a page element and skips over the first 364 * tags, after checking them. 365 * 366 * Besides the opening page element, this function also checks for and skips over the 367 * title, ns, and id tags. Hence after this function, the xml reader is at the first 368 * revision of the current page. 369 * 370 * @param int $id Id of the page to assert 371 * @param int $ns Number of namespage to assert 372 * @param string $name Title of the current page 373 */ 374 public function assertPageStart( $id, $ns, $name ) { 375 $this->assertNodeStart( "page" ); 376 $this->assertTextNode( "title", $name ); 377 $this->assertTextNode( "ns", $ns ); 378 $this->assertTextNode( "id", $id ); 379 } 380 381 /** 382 * Asserts that the xml reader is at the page's closing element and skips to the next 383 * element. 384 */ 385 public function assertPageEnd() { 386 $this->assertNodeEnd( "page" ); 387 } 388 389 /** 390 * Checks and skips tags that represent the properties of a revision. 391 * 392 * @param int $id Id of the revision 393 * @param string $summary Summary of the revision 394 * @param string $text_sha1 The base36 SHA-1 of the revision's text 395 * @param string $hasEarlyText Whether a text tag is expected before the <sha1> tag. 396 * Must be one of 'yes', 'no', or maybe. 397 * @param int|bool $parentid (optional) id of the parent revision 398 * @param string $model The expected content model id (default: CONTENT_MODEL_WIKITEXT) 399 * @param string $format The expected format model id (default: CONTENT_FORMAT_WIKITEXT) 400 * @param bool &$foundText Output, whether a text tag was found before the SHA1 tag. 401 * If this returns false, the text tag should be the next tag after the method returns. 402 */ 403 public function assertRevisionProperties( $id, $summary, 404 $text_sha1, $hasEarlyText = 'maybe', $parentid = false, 405 $model = CONTENT_MODEL_WIKITEXT, $format = CONTENT_FORMAT_WIKITEXT, 406 &$foundText = '' 407 ) { 408 $this->assertTextNode( "id", $id ); 409 if ( $parentid !== false ) { 410 $this->assertTextNode( "parentid", $parentid ); 411 } 412 $this->assertTextNode( "timestamp", false ); 413 414 $this->assertNodeStart( "contributor" ); 415 $this->assertTextNode( "username", false ); 416 $this->assertTextNode( "id", false ); 417 $this->assertNodeEnd( "contributor" ); 418 419 $this->assertTextNode( "comment", $summary ); 420 421 if ( $this->schemaVersion >= XML_DUMP_SCHEMA_VERSION_11 ) { 422 $this->assertTextNode( "origin", false ); 423 } 424 425 $this->assertTextNode( "model", $model ); 426 427 $this->assertTextNode( "format", $format ); 428 429 if ( $hasEarlyText === 'yes' || ( $this->xml->name == "text" && $hasEarlyText === 'maybe' ) ) { 430 $foundText = true; 431 $this->assertNodeStart( "text", false ); 432 $this->xml->next(); 433 $this->skipWhitespace(); 434 } else { 435 $foundText = false; 436 } 437 438 if ( $text_sha1 ) { 439 $this->assertTextNode( "sha1", $text_sha1 ); 440 } else { 441 $this->assertEmptyNode( "sha1" ); 442 } 443 } 444 445 /** 446 * Asserts that the xml reader is at a revision and checks its representation before 447 * skipping over it. 448 * 449 * @param int $id Id of the revision 450 * @param string $summary Summary of the revision 451 * @param int $text_id Id of the revision's text 452 * @param int $text_bytes Number of bytes in the revision's text 453 * @param string $text_sha1 The base36 SHA-1 of the revision's text 454 * @param string|bool $text (optional) The revision's string, or false to check for a 455 * revision stub 456 * @param int|bool $parentid (optional) id of the parent revision 457 * @param string $model The expected content model id (default: CONTENT_MODEL_WIKITEXT) 458 * @param string $format The expected format model id (default: CONTENT_FORMAT_WIKITEXT) 459 */ 460 public function assertRevision( $id, $summary, $text_id, $text_bytes, 461 $text_sha1, $text = false, $parentid = false, 462 $model = CONTENT_MODEL_WIKITEXT, $format = CONTENT_FORMAT_WIKITEXT 463 ) { 464 $this->assertNodeStart( "revision" ); 465 466 $this->assertRevisionProperties( 467 $id, 468 $summary, 469 $text_sha1, 470 'maybe', 471 $parentid, 472 $model, 473 $format, 474 $text_found 475 ); 476 477 if ( !$text_found ) { 478 $this->assertText( $id, $text_id, $text_bytes, $text ); 479 } 480 481 $this->assertNodeEnd( "revision" ); 482 $this->skipWhitespace(); 483 } 484 485 public function assertText( $id, $text_id, $text_bytes, $text ) { 486 $this->assertNodeStart( "text", false ); 487 if ( $text_bytes !== false ) { 488 Assert::assertEquals( $this->xml->getAttribute( "bytes" ), $text_bytes, 489 "Attribute 'bytes' of revision " . $id ); 490 } 491 492 if ( $text === false ) { 493 Assert::assertEquals( $this->xml->getAttribute( "id" ), $text_id, 494 "Text id of revision " . $id ); 495 Assert::assertNull( $this->xml->getAttribute( "xml:space" ), 496 "xml:space attribute shout not be present" ); 497 $this->assertEmptyNode( "text" ); 498 } else { 499 // Testing for a real dump 500 Assert::assertEquals( $this->xml->getAttribute( "xml:space" ), "preserve", 501 "xml:space=preserve should be present" ); 502 Assert::assertTrue( $this->xml->read(), "Skipping text start tag" ); 503 Assert::assertEquals( $text, $this->xml->value, "Text of revision " . $id ); 504 Assert::assertTrue( $this->xml->read(), "Skipping past text" ); 505 $this->assertNodeEnd( "text" ); 506 $this->skipWhitespace(); 507 } 508 } 509 510 /** 511 * asserts that the xml reader is at the beginning of a log entry and skips over 512 * it while analyzing it. 513 * 514 * @param int $id Id of the log entry 515 * @param string $user_name User name of the log entry's performer 516 * @param int $user_id User id of the log entry 's performer 517 * @param string|null $comment Comment of the log entry. If null, the comment text is ignored. 518 * @param string $type Type of the log entry 519 * @param string $subtype Subtype of the log entry 520 * @param string $title Title of the log entry's target 521 * @param array $parameters (optional) unserialized data accompanying the log entry 522 */ 523 public function assertLogItem( $id, $user_name, $user_id, $comment, $type, 524 $subtype, $title, $parameters = [] 525 ) { 526 $this->assertNodeStart( "logitem" ); 527 528 $this->assertTextNode( "id", $id ); 529 $this->assertTextNode( "timestamp", false ); 530 531 $this->assertNodeStart( "contributor" ); 532 $this->assertTextNode( "username", $user_name ); 533 $this->assertTextNode( "id", $user_id ); 534 $this->assertNodeEnd( "contributor" ); 535 536 if ( $comment !== null ) { 537 $this->assertTextNode( "comment", $comment ); 538 } 539 $this->assertTextNode( "type", $type ); 540 $this->assertTextNode( "action", $subtype ); 541 $this->assertTextNode( "logtitle", $title ); 542 543 $this->assertNodeStart( "params" ); 544 $parameters_xml = unserialize( $this->xml->value ); 545 Assert::assertEquals( $parameters, $parameters_xml ); 546 Assert::assertTrue( $this->xml->read(), "Skipping past processed text of params" ); 547 $this->assertNodeEnd( "params" ); 548 549 $this->assertNodeEnd( "logitem" ); 550 } 551 552 /** 553 * Returns the XMLReader's current line number for reporting. 554 * 555 * @param XMLReader|null $xml 556 * 557 * @return int 558 */ 559 public function getLineNumber( XMLReader $xml = null ) { 560 if ( !$xml ) { 561 $xml = $this->xml; 562 } 563 564 if ( $xml->nodeType == XMLReader::NONE ) { 565 return 0; 566 } 567 568 return $xml->expand()->getLineNo(); 569 } 570 571 /** 572 * Opens an XML template file and compares it to the XML structure at the current position of 573 * this asserter. 574 * 575 * If the outer-most tag of the template file is <test:data>, that tag is 576 * ignored during comparison. This allows template files to contain arbitrary snippets of XML. 577 * When the tag <test:end/> is encountered in the template, the comparison is ended. 578 * This allows template files to be written to match the beginning of a structure, 579 * without the need for subsequent contents to match. 580 * 581 * The contents of $file are subject to variable substitution based on 582 * the values provided via setVarMapping(). 583 * 584 * @param string $file Name of file to analyze 585 */ 586 public function assertDOM( $file ) { 587 $exXml = new XMLReader(); 588 589 Assert::assertTrue( $exXml->open( $file ), 590 "Opening fixture file $file via XMLReader failed" ); 591 592 $line = 0; 593 while ( true ) { 594 $line = max( $line, $this->getLineNumber( $exXml ) ); 595 $location = "[$file line $line] "; 596 597 while ( $exXml->nodeType == XMLReader::NONE 598 || $exXml->nodeType == XMLReader::WHITESPACE 599 || $exXml->nodeType == XMLReader::SIGNIFICANT_WHITESPACE 600 || $exXml->nodeType == XMLReader::COMMENT 601 || ( $exXml->nodeType == XMLReader::ELEMENT && $exXml->name === 'test:data' ) ) { 602 603 // Reached the end of the template file, so we are done here. 604 if ( !$exXml->read() ) { 605 break 2; 606 } 607 608 // Reached the end of the test data, so we are done here. 609 if ( $exXml->nodeType == XMLReader::END_ELEMENT && $exXml->name === 'test:data' ) { 610 break 2; 611 } 612 } 613 614 while ( $this->xml->nodeType == XMLReader::NONE 615 || $this->xml->nodeType == XMLReader::WHITESPACE 616 || $this->xml->nodeType == XMLReader::SIGNIFICANT_WHITESPACE 617 || $this->xml->nodeType == XMLReader::COMMENT ) { 618 Assert::assertTrue( $this->xml->read(), $location . 'Document ended unexpectedly' ); 619 } 620 621 // End comparison early, ignore the rest of the contents of the template file. 622 if ( $exXml->nodeType == XMLReader::ELEMENT && $exXml->name === 'test:end' ) { 623 break; 624 } 625 626 $line = max( $line, $this->getLineNumber( $exXml ) ); 627 $location = "[$file line $line] "; 628 629 Assert::assertSame( $exXml->nodeType, $this->xml->nodeType, $location . 'Node type' ); 630 Assert::assertSame( $exXml->name, $this->xml->name, $location . 'Node type' ); 631 Assert::assertSame( 632 $exXml->hasValue, 633 $this->xml->hasValue, 634 $location . 'Node has value?' 635 ); 636 Assert::assertSame( 637 $exXml->hasAttributes, 638 $this->xml->hasAttributes, 639 $location . 'Node has attributes?' 640 ); 641 642 if ( $exXml->hasValue ) { 643 $expValue = $this->resolveVars( $exXml->value ); 644 $actValue = $this->resolveVars( $this->xml->value ); 645 Assert::assertSame( $expValue, $actValue, $location . 'Node value' ); 646 } 647 648 if ( $exXml->hasAttributes ) { 649 $expectedAttributes = $this->getAttributeArray( $exXml ); 650 $actualAttributes = $this->getAttributeArray( $this->xml ); 651 652 Assert::assertEquals( $expectedAttributes, $actualAttributes, $location . 'Attributes' ); 653 } 654 655 // Reached the end of the template file, so we are done here. 656 if ( !$exXml->read() ) { 657 break; 658 } 659 660 // Reached the end of the test data, so we are done here. 661 if ( $exXml->nodeType == XMLReader::END_ELEMENT && $exXml->name === 'test:data' ) { 662 break; 663 } 664 665 Assert::assertTrue( $this->xml->read(), $location . 'Document ended unexpectedly' ); 666 } 667 668 $exXml->close(); 669 } 670 671 /** 672 * Strip any <test:...> tags from a string. 673 * 674 * @param string $text 675 * 676 * @return string 677 */ 678 public function stripTestTags( $text ) { 679 $text = preg_replace( '@<!--.*?-->@s', '', $text ); 680 $text = preg_replace( '@</?test:[^>]+>@s', '', $text ); 681 return $text; 682 } 683 684 private function getAttributeArray( XMLReader $xml = null ) { 685 if ( !$xml ) { 686 $xml = $this->xml; 687 } 688 689 if ( $xml->nodeType !== XMLReader::ELEMENT ) { 690 return null; 691 } 692 693 if ( !$xml->hasAttributes ) { 694 return []; 695 } 696 697 $attr = []; 698 while ( $xml->moveToNextAttribute() ) { 699 $attr[$xml->name] = $this->resolveVars( $xml->value ); 700 } 701 702 return $attr; 703 } 704 705 /** 706 * @param string $text 707 * 708 * @return string 709 */ 710 public function resolveVars( $text ) { 711 return str_replace( 712 array_keys( $this->varMapping ), 713 array_values( $this->varMapping ), 714 $text 715 ); 716 } 717 718 /** 719 * Define a variable mapping to be applied by assertDOM 720 * 721 * @param string $name 722 * @param string $value 723 */ 724 public function setVarMapping( $name, $value ) { 725 $key = '{{' . $name . '}}'; 726 $this->varMapping[$key] = $value; 727 } 728 729 /** 730 * @return string 731 */ 732 public function getSchemaVersion() { 733 return $this->schemaVersion; 734 } 735 736} 737