r75994 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r75993‎ | r75994 | r75995 >
Date:07:53, 4 November 2010
Author:tstarling
Status:ok (Comments)
Tags:
Comment:
* Introduced Xml::encodeJsCall(), to replace the awkward repetitive code that was doing the same thing throughout the resource loader with varying degrees of security and correctness.
* Modified Xml::encodeJsVar() to allow it to pass through JS expressions without encoding, using a special object.
* In ResourceLoader::makeModuleResponse(), renamed $messages to $messagesBlob to make it clear that it's JSON-encoded, not an array.
* Fixed MessageBlobStore to store {} for an empty message array instead of [].
* In ResourceLoader::makeMessageSetScript(), fixed call to non-existent function mediaWiki.msg.set.
* For security, changed the calling convention of makeMessageSetScript() and makeLoaderImplementScript() to require explicit object construction of XmlJsCode() before interpreting their input as JS code.
* Documented several ResourceLoader static functions.
* In ResourceLoaderWikiModule, for readability, reduced the indenting level by flipping some if blocks and adding continue statements.
* In makeCustomLoaderScript(), allow non-numeric $version. The only caller I can find is already sending a non-numeric $version, presumably it was broken. Luckily there aren't any loader scripts in existence, I had to make one to test it.
* wfGetDb -> wfGetDB
* Added an extra line break in the startup module output, for readability.
* In ResourceLoaderStartUpModule::getModuleRegistrations(), fixed another assignment expression
Modified paths:
  • /trunk/phase3/includes/AutoLoader.php (modified) (history)
  • /trunk/phase3/includes/MessageBlobStore.php (modified) (history)
  • /trunk/phase3/includes/Xml.php (modified) (history)
  • /trunk/phase3/includes/resourceloader/ResourceLoader.php (modified) (history)
  • /trunk/phase3/includes/resourceloader/ResourceLoaderFileModule.php (modified) (history)
  • /trunk/phase3/includes/resourceloader/ResourceLoaderStartUpModule.php (modified) (history)
  • /trunk/phase3/includes/resourceloader/ResourceLoaderUserOptionsModule.php (modified) (history)
  • /trunk/phase3/includes/resourceloader/ResourceLoaderWikiModule.php (modified) (history)

Diff [purge]

Index: trunk/phase3/includes/Xml.php
@@ -594,6 +594,8 @@
595595 $s .= self::encodeJsVar( $elt );
596596 }
597597 $s .= ']';
 598+ } elseif ( $value instanceof XmlJsCode ) {
 599+ $s = $value->value;
598600 } elseif ( is_object( $value ) || is_array( $value ) ) {
599601 // Objects and associative arrays
600602 $s = '{';
@@ -611,7 +613,30 @@
612614 return $s;
613615 }
614616
 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+ }
615639
 640+
616641 /**
617642 * Check if a string is well-formed XML.
618643 * Must include the surrounding tag.
@@ -813,3 +838,22 @@
814839 }
815840
816841 }
 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 @@
5454 if ( !count( $modules ) ) {
5555 return; // or else Database*::select() will explode, plus it's cheaper!
5656 }
57 - $dbr = wfGetDb( DB_SLAVE );
 57+ $dbr = wfGetDB( DB_SLAVE );
5858 $skin = $context->getSkin();
5959 $lang = $context->getLanguage();
6060
@@ -385,7 +385,7 @@
386386 }
387387
388388 // Messages
389 - $messages = isset( $blobs[$name] ) ? $blobs[$name] : '{}';
 389+ $messagesBlob = isset( $blobs[$name] ) ? $blobs[$name] : array();
390390
391391 // Append output
392392 switch ( $context->getOnly() ) {
@@ -396,7 +396,7 @@
397397 $out .= self::makeCombinedStyles( $styles );
398398 break;
399399 case 'messages':
400 - $out .= self::makeMessageSetScript( $messages );
 400+ $out .= self::makeMessageSetScript( new XmlJsCode( $messagesBlob ) );
401401 break;
402402 default:
403403 // Minify CSS before embedding in mediaWiki.loader.implement call
@@ -406,7 +406,8 @@
407407 $styles[$media] = $this->filter( 'minify-css', $style );
408408 }
409409 }
410 - $out .= self::makeLoaderImplementScript( $name, $scripts, $styles, $messages );
 410+ $out .= self::makeLoaderImplementScript( $name, $scripts, $styles,
 411+ new XmlJsCode( $messagesBlob ) );
411412 break;
412413 }
413414
@@ -441,26 +442,49 @@
442443
443444 /* Static Methods */
444445
 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+ */
