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 |