Reviewers: felix8a,

Description:
This CL:

1. Adds support to sanitizeStylesheet for @keyframes rules
2. Adds a CSS property bit for the <global-name> CSS value type
3. Adds entries to the CSS schemas for animation and animation-*
properties
4. Adds partial support for <global-nam>s to sanitizeCssProperty
5. Adds tests for well-formed and malformed @keyframes rules

Still TODO: thread the ID suffix to sanitizeCssProperty for use in
mangling global names.  Possibly pass the same virtualization bundle
to sanitizeCssProperty as is passed to other sanitizeCss* functions.

Please review this at https://codereview.appspot.com/11832043/
Index: src/com/google/caja/lang/css/CssPropBit.java
===================================================================
--- src/com/google/caja/lang/css/CssPropBit.java        (revision 5505)
+++ src/com/google/caja/lang/css/CssPropBit.java        (working copy)
@@ -46,6 +46,10 @@
* Allowed to be specified in a history-sensitive manner in a CSS stylesheet.
    */
   ALLOWED_IN_LINK(256),
+  /**
+   * Non-keyword terms treated as global names that need to be namespaced.
+   */
+  GLOBAL_NAME(512),
   ;

   /** a single bit. */
Index: src/com/google/caja/lang/css/CssPropertyPatterns.java
===================================================================
--- src/com/google/caja/lang/css/CssPropertyPatterns.java       (revision 5505)
+++ src/com/google/caja/lang/css/CssPropertyPatterns.java       (working copy)
@@ -284,17 +284,18 @@

   private static final Map<String, CssPropBit> BUILTIN_PROP_BITS
       = Maps.<String, CssPropBit>immutableMap()
-        .put("number", CssPropBit.QUANTITY)
-        .put("percentage", CssPropBit.QUANTITY)
         .put("angle", CssPropBit.QUANTITY)
         .put("frequency", CssPropBit.QUANTITY)
+        .put("global-name", CssPropBit.GLOBAL_NAME)
+        .put("hex-color", CssPropBit.HASH_VALUE)
+        .put("integer", CssPropBit.QUANTITY)
         .put("length", CssPropBit.QUANTITY)
-        .put("time", CssPropBit.QUANTITY)
-        .put("integer", CssPropBit.QUANTITY)
-        .put("hex-color", CssPropBit.HASH_VALUE)
+        .put("number", CssPropBit.QUANTITY)
+        .put("percentage", CssPropBit.QUANTITY)
+        .put("quotable-word", CssPropBit.UNRESERVED_WORD)
         .put("specific-voice", CssPropBit.QSTRING)
         .put("string", CssPropBit.QSTRING)
-        .put("quotable-word", CssPropBit.UNRESERVED_WORD)
+        .put("time", CssPropBit.QUANTITY)
         .put("unicode-range", CssPropBit.UNICODE_RANGE)
         .put("unreserved-word", CssPropBit.UNRESERVED_WORD)
         .put("uri", CssPropBit.URL)