445458 public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) {
446459 if ( is_array( $scripts ) ) {
447460 $scripts = implode( $scripts, "\n" );
448461 }
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+ ) );
456470 }
457471
 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+ */
458479 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 ) );
463481 }
464482
 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+ */
465489 public static function makeCombinedStyles( array $styles ) {
466490 $out = '';
467491 foreach ( $styles as $media => $style ) {
@@ -469,49 +493,95 @@
470494 return $out;
471495 }
472496
 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+ */
473507 public static function makeLoaderStateScript( $name, $state = null ) {
474508 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 ) );
477510 } 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 ) );
481512 }
482513 }
483514
 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+ */
484527 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 );
489528 $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 ) );
492532 }
493533
 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+ */
494556 public static function makeLoaderRegisterScript( $name, $version = null,
495557 $dependencies = null, $group = null )
496558 {
497559 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 ) );
500561 } else {
501 - $name = Xml::escapeJsString( $name );
502562 $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 ) );
506565 }
507566 }
508567
 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+ */
509574 public static function makeLoaderConditionalScript( $script ) {
510575 $script = str_replace( "\n", "\n\t", trim( $script ) );
511576 return "if ( window.mediaWiki ) {\n\t$script\n}\n";
512577 }
513578
 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+ */
514585 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 ) );
517587 }
518588 }
Index: trunk/phase3/includes/resourceloader/ResourceLoaderUserOptionsModule.php
@@ -65,8 +65,8 @@
6666 }
6767
6868 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 ) ) );
7171 }
7272
7373 public function getStyles( ResourceLoaderContext $context ) {
Index: trunk/phase3/includes/resourceloader/ResourceLoaderFileModule.php
@@ -197,8 +197,8 @@
198198 if ( $this->debugRaw ) {
199199 $script = '';
200200 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 ) );
203203 }
204204 return $script;
205205 }
Index: trunk/phase3/includes/resourceloader/ResourceLoaderStartUpModule.php
@@ -102,7 +102,8 @@
103103 $registrations = array();
104104 foreach ( $context->getResourceLoader()->getModules() as $name => $module ) {
105105 // Support module loader scripts
106 - if ( ( $loader = $module->getLoaderScript() ) !== false ) {
 106+ $loader = $module->getLoaderScript();
 107+ if ( $loader !== false ) {
107108 $deps = $module->getDependencies();
108109 $group = $module->getGroup();
109110 $version = wfTimestamp( TS_ISO_8601_BASIC,
@@ -160,18 +161,17 @@
161162 ksort( $query );
162163
163164 // Startup function
164 - $configuration = FormatJson::encode( $this->getConfig( $context ) );
 165+ $configuration = $this->getConfig( $context );
165166 $registrations = self::getModuleRegistrations( $context );
166167 $out .= "var startUp = function() {\n" .
167168 "\t$registrations\n" .
168 - "\tmediaWiki.config.set( $configuration );" .
169 - "\n};";
 169+ "\t" . Xml::encodeJsCall( 'mediaWiki.config.set', array( $configuration ) ) .
 170+ "};\n";
170171
171172 // Conditional script injection
172 - $scriptTag = Xml::escapeJsString(
173 - Html::linkedScript( $wgLoadScript . '?' . wfArrayToCGI( $query ) ) );
 173+ $scriptTag = Html::linkedScript( $wgLoadScript . '?' . wfArrayToCGI( $query ) );
174174 $out .= "if ( isCompatible() ) {\n" .
175 - "\tdocument.write( '$scriptTag' );\n" .
 175+ "\t" . Xml::encodeJsCall( 'document.write', array( $scriptTag ) ) .
176176 "}\n" .
177177 "delete isCompatible;";
178178 }
Index: trunk/phase3/includes/resourceloader/ResourceLoaderWikiModule.php
@@ -46,12 +46,14 @@
4747 return wfEmptyMsg( $page ) ? '' : wfMsgExt( $page, 'content' );
4848 }
4949 $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;
5452 }
55 - return null;
 53+ $revision = Revision::newFromTitle( $title );
 54+ if ( !$revision ) {
 55+ return null;
 56+ }
 57+ return $revision->getRawText();
5658 }
5759
5860 /* Methods */
@@ -59,13 +61,14 @@
6062 public function getScript( ResourceLoaderContext $context ) {
6163 $scripts = '';
6264 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;
6967 }
 68+ $script = $this->getContent( $page, $options['ns'] );
 69+ if ( $script ) {
 70+ $ns = MWNamespace::getCanonicalName( $options['ns'] );
 71+ $scripts .= "/* $ns:$page */\n$script\n";
 72+ }
