| Index: trunk/phase3/docs/hooks.txt |
| — | — | @@ -1337,6 +1337,9 @@ |
| 1338 | 1338 | $article: the page the form is shown for |
| 1339 | 1339 | $out: OutputPage object |
| 1340 | 1340 | |
| | 1341 | +'ResourceLoaderRegisterModules': Right before modules information is required, such as when responding to a resource |
| | 1342 | +loader request or generating HTML output. |
| | 1343 | + |
| 1341 | 1344 | 'RawPageViewBeforeOutput': Right before the text is blown out in action=raw |
| 1342 | 1345 | &$obj: RawPage object |
| 1343 | 1346 | &$text: The text that's going to be the output |
| Index: trunk/phase3/includes/ResourceLoaderContext.php |
| — | — | @@ -27,7 +27,7 @@ |
| 28 | 28 | * of a specific loader request |
| 29 | 29 | */ |
| 30 | 30 | class ResourceLoaderContext { |
| 31 | | - |
| | 31 | + |
| 32 | 32 | /* Protected Members */ |
| 33 | 33 | |
| 34 | 34 | protected $request; |
| Index: trunk/phase3/includes/OutputPage.php |
| — | — | @@ -2282,7 +2282,8 @@ |
| 2283 | 2283 | |
| 2284 | 2284 | // TODO: Document |
| 2285 | 2285 | static function makeResourceLoaderLink( $skin, $modules, $only, $useESI = false ) { |
| 2286 | | - global $wgUser, $wgLang, $wgRequest, $wgLoadScript, $wgResourceLoaderDebug, $wgResourceLoaderUseESI; |
| | 2286 | + global $wgUser, $wgLang, $wgRequest, $wgLoadScript, $wgResourceLoaderDebug, $wgResourceLoaderUseESI, |
| | 2287 | + $wgResourceLoaderInlinePrivateModules; |
| 2287 | 2288 | // TODO: Should this be a static function of ResourceLoader instead? |
| 2288 | 2289 | // TODO: Divide off modules starting with "user", and add the user parameter to them |
| 2289 | 2290 | $query = array( |
| — | — | @@ -2308,20 +2309,38 @@ |
| 2309 | 2310 | $links = ''; |
| 2310 | 2311 | foreach ( $groups as $group => $modules ) { |
| 2311 | 2312 | $query['modules'] = implode( '|', array_keys( $modules ) ); |
| 2312 | | - // Special handling for user group |
| 2313 | | - if ( $group === 'user' && $wgUser->isLoggedIn() ) { |
| | 2313 | + // Special handling for user-specific groups |
| | 2314 | + if ( ( $group === 'user' || $group === 'private' ) && $wgUser->isLoggedIn() ) { |
| 2314 | 2315 | $query['user'] = $wgUser->getName(); |
| 2315 | 2316 | } |
| | 2317 | + // Support inlining of private modules if configured as such |
| | 2318 | + if ( $group === 'private' && $wgResourceLoaderInlinePrivateModules ) { |
| | 2319 | + $context = new ResourceLoaderContext( new FauxRequest( $query ) ); |
| | 2320 | + if ( $only == 'styles' ) { |
| | 2321 | + $links .= Html::inlineStyle( |
| | 2322 | + ResourceLoader::makeLoaderConditionalScript( |
| | 2323 | + ResourceLoader::makeModuleResponse( $context, $modules ) |
| | 2324 | + ) |
| | 2325 | + ); |
| | 2326 | + } else { |
| | 2327 | + $links .= Html::inlineScript( |
| | 2328 | + ResourceLoader::makeLoaderConditionalScript( |
| | 2329 | + ResourceLoader::makeModuleResponse( $context, $modules ) |
| | 2330 | + ) |
| | 2331 | + ); |
| | 2332 | + } |
| | 2333 | + continue; |
| | 2334 | + } |
| 2316 | 2335 | // Special handling for user and site groups; because users might change their stuff on-wiki like site or |
| 2317 | 2336 | // user pages, or user preferences; we need to find the highest timestamp of these user-changable modules so |
| 2318 | 2337 | // we can ensure cache misses on change |
| 2319 | 2338 | if ( $group === 'user' || $group === 'site' ) { |
| 2320 | 2339 | // Create a fake request based on the one we are about to make so modules return correct times |
| 2321 | | - $request = new ResourceLoaderContext( new FauxRequest( $query ) ); |
| | 2340 | + $context = new ResourceLoaderContext( new FauxRequest( $query ) ); |
| 2322 | 2341 | // Get the maximum timestamp |
| 2323 | 2342 | $timestamp = 0; |
| 2324 | 2343 | foreach ( $modules as $module ) { |
| 2325 | | - $timestamp = max( $timestamp, $module->getModifiedTime( $request ) ); |
| | 2344 | + $timestamp = max( $timestamp, $module->getModifiedTime( $context ) ); |
| 2326 | 2345 | } |
| 2327 | 2346 | // Add a version parameter so cache will break when things change |
| 2328 | 2347 | $query['version'] = wfTimestamp( TS_ISO_8601, round( $timestamp, -2 ) ); |
| Index: trunk/phase3/includes/ResourceLoader.php |
| — | — | @@ -59,7 +59,7 @@ |
| 60 | 60 | * This is not inside the module code because it's so much more performant to request all of the information at once |
| 61 | 61 | * than it is to have each module requests it's own information. |
| 62 | 62 | * |
| 63 | | - * @param $modules array list of modules to preload information for |
| | 63 | + * @param $modules array list of module names to preload information for |
| 64 | 64 | * @param $context ResourceLoaderContext context to load the information within |
| 65 | 65 | */ |
| 66 | 66 | protected static function preloadModuleInfo( array $modules, ResourceLoaderContext $context ) { |
| — | — | @@ -268,10 +268,10 @@ |
| 269 | 269 | // Split requested modules into two groups, modules and missing |
| 270 | 270 | $modules = array(); |
| 271 | 271 | $missing = array(); |
| 272 | | - |
| | 272 | + |
| 273 | 273 | foreach ( $context->getModules() as $name ) { |
| 274 | 274 | if ( isset( self::$modules[$name] ) ) { |
| 275 | | - $modules[] = $name; |
| | 275 | + $modules[$name] = self::$modules[$name]; |
| 276 | 276 | } else { |
| 277 | 277 | $missing[] = $name; |
| 278 | 278 | } |
| — | — | @@ -291,14 +291,19 @@ |
| 292 | 292 | } |
| 293 | 293 | |
| 294 | 294 | // Preload information needed to the mtime calculation below |
| 295 | | - self::preloadModuleInfo( $modules, $context ); |
| | 295 | + self::preloadModuleInfo( array_keys( $modules ), $context ); |
| 296 | 296 | |
| 297 | 297 | // To send Last-Modified and support If-Modified-Since, we need to detect |
| 298 | 298 | // the last modified time |
| 299 | 299 | wfProfileIn( __METHOD__.'-getModifiedTime' ); |
| 300 | 300 | $mtime = 1; |
| 301 | | - foreach ( $modules as $name ) { |
| 302 | | - $mtime = max( $mtime, self::$modules[$name]->getModifiedTime( $context ) ); |
| | 301 | + foreach ( $modules as $module ) { |
| | 302 | + // Bypass squid cache if the request includes any private modules |
| | 303 | + if ( $module->getGroup() === 'private' ) { |
| | 304 | + $smaxage = 0; |
| | 305 | + } |
| | 306 | + // Calculate maximum modified time |
| | 307 | + $mtime = max( $mtime, $module->getModifiedTime( $context ) ); |
| 303 | 308 | } |
| 304 | 309 | wfProfileOut( __METHOD__.'-getModifiedTime' ); |
| 305 | 310 | |
| — | — | @@ -316,26 +321,34 @@ |
| 317 | 322 | return; |
| 318 | 323 | } |
| 319 | 324 | |
| | 325 | + echo self::makeModuleResponse( $context, $modules, $missing ); |
| | 326 | + |
| | 327 | + wfProfileOut( __METHOD__ ); |
| | 328 | + } |
| | 329 | + |
| | 330 | + public static function makeModuleResponse( ResourceLoaderContext $context, array $modules, $missing = null ) { |
| | 331 | + global $wgUser; |
| | 332 | + |
| 320 | 333 | // Pre-fetch blobs |
| 321 | 334 | $blobs = $context->shouldIncludeMessages() ? |
| 322 | | - MessageBlobStore::get( $modules, $context->getLanguage() ) : array(); |
| | 335 | + MessageBlobStore::get( array_keys( $modules ), $context->getLanguage() ) : array(); |
| 323 | 336 | |
| 324 | 337 | // Generate output |
| 325 | 338 | $out = ''; |
| 326 | | - foreach ( $modules as $name ) { |
| | 339 | + foreach ( $modules as $name => $module ) { |
| 327 | 340 | wfProfileIn( __METHOD__ . '-' . $name ); |
| 328 | 341 | |
| 329 | 342 | // Scripts |
| 330 | 343 | $scripts = ''; |
| 331 | 344 | if ( $context->shouldIncludeScripts() ) { |
| 332 | | - $scripts .= self::$modules[$name]->getScript( $context ) . "\n"; |
| | 345 | + $scripts .= $module->getScript( $context ) . "\n"; |
| 333 | 346 | } |
| 334 | 347 | |
| 335 | 348 | // Styles |
| 336 | 349 | $styles = array(); |
| 337 | 350 | if ( |
| 338 | 351 | $context->shouldIncludeStyles() && |
| 339 | | - ( count( $styles = self::$modules[$name]->getStyles( $context ) ) ) |
| | 352 | + ( count( $styles = $module->getStyles( $context ) ) ) |
| 340 | 353 | ) { |
| 341 | 354 | // Flip CSS on a per-module basis |
| 342 | 355 | if ( self::$modules[$name]->getFlip( $context ) ) { |
| — | — | @@ -360,7 +373,7 @@ |
| 361 | 374 | $out .= self::makeMessageSetScript( $messages ); |
| 362 | 375 | break; |
| 363 | 376 | default: |
| 364 | | - // Minify CSS, unless in debug mode, before embedding in implment script |
| | 377 | + // Minify CSS before embedding in mediaWiki.loader.implement call (unless in debug mode) |
| 365 | 378 | if ( !$context->getDebug() ) { |
| 366 | 379 | foreach ( $styles as $media => $style ) { |
| 367 | 380 | $styles[$media] = self::filter( 'minify-css', $style ); |
| — | — | @@ -376,29 +389,28 @@ |
| 377 | 390 | // Update module states |
| 378 | 391 | if ( $context->shouldIncludeScripts() ) { |
| 379 | 392 | // Set the state of modules loaded as only scripts to ready |
| 380 | | - if ( count( $modules ) && $context->getOnly() === 'scripts' && !in_array( 'startup', $modules ) ) { |
| 381 | | - $out .= self::makeLoaderStateScript( array_fill_keys( $modules, 'ready' ) ); |
| | 393 | + if ( count( $modules ) && $context->getOnly() === 'scripts' && !isset( $modules['startup'] ) ) { |
| | 394 | + $out .= self::makeLoaderStateScript( array_fill_keys( array_keys( $modules ), 'ready' ) ); |
| 382 | 395 | } |
| 383 | 396 | // Set the state of modules which were requested but unavailable as missing |
| 384 | | - if ( count( $missing ) ) { |
| | 397 | + if ( is_array( $missing ) && count( $missing ) ) { |
| 385 | 398 | $out .= self::makeLoaderStateScript( array_fill_keys( $missing, 'missing' ) ); |
| 386 | 399 | } |
| 387 | 400 | } |
| 388 | 401 | |
| 389 | | - // Send output |
| 390 | 402 | if ( $context->getDebug() ) { |
| 391 | | - echo $out; |
| | 403 | + return $out; |
| 392 | 404 | } else { |
| 393 | 405 | if ( $context->getOnly() === 'styles' ) { |
| 394 | | - echo self::filter( 'minify-css', $out ); |
| | 406 | + return self::filter( 'minify-css', $out ); |
| 395 | 407 | } else { |
| 396 | | - echo self::filter( 'minify-js', $out ); |
| | 408 | + return self::filter( 'minify-js', $out ); |
| 397 | 409 | } |
| 398 | 410 | } |
| 399 | | - |
| 400 | | - wfProfileOut( __METHOD__ ); |
| 401 | 411 | } |
| 402 | | - |
| | 412 | + |
| | 413 | + // Client code generation methods |
| | 414 | + |
| 403 | 415 | public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) { |
| 404 | 416 | if ( is_array( $scripts ) ) { |
| 405 | 417 | $scripts = implode( $scripts, "\n" ); |
| — | — | @@ -437,7 +449,7 @@ |
| 438 | 450 | return "mediaWiki.loader.state( '$name', '$state' );\n"; |
| 439 | 451 | } |
| 440 | 452 | } |
| 441 | | - |
| | 453 | + |
| 442 | 454 | public static function makeCustomLoaderScript( $name, $version, $dependencies, $group, $script ) { |
| 443 | 455 | $name = Xml::escapeJsString( $name ); |
| 444 | 456 | $version = (int) $version > 1 ? (int) $version : 1; |
| — | — | @@ -454,10 +466,10 @@ |
| 455 | 467 | $group = 'null'; |
| 456 | 468 | } |
| 457 | 469 | $script = str_replace( "\n", "\n\t", trim( $script ) ); |
| 458 | | - return "( function( name, version, dependencies ) {\t$script\t} )" . |
| | 470 | + return "( function( name, version, dependencies ) {\n\t$script\n} )" . |
| 459 | 471 | "( '$name', $version, $dependencies, $group );\n"; |
| 460 | 472 | } |
| 461 | | - |
| | 473 | + |
| 462 | 474 | public static function makeLoaderRegisterScript( $name, $version = null, $dependencies = null, $group = null ) { |
| 463 | 475 | if ( is_array( $name ) ) { |
| 464 | 476 | $registrations = FormatJson::encode( $name ); |
| — | — | @@ -480,4 +492,14 @@ |
| 481 | 493 | return "mediaWiki.loader.register( '$name', $version, $dependencies, $group );\n"; |
| 482 | 494 | } |
| 483 | 495 | } |
| | 496 | + |
| | 497 | + public static function makeLoaderConditionalScript( $script ) { |
| | 498 | + $script = str_replace( "\n", "\n\t", trim( $script ) ); |
| | 499 | + return "if ( window.mediaWiki ) {\n\t$script\n}\n"; |
| | 500 | + } |
| | 501 | + |
| | 502 | + public static function makeConfigSetScript( array $configuration ) { |
| | 503 | + $configuration = FormatJson::encode( $configuration ); |
| | 504 | + return "mediaWiki.config.set( $configuration );\n"; |
| | 505 | + } |
| 484 | 506 | } |
| Index: trunk/phase3/includes/DefaultSettings.php |
| — | — | @@ -1661,6 +1661,12 @@ |
| 1662 | 1662 | ); |
| 1663 | 1663 | |
| 1664 | 1664 | /** |
| | 1665 | + * Whether to embed private modules inline with HTML output or to bypass caching and check the user parameter against |
| | 1666 | + * $wgUser to prevent unauthorized access to private modules. |
| | 1667 | + */ |
| | 1668 | +$wgResourceLoaderInlinePrivateModules = true; |
| | 1669 | + |
| | 1670 | +/** |
| 1665 | 1671 | * The default debug mode (on/off) for of ResourceLoader requests. This will still |
| 1666 | 1672 | * be overridden when the debug URL parameter is used. |
| 1667 | 1673 | */ |
| Index: trunk/phase3/includes/Skin.php |
| — | — | @@ -358,7 +358,9 @@ |
| 359 | 359 | |
| 360 | 360 | static function makeVariablesScript( $data ) { |
| 361 | 361 | if ( $data ) { |
| 362 | | - return Html::inlineScript( 'mediaWiki.config.set(' . FormatJson::encode( $data ) . ');' ); |
| | 362 | + return Html::inlineScript( |
| | 363 | + ResourceLoader::makeLoaderConditionalScript( ResourceLoader::makeConfigSetScript( $data ) ) |
| | 364 | + ); |
| 363 | 365 | } else { |
| 364 | 366 | return ''; |
| 365 | 367 | } |
| Index: trunk/phase3/includes/ResourceLoaderModule.php |
| — | — | @@ -205,7 +205,6 @@ |
| 206 | 206 | $this->msgBlobMtime[$lang] = $mtime; |
| 207 | 207 | } |
| 208 | 208 | |
| 209 | | - |
| 210 | 209 | /* Abstract Methods */ |
| 211 | 210 | |
| 212 | 211 | /** |
| — | — | @@ -905,21 +904,20 @@ |
| 906 | 905 | } |
| 907 | 906 | |
| 908 | 907 | global $wgUser; |
| 909 | | - $username = $context->getUser(); |
| 910 | | - // Avoid extra db query by using $wgUser if possible |
| 911 | | - $user = $wgUser->getName() === $username ? $wgUser : User::newFromName( $username ); |
| 912 | 908 | |
| 913 | | - if ( $user ) { |
| 914 | | - return $this->modifiedTime[$hash] = $user->getTouched(); |
| | 909 | + if ( $context->getUser() === $wgUser->getName() ) { |
| | 910 | + return $this->modifiedTime[$hash] = $wgUser->getTouched(); |
| 915 | 911 | } else { |
| 916 | 912 | return 1; |
| 917 | 913 | } |
| 918 | 914 | } |
| 919 | 915 | |
| 920 | 916 | public function getScript( ResourceLoaderContext $context ) { |
| 921 | | - $user = User::newFromName( $context->getUser() ); |
| 922 | | - if ( $user instanceof User ) { |
| 923 | | - $options = FormatJson::encode( $user->getOptions() ); |
| | 917 | + global $wgUser; |
| | 918 | + |
| | 919 | + // Verify identity -- this is a private module |
| | 920 | + if ( $context->getUser() === $wgUser->getName() ) { |
| | 921 | + $options = FormatJson::encode( $wgUser->getOptions() ); |
| 924 | 922 | } else { |
| 925 | 923 | $options = FormatJson::encode( User::getDefaultOptions() ); |
| 926 | 924 | } |
| — | — | @@ -927,11 +925,17 @@ |
| 928 | 926 | } |
| 929 | 927 | |
| 930 | 928 | public function getStyles( ResourceLoaderContext $context ) { |
| 931 | | - global $wgAllowUserCssPrefs; |
| | 929 | + global $wgUser, $wgAllowUserCssPrefs; |
| | 930 | + |
| 932 | 931 | if ( $wgAllowUserCssPrefs ) { |
| 933 | | - $user = User::newFromName( $context->getUser() ); |
| 934 | | - $options = $user instanceof User ? $user->getOptions() : User::getDefaultOptions(); |
| 935 | | - |
| | 932 | + // Verify identity -- this is a private module |
| | 933 | + if ( $context->getUser() === $wgUser->getName() ) { |
| | 934 | + $options = FormatJson::encode( $wgUser->getOptions() ); |
| | 935 | + } else { |
| | 936 | + $options = FormatJson::encode( User::getDefaultOptions() ); |
| | 937 | + } |
| | 938 | + |
| | 939 | + // Build CSS rules |
| 936 | 940 | $rules = array(); |
| 937 | 941 | if ( $options['underline'] < 2 ) { |
| 938 | 942 | $rules[] = "a { text-decoration: " . ( $options['underline'] ? 'underline' : 'none' ) . "; }"; |
| — | — | @@ -965,9 +969,9 @@ |
| 966 | 970 | |
| 967 | 971 | return $wgContLang->getDir() !== $context->getDirection(); |
| 968 | 972 | } |
| 969 | | - |
| | 973 | + |
| 970 | 974 | public function getGroup() { |
| 971 | | - return 'user'; |
| | 975 | + return 'private'; |
| 972 | 976 | } |
| 973 | 977 | } |
| 974 | 978 | |
| Index: trunk/phase3/RELEASE-NOTES |
| — | — | @@ -62,6 +62,11 @@ |
| 63 | 63 | version parameter or not. |
| 64 | 64 | * $wgResourceLoaderDebug was added to specify the default state of debug mode; |
| 65 | 65 | this will still be overridden with the debug URL parameter a la $wgLanguageCode. |
| | 66 | +* $wgResourceLoaderInlinePrivateModules was added to specify whether private |
| | 67 | + modules such as user.options should be embedded in the HTML output or delivered |
| | 68 | + through a resource loader request, which bypasses server cache (like squid) and |
| | 69 | + checks the user parameter against $wgUser. The former adds more data to all |
| | 70 | + pages, while the latter adds a request which cannot be cached server side. |
| 66 | 71 | |
| 67 | 72 | === New features in 1.17 === |
| 68 | 73 | * (bug 10183) Users can now add personal styles and scripts to all skins via |