ResourceLoader.php: Fix E_NOTICE
[lhc/web/wiklou.git] / includes / resourceloader / ResourceLoader.php
index f64cef7..8b1452e 100644 (file)
@@ -1,5 +1,7 @@
 <?php
 /**
+ * Base class for resource loading system.
+ *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  * the Free Software Foundation; either version 2 of the License, or
@@ -29,7 +31,7 @@
 class ResourceLoader {
 
        /* Protected Static Members */
-       protected static $filterCacheVersion = 6;
+       protected static $filterCacheVersion = 7;
        protected static $requiredSourceProperties = array( 'loadScript' );
 
        /** Array: List of module name/ResourceLoaderModule object pairs */
@@ -37,6 +39,10 @@ class ResourceLoader {
 
        /** Associative array mapping module name to info associative array */
        protected $moduleInfos = array();
+       /** Associative array mapping framework ids to a list of names of test suite modules */
+       /** like array( 'qunit' => array( 'mediawiki.tests.qunit.suites', 'ext.foo.tests', .. ), .. ) */
+       protected $testModuleNames = array();
 
        /** array( 'source-id' => array( 'loadScript' => 'http://.../load.php' ) ) **/
        protected $sources = array();
@@ -169,7 +175,7 @@ class ResourceLoader {
                        $cache->set( $key, $result );
                } catch ( Exception $exception ) {
                        // Return exception as a comment
-                       $result = "/*\n{$exception->__toString()}\n*/\n";
+                       $result = $this->makeComment( $exception->__toString() );
                }
 
                wfProfileOut( __METHOD__ );
@@ -183,7 +189,7 @@ class ResourceLoader {
         * Registers core modules and runs registration hooks.
         */
        public function __construct() {
-               global $IP, $wgResourceModules, $wgResourceLoaderSources, $wgLoadScript;
+               global $IP, $wgResourceModules, $wgResourceLoaderSources, $wgLoadScript, $wgEnableJavaScriptTest;
 
                wfProfileIn( __METHOD__ );
 
@@ -199,6 +205,11 @@ class ResourceLoader {
                wfRunHooks( 'ResourceLoaderRegisterModules', array( &$this ) );
                $this->register( $wgResourceModules );
 
+               if ( $wgEnableJavaScriptTest === true ) {
+                       $this->registerTestModules();
+               }
+
+
                wfProfileOut( __METHOD__ );
        }
 
@@ -206,7 +217,7 @@ class ResourceLoader {
         * Registers a module with the ResourceLoader system.
         *
         * @param $name Mixed: Name of module as a string or List of name/object pairs as an array
-        * @param $info Module info array. For backwards compatibility with 1.17alpha,
+        * @param $info array Module info array. For backwards compatibility with 1.17alpha,
         *   this may also be a ResourceLoaderModule object. Optional when using
         *   multiple-registration calling style.
         * @throws MWException: If a duplicate module registration is attempted
@@ -230,9 +241,9 @@ class ResourceLoader {
                                );
                        }
 
-                       // Check $name for illegal characters
-                       if ( preg_match( '/[|,!]/', $name ) ) {
-                               throw new MWException( "ResourceLoader module name '$name' is invalid. Names may not contain pipes (|), commas (,) or exclamation marks (!)" );
+                       // Check $name for validity
+                       if ( !self::isValidModuleName( $name ) ) {
+                               throw new MWException( "ResourceLoader module name '$name' is invalid, see ResourceLoader::isValidModuleName()" );
                        }
 
                        // Attach module
@@ -256,6 +267,40 @@ class ResourceLoader {
                wfProfileOut( __METHOD__ );
        }
 
+       /**
+        */
+       public function registerTestModules() {
+               global $IP, $wgEnableJavaScriptTest;
+
+               if ( $wgEnableJavaScriptTest !== true ) {
+                       throw new MWException( 'Attempt to register JavaScript test modules but <tt>$wgEnableJavaScriptTest</tt> is false. Edit your <tt>LocalSettings.php</tt> to enable it.' );
+               }
+
+               wfProfileIn( __METHOD__ );
+
+               // Get core test suites
+               $testModules = array();
+               $testModules['qunit'] = include( "$IP/tests/qunit/QUnitTestResources.php" );
+               // Get other test suites (e.g. from extensions)
+               wfRunHooks( 'ResourceLoaderTestModules', array( &$testModules, &$this ) );
+
+               // Add the testrunner (which configures QUnit) to the dependencies.
+               // Since it must be ready before any of the test suites are executed.
+               foreach( $testModules['qunit'] as $moduleName => $moduleProps ) {
+                       $testModules['qunit'][$moduleName]['dependencies'][] = 'mediawiki.tests.qunit.testrunner';
+               }
+
+               foreach( $testModules as $id => $names ) {
+                       // Register test modules
+                       $this->register( $testModules[$id] );
+
+                       // Keep track of their names so that they can be loaded together
+                       $this->testModuleNames[$id] = array_keys( $testModules[$id] );
+               }
+
+               wfProfileOut( __METHOD__ );
+       }
+
        /**
         * Add a foreign source of modules.
         *
@@ -300,6 +345,26 @@ class ResourceLoader {
        public function getModuleNames() {
                return array_keys( $this->moduleInfos );
        }
+       /**
+        * Get a list of test module names for one (or all) frameworks.
+        * If the given framework id is unknkown, or if the in-object variable is not an array,
+        * then it will return an empty array.
+        *
+        * @param $framework String: Optional. Get only the test module names for one
+        * particular framework.
+        * @return Array
+        */
+       public function getTestModuleNames( $framework = 'all' ) {
+               /// @TODO: api siteinfo prop testmodulenames modulenames
+               if ( $framework == 'all' ) {
+                       return $this->testModuleNames;
+               } elseif ( isset( $this->testModuleNames[$framework] ) && is_array( $this->testModuleNames[$framework] ) ) {
+                       return $this->testModuleNames[$framework];
+               } else {
+                       return array();
+               }
+       }
 
        /**
         * Get the ResourceLoaderModule object for a given module name.
@@ -368,13 +433,20 @@ class ResourceLoader {
                ob_start();
 
                wfProfileIn( __METHOD__ );
-               $exceptions = '';
+               $errors = '';
 
                // Split requested modules into two groups, modules and missing
                $modules = array();
                $missing = array();
                foreach ( $context->getModules() as $name ) {
                        if ( isset( $this->moduleInfos[$name] ) ) {
+                               $module = $this->getModule( $name );
+                               // Do not allow private modules to be loaded from the web.
+                               // This is a security issue, see bug 34907.
+                               if ( $module->getGroup() === 'private' ) {
+                                       $errors .= $this->makeComment( "Cannot show private module \"$name\"" );
+                                       continue;
+                               }
                                $modules[$name] = $this->getModule( $name );
                        } else {
                                $missing[] = $name;
@@ -386,12 +458,11 @@ class ResourceLoader {
                        $this->preloadModuleInfo( array_keys( $modules ), $context );
                } catch( Exception $e ) {
                        // Add exception to the output as a comment
-                       $exceptions .= "/*\n{$e->__toString()}\n*/\n";
+                       $errors .= $this->makeComment( $e->__toString() );
                }
 
                wfProfileIn( __METHOD__.'-getModifiedTime' );
 
-               $private = false;
                // To send Last-Modified and support If-Modified-Since, we need to detect
                // the last modified time
                $mtime = wfTimestamp( TS_UNIX, $wgCacheEpoch );
@@ -400,22 +471,18 @@ class ResourceLoader {
                         * @var $module ResourceLoaderModule
                         */
                        try {
-                               // Bypass Squid and other shared caches if the request includes any private modules
-                               if ( $module->getGroup() === 'private' ) {
-                                       $private = true;
-                               }
                                // Calculate maximum modified time
                                $mtime = max( $mtime, $module->getModifiedTime( $context ) );
                        } catch ( Exception $e ) {
                                // Add exception to the output as a comment
-                               $exceptions .= "/*\n{$e->__toString()}\n*/\n";
+                               $errors .= $this->makeComment( $e->__toString() );
                        }
                }
 
                wfProfileOut( __METHOD__.'-getModifiedTime' );
 
                // Send content type and cache related headers
-               $this->sendResponseHeaders( $context, $mtime, $private );
+               $this->sendResponseHeaders( $context, $mtime );
 
                // If there's an If-Modified-Since header, respond with a 304 appropriately
                if ( $this->tryRespondLastModified( $context, $mtime ) ) {
@@ -427,20 +494,16 @@ class ResourceLoader {
                $response = $this->makeModuleResponse( $context, $modules, $missing );
 
                // Prepend comments indicating exceptions
-               $response = $exceptions . $response;
+               $response = $errors . $response;
 
                // Capture any PHP warnings from the output buffer and append them to the
                // response in a comment if we're in debug mode.
                if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) {
-                       $response = "/*\n$warnings\n*/\n" . $response;
+                       $response = $this->makeComment( $warnings ) . $response;
                }
 
-               // Remove the output buffer and output the response
-               ob_end_clean();
-               echo $response;
-
-               // Save response to file cache unless there are private modules or errors
-               if ( isset( $fileCache ) && !$private && !$exceptions && !$missing ) {
+               // Save response to file cache unless there are errors
+               if ( isset( $fileCache ) && !$errors && !$missing ) {
                        // Cache single modules...and other requests if there are enough hits
                        if ( ResourceFileCache::useFileCache( $context ) ) {
                                if ( $fileCache->isCacheWorthy() ) {
@@ -451,6 +514,10 @@ class ResourceLoader {
                        }
                }
 
+               // Remove the output buffer and output the response
+               ob_end_clean();
+               echo $response;
+
                wfProfileOut( __METHOD__ );
        }
 
@@ -458,10 +525,9 @@ class ResourceLoader {
         * Send content type and last modified headers to the client.
         * @param $context ResourceLoaderContext
         * @param $mtime string TS_MW timestamp to use for last-modified
-        * @param $private bool True iff response contains any private modules
         * @return void
         */
-       protected function sendResponseHeaders( ResourceLoaderContext $context, $mtime, $private ) {
+       protected function sendResponseHeaders( ResourceLoaderContext $context, $mtime ) {
                global $wgResourceLoaderMaxage;
                // If a version wasn't specified we need a shorter expiry time for updates
                // to propagate to clients quickly
@@ -485,13 +551,8 @@ class ResourceLoader {
                        header( 'Cache-Control: private, no-cache, must-revalidate' );
                        header( 'Pragma: no-cache' );
                } else {
-                       if ( $private ) {
-                               header( "Cache-Control: private, max-age=$maxage" );
-                               $exp = $maxage;
-                       } else {
-                               header( "Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
-                               $exp = min( $maxage, $smaxage );
-                       }
+                       header( "Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
+                       $exp = min( $maxage, $smaxage );
                        header( 'Expires: ' . wfTimestamp( TS_RFC2822, $exp + time() ) );
                }
        }
@@ -531,7 +592,6 @@ class ResourceLoader {
 
                                header( 'HTTP/1.0 304 Not Modified' );
                                header( 'Status: 304 Not Modified' );
-                               wfProfileOut( __METHOD__ );
                                return true;
                        }
                }
@@ -541,7 +601,7 @@ class ResourceLoader {
        /**
         * Send out code for a response from file cache if possible
         *
-        * @param $fileCache ObjectFileCache: Cache object for this request URL
+        * @param $fileCache ResourceFileCache: Cache object for this request URL
         * @param $context ResourceLoaderContext: Context in which to generate a response
         * @return bool If this found a cache file and handled the response
         */
@@ -573,6 +633,11 @@ class ResourceLoader {
                                return false; // output handled (buffers cleared)
                        }
                        $response = $fileCache->fetchText();
+                       // Capture any PHP warnings from the output buffer and append them to the
+                       // response in a comment if we're in debug mode.
+                       if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) {
+                               $response = "/*\n$warnings\n*/\n" . $response;
+                       }
                        // Remove the output buffer and output the response
                        ob_end_clean();
                        echo $response . "\n/* Cached {$ts} */";
@@ -584,6 +649,11 @@ class ResourceLoader {
                return false; // cache miss
        }
 
+       protected function makeComment( $text ) {
+               $encText = str_replace( '*/', '* /', $text );
+               return "/*\n$encText\n*/\n";
+       }
+
        /**
         * Generates code for a response
         *
@@ -608,13 +678,14 @@ class ResourceLoader {
                                $blobs = MessageBlobStore::get( $this, $modules, $context->getLanguage() );
                        } catch ( Exception $e ) {
                                // Add exception to the output as a comment
-                               $exceptions .= "/*\n{$e->__toString()}\n*/\n";
+                               $exceptions .= $this->makeComment( $e->__toString() );
                        }
                } else {
                        $blobs = array();
                }
 
                // Generate output
+               $isRaw = false;
                foreach ( $modules as $name => $module ) {
                        /**
                         * @var $module ResourceLoaderModule
@@ -642,12 +713,27 @@ class ResourceLoader {
                                // Styles
                                $styles = array();
                                if ( $context->shouldIncludeStyles() ) {
-                                       // If we are in debug mode, we'll want to return an array of URLs
-                                       // See comment near shouldIncludeScripts() for more details
-                                       if ( $context->getDebug() && !$context->getOnly() && $module->supportsURLLoading() ) {
-                                               $styles = $module->getStyleURLsForDebug( $context );
-                                       } else {
-                                               $styles = $module->getStyles( $context );
+                                       // Don't create empty stylesheets like array( '' => '' ) for modules
+                                       // that don't *have* any stylesheets (bug 38024).
+                                       $stylePairs = $module->getStyles( $context );
+                                       if ( count ( $stylePairs ) ) {
+                                               // If we are in debug mode without &only= set, we'll want to return an array of URLs
+                                               // See comment near shouldIncludeScripts() for more details
+                                               if ( $context->getDebug() && !$context->getOnly() && $module->supportsURLLoading() ) {
+                                                       $styles = $module->getStyleURLsForDebug( $context );
+                                               } else {
+                                                       // Minify CSS before embedding in mw.loader.implement call
+                                                       // (unless in debug mode)
+                                                       if ( !$context->getDebug() ) {
+                                                               foreach ( $stylePairs as $media => $style ) {
+                                                                       if ( is_string( $style ) ) {
+                                                                               $stylePairs[$media] = $this->filter( 'minify-css', $style );
+                                                                       }
+                                                               }
+                                                       }
+                                                       // Combine styles into @media groups as one big string
+                                                       $styles = array( '' => self::makeCombinedStyles( $stylePairs ) );
+                                               }
                                        }
                                }
 
@@ -666,42 +752,40 @@ class ResourceLoader {
                                                }
                                                break;
                                        case 'styles':
-                                               $out .= self::makeCombinedStyles( $styles );
+                                               // We no longer seperate into media, they are all concatenated now with
+                                               // custom media type groups into @media .. {} sections.
+                                               // Module returns either an empty array or an array with '' (no media type) as
+                                               // only key.
+                                               $out .= isset( $styles[''] ) ? $styles[''] : '';
                                                break;
                                        case 'messages':
                                                $out .= self::makeMessageSetScript( new XmlJsCode( $messagesBlob ) );
                                                break;
                                        default:
-                                               // Minify CSS before embedding in mw.loader.implement call
-                                               // (unless in debug mode)
-                                               if ( !$context->getDebug() ) {
-                                                       foreach ( $styles as $media => $style ) {
-                                                               if ( is_string( $style ) ) {
-                                                                       $styles[$media] = $this->filter( 'minify-css', $style );
-                                                               }
-                                                       }
-                                               }
-                                               $out .= self::makeLoaderImplementScript( $name, $scripts, $styles,
-                                                       new XmlJsCode( $messagesBlob ) );
+                                               $out .= self::makeLoaderImplementScript(
+                                                       $name,
+                                                       $scripts,
+                                                       $styles,
+                                                       new XmlJsCode( $messagesBlob )
+                                               );
                                                break;
                                }
                        } catch ( Exception $e ) {
                                // Add exception to the output as a comment
-                               $exceptions .= "/*\n{$e->__toString()}\n*/\n";
+                               $exceptions .= $this->makeComment( $e->__toString() );
 
                                // Register module as missing
                                $missing[] = $name;
                                unset( $modules[$name] );
                        }
+                       $isRaw |= $module->isRaw();
                        wfProfileOut( __METHOD__ . '-' . $name );
                }
 
                // Update module states
-               if ( $context->shouldIncludeScripts() ) {
+               if ( $context->shouldIncludeScripts() && !$context->getRaw() && !$isRaw ) {
                        // Set the state of modules loaded as only scripts to ready
-                       if ( count( $modules ) && $context->getOnly() === 'scripts'
-                               && !isset( $modules['startup'] ) )
-                       {
+                       if ( count( $modules ) && $context->getOnly() === 'scripts' ) {
                                $out .= self::makeLoaderStateScript(
                                        array_fill_keys( array_keys( $modules ), 'ready' ) );
                        }
@@ -729,9 +813,9 @@ class ResourceLoader {
         * Returns JS code to call to mw.loader.implement for a module with
         * given properties.
         *
-        * @param $name Module name
+        * @param $name string Module name
         * @param $scripts Mixed: List of URLs to JavaScript files or String of JavaScript code
-        * @param $styles Mixed: List of CSS strings keyed by media type, or list of lists of URLs to
+        * @param $styles Mixed: Array of CSS strings keyed by media type, or an array of lists of URLs to
         * CSS files keyed by media type
         * @param $messages Mixed: List of messages associated with this module. May either be an
         *     associative array mapping message key to value, or a JSON-encoded message blob containing
@@ -741,7 +825,7 @@ class ResourceLoader {
         */
        public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) {
                if ( is_string( $scripts ) ) {
-                       $scripts = new XmlJsCode( "function( $ ) {{$scripts}}" );
+                       $scripts = new XmlJsCode( "function () {\n{$scripts}\n}" );
                } elseif ( !is_array( $scripts ) ) {
                        throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' );
                }
@@ -750,6 +834,11 @@ class ResourceLoader {
                        array(
                                $name,
                                $scripts,
+                               // Force objects. mw.loader.implement requires them to be javascript objects.
+                               // Although these variables are associative arrays, which become javascript
+                               // objects through json_encode. In many cases they will be empty arrays, and
+                               // PHP/json_encode() consider empty arrays to be numerical arrays and
+                               // output javascript "[]" instead of "{}". This fixes that.
                                (object)$styles,
                                (object)$messages
                        ) );
@@ -835,7 +924,7 @@ class ResourceLoader {
        public static function makeCustomLoaderScript( $name, $version, $dependencies, $group, $source, $script ) {
                $script = str_replace( "\n", "\n\t", trim( $script ) );
                return Xml::encodeJsCall(
-                       "( function( name, version, dependencies, group, source ) {\n\t$script\n} )",
+                       "( function ( name, version, dependencies, group, source ) {\n\t$script\n} )",
                        array( $name, $version, $dependencies, $group, $source ) );
        }
 
@@ -908,8 +997,7 @@ class ResourceLoader {
         * @return string
         */
        public static function makeLoaderConditionalScript( $script ) {
-               $script = str_replace( "\n", "\n\t", trim( $script ) );
-               return "if(window.mw){\n\t$script\n}\n";
+               return "if(window.mw){\n" . trim( $script ) . "\n}";
        }
 
        /**
@@ -1025,4 +1113,17 @@ class ResourceLoader {
                ksort( $query );
                return $query;
        }
+
+       /**
+        * Check a module name for validity.
+        *
+        * Module names may not contain pipes (|), commas (,) or exclamation marks (!) and can be
+        * at most 255 bytes.
+        *
+        * @param $moduleName string Module name to check
+        * @return bool Whether $moduleName is a valid module name
+        */
+       public static function isValidModuleName( $moduleName ) {
+               return !preg_match( '/[|,!]/', $moduleName ) && strlen( $moduleName ) <= 255;
+       }
 }