From: Tim Starling Date: Thu, 4 Nov 2010 07:53:37 +0000 (+0000) Subject: * Introduced Xml::encodeJsCall(), to replace the awkward repetitive code that was... X-Git-Tag: 1.31.0-rc.0~34103 X-Git-Url: http://git.cyclocoop.org/data/%7B%24admin_url%7Dconfig?a=commitdiff_plain;h=82f274088a61476e309dcf7a6796e7f38aa7b2a7;p=lhc%2Fweb%2Fwiklou.git * 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 --- diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 4bb9d66187..f93c0ad007 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -259,6 +259,7 @@ $wgAutoloadLocalClasses = array( 'XCacheBagOStuff' => 'includes/BagOStuff.php', 'XmlDumpWriter' => 'includes/Export.php', 'Xml' => 'includes/Xml.php', + 'XmlJsCode' => 'includes/Xml.php', 'XmlSelect' => 'includes/Xml.php', 'XmlTypeCheck' => 'includes/XmlTypeCheck.php', 'ZhClient' => 'includes/ZhClient.php', diff --git a/includes/MessageBlobStore.php b/includes/MessageBlobStore.php index ca229c8eed..d180582d08 100644 --- a/includes/MessageBlobStore.php +++ b/includes/MessageBlobStore.php @@ -310,7 +310,7 @@ class MessageBlobStore { $decoded = FormatJson::decode( $blob, true ); $decoded[$key] = wfMsgExt( $key, array( 'language' => $lang ) ); - return FormatJson::encode( $decoded ); + return FormatJson::encode( (object)$decoded ); } /** @@ -365,6 +365,6 @@ class MessageBlobStore { $messages[$key] = wfMsgExt( $key, array( 'language' => $lang ) ); } - return FormatJson::encode( $messages ); + return FormatJson::encode( (object)$messages ); } } diff --git a/includes/Xml.php b/includes/Xml.php index 77b43d9bf4..ef2eda7096 100644 --- a/includes/Xml.php +++ b/includes/Xml.php @@ -594,6 +594,8 @@ class Xml { $s .= self::encodeJsVar( $elt ); } $s .= ']'; + } elseif ( $value instanceof XmlJsCode ) { + $s = $value->value; } elseif ( is_object( $value ) || is_array( $value ) ) { // Objects and associative arrays $s = '{'; @@ -611,6 +613,29 @@ class Xml { return $s; } + /** + * Create a call to a JavaScript function. The supplied arguments will be + * encoded using Xml::encodeJsVar(). + * + * @param $name The name of the function to call, or a JavaScript expression + * which evaluates to a function object which is called. + * @param $args Array of arguments to pass to the function. + */ + public static function encodeJsCall( $name, $args ) { + $s = "$name("; + $first = true; + foreach ( $args as $arg ) { + if ( $first ) { + $first = false; + } else { + $s .= ', '; + } + $s .= Xml::encodeJsVar( $arg ); + } + $s .= ");\n"; + return $s; + } + /** * Check if a string is well-formed XML. @@ -813,3 +838,22 @@ class XmlSelect { } } + +/** + * A wrapper class which causes Xml::encodeJsVar() and Xml::encodeJsCall() to + * interpret a given string as being a JavaScript expression, instead of string + * data. + * + * Example: + * + * Xml::encodeJsVar( new XmlJsCode( 'a + b' ) ); + * + * Returns "a + b". + */ +class XmlJsCode { + public $value; + + function __construct( $value ) { + $this->value = $value; + } +} diff --git a/includes/resourceloader/ResourceLoader.php b/includes/resourceloader/ResourceLoader.php index 93f2b1bdb3..3ce7148f29 100644 --- a/includes/resourceloader/ResourceLoader.php +++ b/includes/resourceloader/ResourceLoader.php @@ -53,7 +53,7 @@ class ResourceLoader { if ( !count( $modules ) ) { return; // or else Database*::select() will explode, plus it's cheaper! } - $dbr = wfGetDb( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $skin = $context->getSkin(); $lang = $context->getLanguage(); @@ -385,7 +385,7 @@ class ResourceLoader { } // Messages - $messages = isset( $blobs[$name] ) ? $blobs[$name] : '{}'; + $messagesBlob = isset( $blobs[$name] ) ? $blobs[$name] : array(); // Append output switch ( $context->getOnly() ) { @@ -396,7 +396,7 @@ class ResourceLoader { $out .= self::makeCombinedStyles( $styles ); break; case 'messages': - $out .= self::makeMessageSetScript( $messages ); + $out .= self::makeMessageSetScript( new XmlJsCode( $messagesBlob ) ); break; default: // Minify CSS before embedding in mediaWiki.loader.implement call @@ -406,7 +406,8 @@ class ResourceLoader { $styles[$media] = $this->filter( 'minify-css', $style ); } } - $out .= self::makeLoaderImplementScript( $name, $scripts, $styles, $messages ); + $out .= self::makeLoaderImplementScript( $name, $scripts, $styles, + new XmlJsCode( $messagesBlob ) ); break; } @@ -441,26 +442,49 @@ class ResourceLoader { /* Static Methods */ + /** + * Returns JS code to call to mediaWiki.loader.implement for a module with + * given properties. + * + * @param $name Module name + * @param $scripts Array of JavaScript code snippets to be executed after the + * module is loaded + * @param $styles Associative array mapping media type to associated CSS string + * @param $messages Messages associated with this module. May either be an + * associative array mapping message key to value, or a JSON-encoded message blob + * containing the same data, wrapped in an XmlJsCode object. + */ public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) { if ( is_array( $scripts ) ) { $scripts = implode( $scripts, "\n" ); } - if ( is_array( $styles ) ) { - $styles = count( $styles ) ? FormatJson::encode( $styles ) : 'null'; - } - if ( is_array( $messages ) ) { - $messages = count( $messages ) ? FormatJson::encode( $messages ) : 'null'; - } - return "mediaWiki.loader.implement( '$name', function() {{$scripts}},\n$styles,\n$messages );\n"; + return Xml::encodeJsCall( + 'mediaWiki.loader.implement', + array( + $name, + new XmlJsCode( "function() {{$scripts}}" ), + (object)$styles, + (object)$messages + ) ); } + /** + * Returns JS code which, when called, will register a given list of messages. + * + * @param $messages May either be an associative array mapping message key + * to value, or a JSON-encoded message blob containing the same data, + * wrapped in an XmlJsCode object. + */ public static function makeMessageSetScript( $messages ) { - if ( is_array( $messages ) ) { - $messages = count( $messages ) ? FormatJson::encode( $messages ) : 'null'; - } - return "mediaWiki.msg.set( $messages );\n"; + return Xml::encodeJsCall( 'mediaWiki.messages.set', array( (object)$messages ) ); } + /** + * Combines an associative array mapping media type to CSS into a + * single stylesheet with @media blocks. + * + * @param $styles Array of CSS strings + */ public static function makeCombinedStyles( array $styles ) { $out = ''; foreach ( $styles as $media => $style ) { @@ -469,49 +493,95 @@ class ResourceLoader { return $out; } + /** + * Returns a JS call to mediaWiki.loader.state, which sets the state of a + * module or modules to a given value. Has two calling conventions: + * + * - ResourceLoader::makeLoaderStateScript( $name, $state ): + * Set the state of a single module called $name to $state + * + * - ResourceLoader::makeLoaderStateScript( array( $name => $state, ... ) ): + * Set the state of modules with the given names to the given states + */ public static function makeLoaderStateScript( $name, $state = null ) { if ( is_array( $name ) ) { - $statuses = FormatJson::encode( $name ); - return "mediaWiki.loader.state( $statuses );\n"; + return Xml::encodeJsCall( 'mediaWiki.loader.state', array( $name ) ); } else { - $name = Xml::escapeJsString( $name ); - $state = Xml::escapeJsString( $state ); - return "mediaWiki.loader.state( '$name', '$state' );\n"; + return Xml::encodeJsCall( 'mediaWiki.loader.state', array( $name, $state ) ); } } + /** + * Returns JS code which calls the script given by $script. The script will + * be called with local variables name, version, dependencies and group, + * which will have values corresponding to $name, $version, $dependencies + * and $group as supplied. + * + * @param $name The module name + * @param $version The module version string + * @param $dependencies Array of module names on which this module depends + * @param $group The group which the module is in. + * @param $script The JS loader script + */ public static function makeCustomLoaderScript( $name, $version, $dependencies, $group, $script ) { - $name = Xml::escapeJsString( $name ); - $version = (int) $version > 1 ? (int) $version : 1; - $dependencies = FormatJson::encode( $dependencies ); - $group = FormatJson::encode( $group ); $script = str_replace( "\n", "\n\t", trim( $script ) ); - return "( function( name, version, dependencies, group ) {\n\t$script\n} )" . - "( '$name', $version, $dependencies, $group );\n"; + return Xml::encodeJsCall( + "( function( name, version, dependencies, group ) {\n\t$script\n} )", + array( $name, $version, $dependencies, $group ) ); } + /** + * Returns JS code which calls mediaWiki.loader.register with the given + * parameters. Has three calling conventions: + * + * - ResourceLoader::makeLoaderRegisterScript( $name, $version, $dependencies, $group ): + * Register a single module. + * + * - ResourceLoader::makeLoaderRegisterScript( array( $name1, $name2 ) ): + * Register modules with the given names. + * + * - ResourceLoader::makeLoaderRegisterScript( array( + * array( $name1, $version1, $dependencies1, $group1 ), + * array( $name2, $version2, $dependencies1, $group2 ), + * ... + * ) ): + * Registers modules with the given names and parameters. + * + * @param $name The module name + * @param $version The module version string + * @param $dependencies Array of module names on which this module depends + * @param $group The group which the module is in. + */ public static function makeLoaderRegisterScript( $name, $version = null, $dependencies = null, $group = null ) { if ( is_array( $name ) ) { - $registrations = FormatJson::encode( $name ); - return "mediaWiki.loader.register( $registrations );\n"; + return Xml::encodeJsCall( 'mediaWiki.loader.register', array( $name ) ); } else { - $name = Xml::escapeJsString( $name ); $version = (int) $version > 1 ? (int) $version : 1; - $dependencies = FormatJson::encode( $dependencies ); - $group = FormatJson::encode( $group ); - return "mediaWiki.loader.register( '$name', $version, $dependencies, $group );\n"; + return Xml::encodeJsCall( 'mediaWiki.loader.register', + array( $name, $version, $dependencies, $group ) ); } } + /** + * Returns JS code which runs given JS code if the client-side framework is + * present. + * + * @param $script JS code to run + */ public static function makeLoaderConditionalScript( $script ) { $script = str_replace( "\n", "\n\t", trim( $script ) ); return "if ( window.mediaWiki ) {\n\t$script\n}\n"; } + /** + * Returns JS code which will set the MediaWiki configuration array to + * the given value. + * + * @param $configuration Associative array of configuration parameters + */ public static function makeConfigSetScript( array $configuration ) { - $configuration = FormatJson::encode( $configuration ); - return "mediaWiki.config.set( $configuration );\n"; + return Xml::encodeJsCall( 'mediaWiki.config.set', array( $configuration ) ); } } diff --git a/includes/resourceloader/ResourceLoaderFileModule.php b/includes/resourceloader/ResourceLoaderFileModule.php index bff4812175..9decac6cd3 100644 --- a/includes/resourceloader/ResourceLoaderFileModule.php +++ b/includes/resourceloader/ResourceLoaderFileModule.php @@ -197,8 +197,8 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { if ( $this->debugRaw ) { $script = ''; foreach ( $files as $file ) { - $path = FormatJson::encode( $wgServer . $this->getRemotePath( $file ) ); - $script .= "\n\tmediaWiki.loader.load( $path );"; + $path = $wgServer . $this->getRemotePath( $file ); + $script .= "\n\t" . Xml::encodeJsCall( 'mediaWiki.loader.load', array( $path ) ); } return $script; } diff --git a/includes/resourceloader/ResourceLoaderStartUpModule.php b/includes/resourceloader/ResourceLoaderStartUpModule.php index 0229f61440..4e7f0b9214 100644 --- a/includes/resourceloader/ResourceLoaderStartUpModule.php +++ b/includes/resourceloader/ResourceLoaderStartUpModule.php @@ -102,7 +102,8 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { $registrations = array(); foreach ( $context->getResourceLoader()->getModules() as $name => $module ) { // Support module loader scripts - if ( ( $loader = $module->getLoaderScript() ) !== false ) { + $loader = $module->getLoaderScript(); + if ( $loader !== false ) { $deps = $module->getDependencies(); $group = $module->getGroup(); $version = wfTimestamp( TS_ISO_8601_BASIC, @@ -160,18 +161,17 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { ksort( $query ); // Startup function - $configuration = FormatJson::encode( $this->getConfig( $context ) ); + $configuration = $this->getConfig( $context ); $registrations = self::getModuleRegistrations( $context ); $out .= "var startUp = function() {\n" . "\t$registrations\n" . - "\tmediaWiki.config.set( $configuration );" . - "\n};"; + "\t" . Xml::encodeJsCall( 'mediaWiki.config.set', array( $configuration ) ) . + "};\n"; // Conditional script injection - $scriptTag = Xml::escapeJsString( - Html::linkedScript( $wgLoadScript . '?' . wfArrayToCGI( $query ) ) ); + $scriptTag = Html::linkedScript( $wgLoadScript . '?' . wfArrayToCGI( $query ) ); $out .= "if ( isCompatible() ) {\n" . - "\tdocument.write( '$scriptTag' );\n" . + "\t" . Xml::encodeJsCall( 'document.write', array( $scriptTag ) ) . "}\n" . "delete isCompatible;"; } diff --git a/includes/resourceloader/ResourceLoaderUserOptionsModule.php b/includes/resourceloader/ResourceLoaderUserOptionsModule.php index ceae2240d2..f32d08938a 100644 --- a/includes/resourceloader/ResourceLoaderUserOptionsModule.php +++ b/includes/resourceloader/ResourceLoaderUserOptionsModule.php @@ -65,8 +65,8 @@ class ResourceLoaderUserOptionsModule extends ResourceLoaderModule { } public function getScript( ResourceLoaderContext $context ) { - $encOptions = FormatJson::encode( $this->contextUserOptions( $context ) ); - return "mediaWiki.user.options.set( $encOptions );"; + return Xml::encodeJsCall( 'mediaWiki.user.options.set', + array( $this->contextUserOptions( $context ) ) ); } public function getStyles( ResourceLoaderContext $context ) { diff --git a/includes/resourceloader/ResourceLoaderWikiModule.php b/includes/resourceloader/ResourceLoaderWikiModule.php index a395cf055c..92c8779a98 100644 --- a/includes/resourceloader/ResourceLoaderWikiModule.php +++ b/includes/resourceloader/ResourceLoaderWikiModule.php @@ -46,12 +46,14 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { return wfEmptyMsg( $page ) ? '' : wfMsgExt( $page, 'content' ); } $title = Title::newFromText( $page, $ns ); - if ( $title ) { - if ( $title->isValidCssJsSubpage() && $revision = Revision::newFromTitle( $title ) ) { - return $revision->getRawText(); - } + if ( !$title || !$title->isValidCssJsSubpage() ) { + return null; + } + $revision = Revision::newFromTitle( $title ); + if ( !$revision ) { + return null; } - return null; + return $revision->getRawText(); } /* Methods */ @@ -59,12 +61,13 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { public function getScript( ResourceLoaderContext $context ) { $scripts = ''; foreach ( $this->getPages( $context ) as $page => $options ) { - if ( $options['type'] === 'script' ) { - $script = $this->getContent( $page, $options['ns'] ); - if ( $script ) { - $ns = MWNamespace::getCanonicalName( $options['ns'] ); - $scripts .= "/*$ns:$page */\n$script\n"; - } + if ( $options['type'] !== 'script' ) { + continue; + } + $script = $this->getContent( $page, $options['ns'] ); + if ( $script ) { + $ns = MWNamespace::getCanonicalName( $options['ns'] ); + $scripts .= "/* $ns:$page */\n$script\n"; } } return $scripts; @@ -74,17 +77,19 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { $styles = array(); foreach ( $this->getPages( $context ) as $page => $options ) { - if ( $options['type'] === 'style' ) { - $media = isset( $options['media'] ) ? $options['media'] : 'all'; - $style = $this->getContent( $page, $options['ns'] ); - if ( $style ) { - if ( !isset( $styles[$media] ) ) { - $styles[$media] = ''; - } - $ns = MWNamespace::getCanonicalName( $options['ns'] ); - $styles[$media] .= "/* $ns:$page */\n$style\n"; - } + if ( $options['type'] !== 'style' ) { + continue; + } + $media = isset( $options['media'] ) ? $options['media'] : 'all'; + $style = $this->getContent( $page, $options['ns'] ); + if ( !$style ) { + continue; + } + if ( !isset( $styles[$media] ) ) { + $styles[$media] = ''; } + $ns = MWNamespace::getCanonicalName( $options['ns'] ); + $styles[$media] .= "/* $ns:$page */\n$style\n"; } return $styles; }