7073 }
7174 return $scripts;
7275 }
@@ -74,17 +77,19 @@
7578
7679 $styles = array();
7780 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;
8883 }
 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";
8994 }
9095 return $styles;
9196 }
Index: trunk/phase3/includes/AutoLoader.php
@@ -259,6 +259,7 @@
260260 'XCacheBagOStuff' => 'includes/BagOStuff.php',
261261 'XmlDumpWriter' => 'includes/Export.php',
262262 'Xml' => 'includes/Xml.php',
 263+ 'XmlJsCode' => 'includes/Xml.php',
263264 'XmlSelect' => 'includes/Xml.php',
264265 'XmlTypeCheck' => 'includes/XmlTypeCheck.php',
265266 'ZhClient' => 'includes/ZhClient.php',
Index: trunk/phase3/includes/MessageBlobStore.php
@@ -310,7 +310,7 @@
311311 $decoded = FormatJson::decode( $blob, true );
312312 $decoded[$key] = wfMsgExt( $key, array( 'language' => $lang ) );
313313
314 - return FormatJson::encode( $decoded );
 314+ return FormatJson::encode( (object)$decoded );
315315 }
316316
317317 /**
@@ -365,6 +365,6 @@
366366 $messages[$key] = wfMsgExt( $key, array( 'language' => $lang ) );
367367 }
368368
369 - return FormatJson::encode( $messages );
 369+ return FormatJson::encode( (object)$messages );
370370 }
371371 }

Follow-up revisions

RevisionCommit summaryAuthorDate
r76006Added @since tags for stuff introduced in r75994nikerabbit14:14, 4 November 2010
r77594Fix r75994 per CR: don't pass an array as a JS string if MessageBlobStore::ge...catrope18:45, 2 December 2010

Comments

#Comment by Nikerabbit (talk | contribs)   12:48, 4 November 2010

I wonder why you chose to add the code in the Xml class. Is it because it already had JS related functions? I also wonder why the params are passed as an array instead of varargs.

#Comment by Tim Starling (talk | contribs)   00:02, 5 November 2010

1. Because it already had Xml::encodeJsVar() which is almost the same thing.

2.

<TimStarling> what do you think is better: Xml::encodeJsCall( $name, array( $arg1, $arg2 ) ) or Xml::encodeJsCall( $name, $arg1, $arg2 )?
<TimStarling> I'm leaning towards the first one
<TrevorParscal> I prefer the first
<TrevorParscal> when you are abstracting things, variadics become irritating quickly

#Comment by Catrope (talk | contribs)   10:43, 8 November 2010
+			$messagesBlob = isset( $blobs[$name] ) ? $blobs[$name] : array();
[...]
+					$out .= self::makeMessageSetScript( new XmlJsCode( $messagesBlob ) );

When the second branch of the ternary is taken (fortunately, MessageBlobStore::get() is written such that this'll never happen), array() will be passed into the XmlJsCode constructor, which gets stringified somewhere along the way and ends up as mediaWiki.messages.set(Array).

+		foreach ( $args as $arg ) {
+			if ( $first ) {
+				$first = false;
+			} else {
+				$s .= ', ';
+			}
+			$s .= Xml::encodeJsVar( $arg );
+		}

How about implode( ', ', array_map( array( 'Xml', 'encodeJsVar' ), $args ); ? Or does that qualify as "excessively cute"? :)

#Comment by Tim Starling (talk | contribs)   23:00, 8 November 2010

It's cute, but probably not excessively so.

#Comment by Catrope (talk | contribs)   18:45, 2 December 2010

array() vs. '{}' issue fixed in r77594.

Status & tagging log