Index: src/com/google/caja/lang/css/css-extensions-defs.json
===================================================================
--- src/com/google/caja/lang/css/css-extensions-defs.json       (revision 5505)
+++ src/com/google/caja/lang/css/css-extensions-defs.json       (working copy)
@@ -8,6 +8,86 @@

   "types": [

+    { "key": "animation",
+      "signature": "<single-animation> [, <single-animation>]*",
+      "appliesTo": "*",
+      "inherited": false,
+      "mediaGroups": ["visual"],
+      "source": "http://dev.w3.org/csswg/css-animations/#animation";
+    },
+
+    { "key": "animation-delay",
+      "signature": "<time> [, <time>]*",
+      "initial": "0s",
+      "appliesTo": "*",
+      "inherited": false,
+      "mediaGroups": ["visual"],
+      "source": "http://dev.w3.org/csswg/css-animations/#animation-delay";
+    },
+
+    { "key": "animation-direction",
+ "signature": "<single-animation-direction> [, <single-animation-direction>]*",
+      "initial": "normal",
+      "appliesTo": "*",
+      "inherited": false,
+      "mediaGroups": ["visual"],
+      "source": "http://dev.w3.org/csswg/css-animations/#animation-direction";
+    },
+
+    { "key": "animation-duration",
+      "signature": "<time> [, <time>]*",
+      "initial": "0s",
+      "appliesTo": "*",
+      "inherited": false,
+      "mediaGroups": ["visual"],
+      "source": "http://dev.w3.org/csswg/css-animations/#animation-duration";
+    },
+
+    { "key": "animation-fill-mode",
+ "signature": "<single-animation-fill-mode> [, <single-animation-fill-mode>]*",
+      "initial": "none",
+      "appliesTo": "*",
+      "inherited": false,
+      "mediaGroups": ["visual"],
+      "source": "http://dev.w3.org/csswg/css-animations/#animation-fill-mode";
+    },
+
+    { "key": "animation-iteration-count",
+ "signature": "<single-animation-iteration-count> [, <single-animation-iteration-count>]*",
+      "initial": "1",
+      "appliesTo": "*",
+      "inherited": false,
+      "mediaGroups": ["visual"],
+      "source": 
"http://dev.w3.org/csswg/css-animations/#animation-iteration-count";
+    },
+
+    { "key": "animation-name",
+      "signature": "<single-animation-name> [, <single-animation-name>]*",
+      "initial": "none",
+      "appliesTo": "*",
+      "inherited": false,
+      "mediaGroups": ["visual"],
+      "source": "http://dev.w3.org/csswg/css-animations/#animation-name";
+    },
+
+    { "key": "animation-play-state",
+ "signature": "<single-animation-play-state> [, <single-animation-play-state>]*",
+      "initial": "running",
+      "appliesTo": "*",
+      "inherited": false,
+      "mediaGroups": ["visual"],
+      "source": "http://dev.w3.org/csswg/css-animations/#animation-play-state";
+    },
+
+    { "key": "animation-timing-function",
+ "signature": "<single-timing-function> [, <single-timing-function>]*",
+      "initial": "ease",
+      "appliesTo": "*",
+      "inherited": false,
+      "mediaGroups": ["visual"],
+      "source": 
"http://dev.w3.org/csswg/css-animations/#animation-timing-function";
+    },
+
     { "key": "background",
"signature": "[ [ <bg-image> || [<bg-position> [/ <bg-size>]? | / <bg-size>] || <repeat-style> || <attachment> || <bg-origin> ] , ]* ['background-color' || <bg-image> || [<bg-position> [/ <bg-size>]? | / <bg-size>] || <repeat-style> || <attachment> || <bg-origin>]",
       "appliesTo": "*",
@@ -526,5 +606,46 @@
       "source": "http://dev.w3.org/csswg/css-backgrounds/#position";,
     },

+    { "key": "<single-animation>",
+ "signature": "<single-animation-name> || <time> || <single-timing-function> || <time> || <single-animation-iteration-count> || <single-animation-direction> || <single-animation-fill-mode> || <single-animation-play-state>",
+      "source": "http://dev.w3.org/csswg/css-animations/#single-animation";
+    },
+
+    { "key": "<single-animation-direction>",
+      "default": "normal",
+      "signature": "normal | reverse | alternate | alternate-reverse",
+      "source": "http://dev.w3.org/csswg/css-animations/#animation-direction";
+    },
+
+    { "key": "<single-animation-fill-mode>",
+      "default": "none",
+      "signature": "none | forwards | backwards | both",
+      "source": "http://dev.w3.org/csswg/css-animations/#animation-fill-mode";
+    },
+
+    { "key": "<single-animation-iteration-count>",
+      "default": "1",
+      "signature": "infinite | <number>",
+      "source": 
"http://dev.w3.org/csswg/css-animations/#single-animation-iteration-count";
+    },
+
+    { "key": "<single-animation-name>",
+      "default": "none",
+      "signature": "none | <global-name>",
+      "source": "http://dev.w3.org/csswg/css-animations/#single-animation-name";
+    },
+
+    { "key": "<single-animation-play-state>",
+      "default": "running",
+      "signature": "running | paused",
+      "source": 
"http://dev.w3.org/csswg/css-animations/#single-animation-play-state";
+    },
+
+    { "key": "<single-timing-function>",
+ "signature": "ease | linear | ease-in | ease-out | ease-in-out | step-start | step-end | steps(<integer>[, [ start | end ] ]?) | cubic-bezier(<number>, <number>, <number>, <number>)",
+      "default": "ease",
+      "source": 
"http://dev.w3.org/csswg/css-transitions/#transition-timing-function";
+    }
+
   ]
 }
