Introduce ResourceLoaderLessVarFileModule
authorjdlrobson <jdlrobson@gmail.com>
Fri, 20 Apr 2018 18:42:41 +0000 (11:42 -0700)
committerjdlrobson <jdlrobson@gmail.com>
Wed, 25 Apr 2018 18:27:11 +0000 (11:27 -0700)
This ResourceLoader module provides a way to ship
messages to CSS variables.

We will need this going forward to deal with flash of
unstyled content in various JavaScript based UIs that
are subject to i18n such as table sorting and
collapsible elements.

To avoid overhead of hitting the database to fetch and
transform localisation messages we make use of the MessageBlobStore
making use of `messages` definition already inside
ResourceLoaderFileModule. Given this resource is only intended
for render blocking styles without JavaScript this should be okay
(although if requested in JavaScript will also ship associated
messages)

Bug: T42812
Change-Id: I2bf12cdc848478889acbe9a7a970e46f8aefa287

autoload.php
includes/resourceloader/ResourceLoaderLessVarFileModule.php [new file with mode: 0644]
tests/phpunit/includes/resourceloader/ResourceLoaderLessVarFileModuleTest.php [new file with mode: 0644]

index 881d0dd..12958ca 100644 (file)
@@ -1284,6 +1284,7 @@ $wgAutoloadLocalClasses = [
        'ResourceLoaderJqueryMsgModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderJqueryMsgModule.php',
        'ResourceLoaderLanguageDataModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderLanguageDataModule.php',
        'ResourceLoaderLanguageNamesModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderLanguageNamesModule.php',
+       'ResourceLoaderLessVarFileModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderLessVarFileModule.php',
        'ResourceLoaderMediaWikiUtilModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderMediaWikiUtilModule.php',
        'ResourceLoaderModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderModule.php',
        'ResourceLoaderOOUIFileModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderOOUIFileModule.php',
diff --git a/includes/resourceloader/ResourceLoaderLessVarFileModule.php b/includes/resourceloader/ResourceLoaderLessVarFileModule.php
new file mode 100644 (file)
index 0000000..17d00e0
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * Subclass with context specific LESS variables
+ */
+class ResourceLoaderLessVarFileModule extends ResourceLoaderFileModule {
+       protected $lessVariables = [
+               'collapsible-collapse',
+               'collapsible-expand',
+       ];
+
+       /**
+        * @inheritDoc
+        */
+       public function getMessages() {
+               // Overload so MessageBlobStore can detect updates to messages and purge as needed.
+               return array_merge( $this->messages, $this->lessVariables );
+       }
+
+       /**
+        * Exclude a set of messages from a JSON string representation
+        * @param string $blob
+        * @param array $exclusions
+        * @return array $blob
+        */
+       protected function excludeMessagesFromBlob( $blob, $exclusions ) {
+               $data = json_decode( $blob, true );
+               // unset the LESS variables so that they are not forwarded to JavaScript
+               foreach ( $exclusions as $key ) {
+                       unset( $data[$key] );
+               }
+               return $data;
+       }
+
+       /**
+        * @inheritDoc
+        */
+       protected function getMessageBlob( ResourceLoaderContext $context ) {
+               $blob = parent::getMessageBlob( $context );
+               return json_encode( $this->excludeMessagesFromBlob( $blob, $this->lessVariables ) );
+       }
+
+       /**
+        * Takes a message and wraps it in quotes for compatibility with LESS parser
+        * (ModifyVars) method so that the variable can be loaded and made available to stylesheets.
+        * Note this does not take care of CSS escaping. That will be taken care of as part
+        * of CSS Janus.
+        * @param string $msg
+        * @return string wrapped LESS variable definition
+        */
+       private static function wrapAndEscapeMessage( $msg ) {
+               return str_replace( "'", "\'", CSSMin::serializeStringValue( $msg ) );
+       }
+
+       /**
+        * @param \ResourceLoaderContext $context
+        * @return array LESS variables
+        */
+       protected function getLessVars( \ResourceLoaderContext $context ) {
+               $blob = parent::getMessageBlob( $context );
+               $lessMessages = $this->excludeMessagesFromBlob( $blob, $this->messages );
+
+               $vars = [];
+               foreach ( $lessMessages as $msgKey => $value ) {
+                       $vars['msg-' . $msgKey] = self::wrapAndEscapeMessage( $value );
+               }
+               return $vars;
+       }
+}
diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderLessVarFileModuleTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderLessVarFileModuleTest.php
new file mode 100644 (file)
index 0000000..a42e4be
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * @group ResourceLoader
+ */
+class ResourceLoaderLessVarFileModuleTest extends ResourceLoaderTestCase {
+
+       public static function providerWrapAndEscapeMessage() {
+               return [
+                       [
+                               "Foo", '"Foo"',
+                       ],
+                       [
+                               "Foo bananas", '"Foo bananas"',
+                       ],
+                       [
+                               "Who's that test? Who's that test? It's Jess!",
+                               '"Who\\\'s that test? Who\\\'s that test? It\\\'s Jess!"',
+                       ],
+                       [
+                               'Hello "he" said',
+                               '"Hello \"he\" said"',
+                       ],
+                       [
+                               'boo";-o-link:javascript:alert(1);color:red;content:"',
+                               '"boo\";-o-link:javascript:alert(1);color:red;content:\""',
+                       ],
+                       [
+                               '"jon\'s"',
+                               '"\"jon\\\'s\""'
+                       ]
+               ];
+       }
+       /**
+        * @dataProvider providerWrapAndEscapeMessage
+        * @covers ResourceLoaderLessVarFileModule::wrapAndEscapeMessage
+        */
+       public function testEscapeMessage( $msg, $expected ) {
+               $method = new ReflectionMethod( ResourceLoaderLessVarFileModule::class, 'wrapAndEscapeMessage' );
+               $method->setAccessible( true );
+               $this->assertEquals( $expected, $method->invoke( ResourceLoaderLessVarFileModule::class, $msg ) );
+       }
+}