Index: trunk/phase3/includes/Xml.php |
— | — | @@ -594,6 +594,8 @@ |
595 | 595 | $s .= self::encodeJsVar( $elt ); |
596 | 596 | } |
597 | 597 | $s .= ']'; |
| 598 | + } elseif ( $value instanceof XmlJsCode ) { |
| 599 | + $s = $value->value; |
598 | 600 | } elseif ( is_object( $value ) || is_array( $value ) ) { |
599 | 601 | // Objects and associative arrays |
600 | 602 | $s = '{'; |
— | — | @@ -611,7 +613,30 @@ |
612 | 614 | return $s; |
613 | 615 | } |
614 | 616 | |
| 617 | + /** |
| 618 | + * Create a call to a JavaScript function. The supplied arguments will be |
| 619 | + * encoded using Xml::encodeJsVar(). |
| 620 | + * |
| 621 | + * @param $name The name of the function to call, or a JavaScript expression |
| 622 | + * which evaluates to a function object which is called. |
| 623 | + * @param $args Array of arguments to pass to the function. |
| 624 | + */ |
| 625 | + public static function encodeJsCall( $name, $args ) { |
| 626 | + $s = "$name("; |
| 627 | + $first = true; |
| 628 | + foreach ( $args as $arg ) { |
| 629 | + if ( $first ) { |
| 630 | + $first = false; |
| 631 | + } else { |
| 632 | + $s .= ', '; |
| 633 | + } |
| 634 | + $s .= Xml::encodeJsVar( $arg ); |
| 635 | + } |
| 636 | + $s .= ");\n"; |
| 637 | + return $s; |
| 638 | + } |
615 | 639 | |
| 640 | + |
616 | 641 | /** |
617 | 642 | * Check if a string is well-formed XML. |
618 | 643 | * Must include the surrounding tag. |
— | — | @@ -813,3 +838,22 @@ |
814 | 839 | } |
815 | 840 | |
816 | 841 | } |
| 842 | + |
| 843 | +/** |
| 844 | + * A wrapper class which causes Xml::encodeJsVar() and Xml::encodeJsCall() to |
| 845 | + * interpret a given string as being a JavaScript expression, instead of string |
| 846 | + * data. |
| 847 | + * |
| 848 | + * Example: |
| 849 | + * |
| 850 | + * Xml::encodeJsVar( new XmlJsCode( 'a + b' ) ); |
| 851 | + * |
| 852 | + * Returns "a + b". |
| 853 | + */ |
| 854 | +class XmlJsCode { |
| 855 | + public $value; |
| 856 | + |
| 857 | + function __construct( $value ) { |
| 858 | + $this->value = $value; |
| 859 | + } |
| 860 | +} |
Index: trunk/phase3/includes/resourceloader/ResourceLoader.php |
— | — | @@ -53,7 +53,7 @@ |
54 | 54 | if ( !count( $modules ) ) { |
55 | 55 | return; // or else Database*::select() will explode, plus it's cheaper! |
56 | 56 | } |
57 | | - $dbr = wfGetDb( DB_SLAVE ); |
| 57 | + $dbr = wfGetDB( DB_SLAVE ); |
58 | 58 | $skin = $context->getSkin(); |
59 | 59 | $lang = $context->getLanguage(); |
60 | 60 | |
— | — | @@ -385,7 +385,7 @@ |
386 | 386 | } |
387 | 387 | |
388 | 388 | // Messages |
389 | | - $messages = isset( $blobs[$name] ) ? $blobs[$name] : '{}'; |
| 389 | + $messagesBlob = isset( $blobs[$name] ) ? $blobs[$name] : array(); |
390 | 390 | |
391 | 391 | // Append output |
392 | 392 | switch ( $context->getOnly() ) { |
— | — | @@ -396,7 +396,7 @@ |
397 | 397 | $out .= self::makeCombinedStyles( $styles ); |
398 | 398 | break; |
399 | 399 | case 'messages': |
400 | | - $out .= self::makeMessageSetScript( $messages ); |
| 400 | + $out .= self::makeMessageSetScript( new XmlJsCode( $messagesBlob ) ); |
401 | 401 | break; |
402 | 402 | default: |
403 | 403 | // Minify CSS before embedding in mediaWiki.loader.implement call |
— | — | @@ -406,7 +406,8 @@ |
407 | 407 | $styles[$media] = $this->filter( 'minify-css', $style ); |
408 | 408 | } |
409 | 409 | } |
410 | | - $out .= self::makeLoaderImplementScript( $name, $scripts, $styles, $messages ); |
| 410 | + $out .= self::makeLoaderImplementScript( $name, $scripts, $styles, |
| 411 | + new XmlJsCode( $messagesBlob ) ); |
411 | 412 | break; |
412 | 413 | } |
413 | 414 | |
— | — | @@ -441,26 +442,49 @@ |
442 | 443 | |
443 | 444 | /* Static Methods */ |
444 | 445 | |
| 446 | + /** |
| 447 | + * Returns JS code to call to mediaWiki.loader.implement for a module with |
| 448 | + * given properties. |
| 449 | + * |
| 450 | + * @param $name Module name |
| 451 | + * @param $scripts Array of JavaScript code snippets to be executed after the |
| 452 | + * module is loaded |
| 453 | + * @param $styles Associative array mapping media type to associated CSS string |
| 454 | + * @param $messages Messages associated with this module. May either be an |
| 455 | + * associative array mapping message key to value, or a JSON-encoded message blob |
| 456 | + * containing the same data, wrapped in an XmlJsCode object. |
| 457 | + */ |
445 | 458 | public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) { |
446 | 459 | if ( is_array( $scripts ) ) { |
447 | 460 | $scripts = implode( $scripts, "\n" ); |
448 | 461 | } |
449 | | - if ( is_array( $styles ) ) { |
450 | | - $styles = count( $styles ) ? FormatJson::encode( $styles ) : 'null'; |
451 | | - } |
452 | | - if ( is_array( $messages ) ) { |
453 | | - $messages = count( $messages ) ? FormatJson::encode( $messages ) : 'null'; |
454 | | - } |
455 | | - return "mediaWiki.loader.implement( '$name', function() {{$scripts}},\n$styles,\n$messages );\n"; |
| 462 | + return Xml::encodeJsCall( |
| 463 | + 'mediaWiki.loader.implement', |
| 464 | + array( |
| 465 | + $name, |
| 466 | + new XmlJsCode( "function() {{$scripts}}" ), |
| 467 | + (object)$styles, |
| 468 | + (object)$messages |
| 469 | + ) ); |
456 | 470 | } |
457 | 471 | |
| 472 | + /** |
| 473 | + * Returns JS code which, when called, will register a given list of messages. |
| 474 | + * |
| 475 | + * @param $messages May either be an associative array mapping message key |
| 476 | + * to value, or a JSON-encoded message blob containing the same data, |
| 477 | + * wrapped in an XmlJsCode object. |
| 478 | + */ |
458 | 479 | public static function makeMessageSetScript( $messages ) { |
459 | | - if ( is_array( $messages ) ) { |
460 | | - $messages = count( $messages ) ? FormatJson::encode( $messages ) : 'null'; |
461 | | - } |
462 | | - return "mediaWiki.msg.set( $messages );\n"; |
| 480 | + return Xml::encodeJsCall( 'mediaWiki.messages.set', array( (object)$messages ) ); |
463 | 481 | } |
464 | 482 | |
| 483 | + /** |
| 484 | + * Combines an associative array mapping media type to CSS into a |
| 485 | + * single stylesheet with @media blocks. |
| 486 | + * |
| 487 | + * @param $styles Array of CSS strings |
| 488 | + */ |
465 | 489 | public static function makeCombinedStyles( array $styles ) { |
466 | 490 | $out = ''; |
467 | 491 | foreach ( $styles as $media => $style ) { |
— | — | @@ -469,49 +493,95 @@ |
470 | 494 | return $out; |
471 | 495 | } |
472 | 496 | |
| 497 | + /** |
| 498 | + * Returns a JS call to mediaWiki.loader.state, which sets the state of a |
| 499 | + * module or modules to a given value. Has two calling conventions: |
| 500 | + * |
| 501 | + * - ResourceLoader::makeLoaderStateScript( $name, $state ): |
| 502 | + * Set the state of a single module called $name to $state |
| 503 | + * |
| 504 | + * - ResourceLoader::makeLoaderStateScript( array( $name => $state, ... ) ): |
| 505 | + * Set the state of modules with the given names to the given states |
| 506 | + */ |
473 | 507 | public static function makeLoaderStateScript( $name, $state = null ) { |
474 | 508 | if ( is_array( $name ) ) { |
475 | | - $statuses = FormatJson::encode( $name ); |
476 | | - return "mediaWiki.loader.state( $statuses );\n"; |
| 509 | + return Xml::encodeJsCall( 'mediaWiki.loader.state', array( $name ) ); |
477 | 510 | } else { |
478 | | - $name = Xml::escapeJsString( $name ); |
479 | | - $state = Xml::escapeJsString( $state ); |
480 | | - return "mediaWiki.loader.state( '$name', '$state' );\n"; |
| 511 | + return Xml::encodeJsCall( 'mediaWiki.loader.state', array( $name, $state ) ); |
481 | 512 | } |
482 | 513 | } |
483 | 514 | |
| 515 | + /** |
| 516 | + * Returns JS code which calls the script given by $script. The script will |
| 517 | + * be called with local variables name, version, dependencies and group, |
| 518 | + * which will have values corresponding to $name, $version, $dependencies |
| 519 | + * and $group as supplied. |
| 520 | + * |
| 521 | + * @param $name The module name |
| 522 | + * @param $version The module version string |
| 523 | + * @param $dependencies Array of module names on which this module depends |
| 524 | + * @param $group The group which the module is in. |
| 525 | + * @param $script The JS loader script |
| 526 | + */ |
484 | 527 | public static function makeCustomLoaderScript( $name, $version, $dependencies, $group, $script ) { |
485 | | - $name = Xml::escapeJsString( $name ); |
486 | | - $version = (int) $version > 1 ? (int) $version : 1; |
487 | | - $dependencies = FormatJson::encode( $dependencies ); |
488 | | - $group = FormatJson::encode( $group ); |
489 | 528 | $script = str_replace( "\n", "\n\t", trim( $script ) ); |
490 | | - return "( function( name, version, dependencies, group ) {\n\t$script\n} )" . |
491 | | - "( '$name', $version, $dependencies, $group );\n"; |
| 529 | + return Xml::encodeJsCall( |
| 530 | + "( function( name, version, dependencies, group ) {\n\t$script\n} )", |
| 531 | + array( $name, $version, $dependencies, $group ) ); |
492 | 532 | } |
493 | 533 | |
| 534 | + /** |
| 535 | + * Returns JS code which calls mediaWiki.loader.register with the given |
| 536 | + * parameters. Has three calling conventions: |
| 537 | + * |
| 538 | + * - ResourceLoader::makeLoaderRegisterScript( $name, $version, $dependencies, $group ): |
| 539 | + * Register a single module. |
| 540 | + * |
| 541 | + * - ResourceLoader::makeLoaderRegisterScript( array( $name1, $name2 ) ): |
| 542 | + * Register modules with the given names. |
| 543 | + * |
| 544 | + * - ResourceLoader::makeLoaderRegisterScript( array( |
| 545 | + * array( $name1, $version1, $dependencies1, $group1 ), |
| 546 | + * array( $name2, $version2, $dependencies1, $group2 ), |
| 547 | + * ... |
| 548 | + * ) ): |
| 549 | + * Registers modules with the given names and parameters. |
| 550 | + * |
| 551 | + * @param $name The module name |
| 552 | + * @param $version The module version string |
| 553 | + * @param $dependencies Array of module names on which this module depends |
| 554 | + * @param $group The group which the module is in. |
| 555 | + */ |
494 | 556 | public static function makeLoaderRegisterScript( $name, $version = null, |
495 | 557 | $dependencies = null, $group = null ) |
496 | 558 | { |
497 | 559 | if ( is_array( $name ) ) { |
498 | | - $registrations = FormatJson::encode( $name ); |
499 | | - return "mediaWiki.loader.register( $registrations );\n"; |
| 560 | + return Xml::encodeJsCall( 'mediaWiki.loader.register', array( $name ) ); |
500 | 561 | } else { |
501 | | - $name = Xml::escapeJsString( $name ); |
502 | 562 | $version = (int) $version > 1 ? (int) $version : 1; |
503 | | - $dependencies = FormatJson::encode( $dependencies ); |
504 | | - $group = FormatJson::encode( $group ); |
505 | | - return "mediaWiki.loader.register( '$name', $version, $dependencies, $group );\n"; |
| 563 | + return Xml::encodeJsCall( 'mediaWiki.loader.register', |
| 564 | + array( $name, $version, $dependencies, $group ) ); |
506 | 565 | } |
507 | 566 | } |
508 | 567 | |
| 568 | + /** |
| 569 | + * Returns JS code which runs given JS code if the client-side framework is |
| 570 | + * present. |
| 571 | + * |
| 572 | + * @param $script JS code to run |
| 573 | + */ |
509 | 574 | public static function makeLoaderConditionalScript( $script ) { |
510 | 575 | $script = str_replace( "\n", "\n\t", trim( $script ) ); |
511 | 576 | return "if ( window.mediaWiki ) {\n\t$script\n}\n"; |
512 | 577 | } |
513 | 578 | |
| 579 | + /** |
| 580 | + * Returns JS code which will set the MediaWiki configuration array to |
| 581 | + * the given value. |
| 582 | + * |
| 583 | + * @param $configuration Associative array of configuration parameters |
| 584 | + */ |
514 | 585 | public static function makeConfigSetScript( array $configuration ) { |
515 | | - $configuration = FormatJson::encode( $configuration ); |
516 | | - return "mediaWiki.config.set( $configuration );\n"; |
| 586 | + return Xml::encodeJsCall( 'mediaWiki.config.set', array( $configuration ) ); |
517 | 587 | } |
518 | 588 | } |
Index: trunk/phase3/includes/resourceloader/ResourceLoaderUserOptionsModule.php |
— | — | @@ -65,8 +65,8 @@ |
66 | 66 | } |
67 | 67 | |
68 | 68 | public function getScript( ResourceLoaderContext $context ) { |
69 | | - $encOptions = FormatJson::encode( $this->contextUserOptions( $context ) ); |
70 | | - return "mediaWiki.user.options.set( $encOptions );"; |
| 69 | + return Xml::encodeJsCall( 'mediaWiki.user.options.set', |
| 70 | + array( $this->contextUserOptions( $context ) ) ); |
71 | 71 | } |
72 | 72 | |
73 | 73 | public function getStyles( ResourceLoaderContext $context ) { |
Index: trunk/phase3/includes/resourceloader/ResourceLoaderFileModule.php |
— | — | @@ -197,8 +197,8 @@ |
198 | 198 | if ( $this->debugRaw ) { |
199 | 199 | $script = ''; |
200 | 200 | foreach ( $files as $file ) { |
201 | | - $path = FormatJson::encode( $wgServer . $this->getRemotePath( $file ) ); |
202 | | - $script .= "\n\tmediaWiki.loader.load( $path );"; |
| 201 | + $path = $wgServer . $this->getRemotePath( $file ); |
| 202 | + $script .= "\n\t" . Xml::encodeJsCall( 'mediaWiki.loader.load', array( $path ) ); |
203 | 203 | } |
204 | 204 | return $script; |
205 | 205 | } |
Index: trunk/phase3/includes/resourceloader/ResourceLoaderStartUpModule.php |
— | — | @@ -102,7 +102,8 @@ |
103 | 103 | $registrations = array(); |
104 | 104 | foreach ( $context->getResourceLoader()->getModules() as $name => $module ) { |
105 | 105 | // Support module loader scripts |
106 | | - if ( ( $loader = $module->getLoaderScript() ) !== false ) { |
| 106 | + $loader = $module->getLoaderScript(); |
| 107 | + if ( $loader !== false ) { |
107 | 108 | $deps = $module->getDependencies(); |
108 | 109 | $group = $module->getGroup(); |
109 | 110 | $version = wfTimestamp( TS_ISO_8601_BASIC, |
— | — | @@ -160,18 +161,17 @@ |
161 | 162 | ksort( $query ); |
162 | 163 | |
163 | 164 | // Startup function |
164 | | - $configuration = FormatJson::encode( $this->getConfig( $context ) ); |
| 165 | + $configuration = $this->getConfig( $context ); |
165 | 166 | $registrations = self::getModuleRegistrations( $context ); |
166 | 167 | $out .= "var startUp = function() {\n" . |
167 | 168 | "\t$registrations\n" . |
168 | | - "\tmediaWiki.config.set( $configuration );" . |
169 | | - "\n};"; |
| 169 | + "\t" . Xml::encodeJsCall( 'mediaWiki.config.set', array( $configuration ) ) . |
| 170 | + "};\n"; |
170 | 171 | |
171 | 172 | // Conditional script injection |
172 | | - $scriptTag = Xml::escapeJsString( |
173 | | - Html::linkedScript( $wgLoadScript . '?' . wfArrayToCGI( $query ) ) ); |
| 173 | + $scriptTag = Html::linkedScript( $wgLoadScript . '?' . wfArrayToCGI( $query ) ); |
174 | 174 | $out .= "if ( isCompatible() ) {\n" . |
175 | | - "\tdocument.write( '$scriptTag' );\n" . |
| 175 | + "\t" . Xml::encodeJsCall( 'document.write', array( $scriptTag ) ) . |
176 | 176 | "}\n" . |
177 | 177 | "delete isCompatible;"; |
178 | 178 | } |
Index: trunk/phase3/includes/resourceloader/ResourceLoaderWikiModule.php |
— | — | @@ -46,12 +46,14 @@ |
47 | 47 | return wfEmptyMsg( $page ) ? '' : wfMsgExt( $page, 'content' ); |
48 | 48 | } |
49 | 49 | $title = Title::newFromText( $page, $ns ); |
50 | | - if ( $title ) { |
51 | | - if ( $title->isValidCssJsSubpage() && $revision = Revision::newFromTitle( $title ) ) { |
52 | | - return $revision->getRawText(); |
53 | | - } |
| 50 | + if ( !$title || !$title->isValidCssJsSubpage() ) { |
| 51 | + return null; |
54 | 52 | } |
55 | | - return null; |
| 53 | + $revision = Revision::newFromTitle( $title ); |
| 54 | + if ( !$revision ) { |
| 55 | + return null; |
| 56 | + } |
| 57 | + return $revision->getRawText(); |
56 | 58 | } |
57 | 59 | |
58 | 60 | /* Methods */ |
— | — | @@ -59,13 +61,14 @@ |
60 | 62 | public function getScript( ResourceLoaderContext $context ) { |
61 | 63 | $scripts = ''; |
62 | 64 | foreach ( $this->getPages( $context ) as $page => $options ) { |
63 | | - if ( $options['type'] === 'script' ) { |
64 | | - $script = $this->getContent( $page, $options['ns'] ); |
65 | | - if ( $script ) { |
66 | | - $ns = MWNamespace::getCanonicalName( $options['ns'] ); |
67 | | - $scripts .= "/*$ns:$page */\n$script\n"; |
68 | | - } |
| 65 | + if ( $options['type'] !== 'script' ) { |
| 66 | + continue; |
69 | 67 | } |
| 68 | + $script = $this->getContent( $page, $options['ns'] ); |
| 69 | + if ( $script ) { |
| 70 | + $ns = MWNamespace::getCanonicalName( $options['ns'] ); |
| 71 | + $scripts .= "/* $ns:$page */\n$script\n"; |
| 72 | + } |
70 | 73 | } |
71 | 74 | return $scripts; |
72 | 75 | } |
— | — | @@ -74,17 +77,19 @@ |
75 | 78 | |
76 | 79 | $styles = array(); |
77 | 80 | foreach ( $this->getPages( $context ) as $page => $options ) { |
78 | | - if ( $options['type'] === 'style' ) { |
79 | | - $media = isset( $options['media'] ) ? $options['media'] : 'all'; |
80 | | - $style = $this->getContent( $page, $options['ns'] ); |
81 | | - if ( $style ) { |
82 | | - if ( !isset( $styles[$media] ) ) { |
83 | | - $styles[$media] = ''; |
84 | | - } |
85 | | - $ns = MWNamespace::getCanonicalName( $options['ns'] ); |
86 | | - $styles[$media] .= "/* $ns:$page */\n$style\n"; |
87 | | - } |
| 81 | + if ( $options['type'] !== 'style' ) { |
| 82 | + continue; |
88 | 83 | } |
| 84 | + $media = isset( $options['media'] ) ? $options['media'] : 'all'; |
| 85 | + $style = $this->getContent( $page, $options['ns'] ); |
| 86 | + if ( !$style ) { |
| 87 | + continue; |
| 88 | + } |
| 89 | + if ( !isset( $styles[$media] ) ) { |
| 90 | + $styles[$media] = ''; |
| 91 | + } |
| 92 | + $ns = MWNamespace::getCanonicalName( $options['ns'] ); |
| 93 | + $styles[$media] .= "/* $ns:$page */\n$style\n"; |
89 | 94 | } |
90 | 95 | return $styles; |
91 | 96 | } |
Index: trunk/phase3/includes/AutoLoader.php |
— | — | @@ -259,6 +259,7 @@ |
260 | 260 | 'XCacheBagOStuff' => 'includes/BagOStuff.php', |
261 | 261 | 'XmlDumpWriter' => 'includes/Export.php', |
262 | 262 | 'Xml' => 'includes/Xml.php', |
| 263 | + 'XmlJsCode' => 'includes/Xml.php', |
263 | 264 | 'XmlSelect' => 'includes/Xml.php', |
264 | 265 | 'XmlTypeCheck' => 'includes/XmlTypeCheck.php', |
265 | 266 | 'ZhClient' => 'includes/ZhClient.php', |
Index: trunk/phase3/includes/MessageBlobStore.php |
— | — | @@ -310,7 +310,7 @@ |
311 | 311 | $decoded = FormatJson::decode( $blob, true ); |
312 | 312 | $decoded[$key] = wfMsgExt( $key, array( 'language' => $lang ) ); |
313 | 313 | |
314 | | - return FormatJson::encode( $decoded ); |
| 314 | + return FormatJson::encode( (object)$decoded ); |
315 | 315 | } |
316 | 316 | |
317 | 317 | /** |
— | — | @@ -365,6 +365,6 @@ |
366 | 366 | $messages[$key] = wfMsgExt( $key, array( 'language' => $lang ) ); |
367 | 367 | } |
368 | 368 | |
369 | | - return FormatJson::encode( $messages ); |
| 369 | + return FormatJson::encode( (object)$messages ); |
370 | 370 | } |
371 | 371 | } |