Index: src/com/google/caja/lang/css/css-extensions-whitelist.json
===================================================================
--- src/com/google/caja/lang/css/css-extensions-whitelist.json (revision 5505) +++ src/com/google/caja/lang/css/css-extensions-whitelist.json (working copy)
@@ -4,6 +4,9 @@
   "inherits": ["css21-whitelist.json"],

   "allowed": [
+    "animation", "animation-delay", "animation-direction", 
"animation-duration",
+    "animation-fill-mode", "animation-iteration-count", "animation-name",
+    "animation-play-state", "animation-timing-function",
     "border-bottom-left-radius", "border-bottom-right-radius", "border-radius",
     "border-top-left-radius", "border-top-right-radius",
     "box-shadow",
Index: src/com/google/caja/plugin/sanitizecss.js
===================================================================
--- src/com/google/caja/plugin/sanitizecss.js   (revision 5505)
+++ src/com/google/caja/plugin/sanitizecss.js   (working copy)
@@ -21,6 +21,7 @@
  * @author [email protected]
  * \@requires CSS_PROP_BIT_ALLOWED_IN_LINK
  * \@requires CSS_PROP_BIT_HASH_VALUE
+ * \@requires CSS_PROP_BIT_GLOBAL_NAME
  * \@requires CSS_PROP_BIT_NEGATIVE_QUANTITY
  * \@requires CSS_PROP_BIT_QUANTITY
  * \@requires CSS_PROP_BIT_QSTRING
@@ -200,6 +201,7 @@
         var token = tokens[i].toLowerCase();
         var cc = token.charCodeAt(0), cc1, cc2, isnum1, isnum2, end;
         var litGroup, litMap;
+
         token = (

           // Strip out spaces.  Normally cssparser.js dumps these, but we
@@ -289,6 +291,10 @@
           : (token.charAt(token.length-1) === '(')
           ? sanitizeFunctionCall(tokens, i)

+          : ((propBits & CSS_PROP_BIT_GLOBAL_NAME)
+             && /^-?[a-z_][\w\-]*$/.test(token) && !/__$/.test(token))
+          ? token + '-suffix'
+
           : (/^\w+$/.test(token)
              && stringDisposition === CSS_PROP_BIT_UNRESERVED_WORD
              && (propBits & CSS_PROP_BIT_QSTRING))
@@ -800,6 +806,15 @@
                 atIdent = null;
               } else if (atIdent === '@media') {
safeCss.push('@media', ' ', sanitizeMediaQuery(headerArray));
+              } else if (atIdent === '@keyframes') {
+                var animationId = headerArray[0];
+                if (headerArray.length === 1
+                    && !/__$|[^#0-9A-Za-z:_\-]/.test(animationId)) {
+                  safeCss.push(
+ '@keyframes ', animationId + virtualization.idSuffix);
+                } else {
+                  atIdent = null;
+                }
               } else {
                 if (atIdent === '@import' && headerArray.length > 0) {
                   if ('function' === typeof continuation) {
@@ -830,7 +845,7 @@
               blockStack.push(atIdent);
             },
             endAtrule: function () {
-              var atIdent = blockStack.pop();
+              blockStack.pop();
               if (!elide) {
                 safeCss.push(';');
               }
@@ -854,23 +869,35 @@
               var historySensitiveSelectors = void 0;
               var removeHistoryInsensitiveSelectors = false;
               if (!elide) {
-                var selectors = sanitizeCssSelectors(selectorArray,
-                    virtualization);
-                var historyInsensitiveSelectors = selectors[0];
-                historySensitiveSelectors = selectors[1];
-                if (!historyInsensitiveSelectors.length
-                    && !historySensitiveSelectors.length) {
-                  elide = true;
+                var selector = void 0;
+                if (blockStack[blockStack.length - 1] === '@keyframes') {
+                  // Allow [from | to | <percentage>]
+                  selector = selectorArray.join(' ')
+                    .match(/^ *(?:from|to|\d+(?:\.\d+)?%) *$/i);
+                  elide = !selector;
+                  historySensitiveSelectors = [];
+ if (selector) { selector = selector[0].replace(/ +/g, ''); }
                 } else {
-                  var selector = historyInsensitiveSelectors.join(', ');
-                  if (!selector) {
-                    // If we have only history sensitive selectors,
- // use an impossible rule so that we can capture the content
-                    // for later processing by
-                    // history insenstive content for use below.
-                    selector = 'head > html';
-                    removeHistoryInsensitiveSelectors = true;
+                  var selectors = sanitizeCssSelectors(
+                      selectorArray, virtualization);
+                  var historyInsensitiveSelectors = selectors[0];
+                  historySensitiveSelectors = selectors[1];
+                  if (!historyInsensitiveSelectors.length
+                      && !historySensitiveSelectors.length) {
+                    elide = true;
+                  } else {
+                    selector = historyInsensitiveSelectors.join(', ');
+                    if (!selector) {
+ // If we have only history sensitive selectors, use an + // impossible rule so that we can capture the content for + // later processing by history insenstive content for use
+                      // below.
+                      selector = 'head > html';
+                      removeHistoryInsensitiveSelectors = true;
+                    }
                   }
+                }
+                if (!elide) {
                   safeCss.push(selector, '{');
                 }
               }
@@ -898,7 +925,7 @@
               var propertiesEnd = safeCss.length;
               if (!elide) {
                 safeCss.push('}');
-                if (rules) {
+                if ('object' === typeof rules) {
                   var extraSelectors = rules.historySensitiveSelectors;
                   if (extraSelectors.length) {
                     var propertyGroupTokens =
@@ -910,8 +937,8 @@
               }
               if (rules && rules.removeHistoryInsensitiveSelectors) {
                 safeCss.splice(
-                  // -1 and +1 account for curly braces.
-                  rules.endOfSelectors - 1, propertiesEnd + 1);
+                    // -1 and +1 account for curly braces.
+                    rules.endOfSelectors - 1, propertiesEnd + 1);
               }
               checkElide();
             },
@@ -936,9 +963,7 @@
             }
           });
       function checkElide() {
-        elide = blockStack.length !== 0
-            && blockStack[blockStack.length-1] !== null
-            && blockStack[blockStack.length-1][0] !== '@';
+ elide = blockStack.length && blockStack[blockStack.length-1] === null;
       }
       return {
         result : safeCss.join(''),
Index: tests/com/google/caja/plugin/sanitizecss_test.js
===================================================================
--- tests/com/google/caja/plugin/sanitizecss_test.js    (revision 5505)
+++ tests/com/google/caja/plugin/sanitizecss_test.js    (working copy)
@@ -34,6 +34,17 @@
   assertEquals(expected, tokens.join(' '));
 }

+function assertSanitizedStylesheet(golden, input) {
+  var selectors = sanitizeStylesheet(
+      'http://example.com/baseurl', input,
+      {
+        containerClass: 'scopeClass',
+        idSuffix: '-suffix',
+        tagPolicy: function (elName, attrs) { return []; }
+      });
+  assertArrayEquals(input, golden, selectors);
+}
+
 jsunitRegister('testFontFamily',
                function testFontFamily() {
   var tokens = ['Arial', ' ', 'Black', ',', 'monospace', ',',
@@ -365,17 +376,6 @@
 });

 jsunitRegister('testImportant', function testImportant() {
-  function assertSanitizedStylesheet(golden, input) {
-    var selectors = sanitizeStylesheet(
-        'http://example.com/baseurl', input,
-        {
-          containerClass: 'scopeClass',
-          idSuffix: '-suffix',
-          tagPolicy: function (elName, attrs) { return []; }
-        });
-    assertArrayEquals(input, golden, selectors);
-  }
-
   assertSanitizedStylesheet(
       ''
       + '.scopeClass p{color:red !important;}'
@@ -495,3 +495,72 @@
   }
   jsunit.pass();
 });
+
+jsunitRegister('testKeyframes', function testKeyframes() {
+  // Mixture of example 1 and example 2 from
+  // http://dev.w3.org/csswg/css-animations/
+  var input = [
+    'div {',
+    '  animation-name: diagonal-slide;',
+    '  animation-duration: 5s;',
+    '  animation-iteration-count: 10;',
+    '}',
+    '',
+    '@keyframes diagonal-slide {',
+    '',
+    '  from {',
+    '    left: 0;',
+    '    top: 0;',
+    '  }',
+    '',
+    '  50% {',
+    '    left: 55px;',
+    '  }',
+    '',
+    '  to {',
+    '    left: 100px;',
+    '    top: 100px;',
+    '  }',
+    '',
+    '}'].join('\n');
+
+  assertSanitizedStylesheet(
+      ''
+      + '.scopeClass div{'
+      + 'animation-name:diagonal-slide-suffix;'
+      + 'animation-duration:5s;'
+      + 'animation-iteration-count:10;'
+      + '}'
+      + '@keyframes diagonal-slide-suffix{'
+      + 'from{left:0;top:0;}'
+      + '50%{left:55px;}'
+      + 'to{left:100px;top:100px;}'
+      + '}',
+      input);
+
+  // "Rules" in @keyframes must match from/to/<percentage>
+  assertSanitizedStylesheet(
+      '@keyframes foo-suffix{}',
+      ''
+      + '@keyframes foo {'
+      + '  whence { left: 0; top: 100px }'
+      + '  .foo { left: 100px; top: 0 }'
+      + '  a[href] { right: 50px }'
+      + '}');
+
+  // Drop @keyframes with bad IDs.
+  assertSanitizedStylesheet(
+      ''
+      + '.scopeClass b{color:blue;}'
+      + '.scopeClass p{color:pink;}',
+
+      ''
+      + '@keyframes foo__  { from { left: 0 } }'
+      + '@keyframes foo_ _ { from { left: 0 } }'
+      + 'b { color: blue }'
+      + '@keyframes "foo"  { from { left: 0 } }'
+      + '@keyframes        { from { left: 0 } }'
+      + 'p { color: pink }');
+
+  jsunit.pass();
+});


--

--- You received this message because you are subscribed to the Google Groups "Google Caja Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
For more options, visit https://groups.google.com/groups/opt_out.


Reply via email to