* Introduced Xml::encodeJsCall(), to replace the awkward repetitive code that was...
authorTim Starling <tstarling@users.mediawiki.org>
Thu, 4 Nov 2010 07:53:37 +0000 (07:53 +0000)
committerTim Starling <tstarling@users.mediawiki.org>
Thu, 4 Nov 2010 07:53:37 +0000 (07:53 +0000)
* 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

includes/AutoLoader.php
includes/MessageBlobStore.php
includes/Xml.php
includes/resourceloader/ResourceLoader.php
includes/resourceloader/ResourceLoaderFileModule.php
includes/resourceloader/ResourceLoaderStartUpModule.php
includes/resourceloader/ResourceLoaderUserOptionsModule.php
includes/resourceloader/ResourceLoaderWikiModule.php

index 4bb9d66..f93c0ad 100644 (file)
@@ -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',
index ca229c8..d180582 100644 (file)
@@ -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 );
        }
 }
index 77b43d9..ef2eda7 100644 (file)
@@ -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;
+       }
+}
index 93f2b1b..3ce7148 100644 (file)
@@ -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 ) );
        }
 }
index bff4812..9decac6 100644 (file)
@@ -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;
                        }
index 0229f61..4e7f0b9 100644 (file)
@@ -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;";
                }
index ceae224..f32d089 100644 (file)
@@ -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 ) {
index a395cf0..92c8779 100644 (file)
@@ -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;
        }