Revision: 5503
Author: [email protected]
Date: Wed Jul 24 11:44:50 2013
Log: Added sanitizeMediaQuery to sanitizecss.js
https://codereview.appspot.com/11756043
Implemented a function to verify that the [media-query] production in
HTML: <link rel="stylesheet" media="[media-query]">
CSS: @import "[url]" [media-query] ;
CSS: @media [media-query] { [stylesheet-element] }
JS: window.matchMedia("[media-query]")
is valid.
This CL handles the @media case above, but does not yet handle the other
cases.
Follow-on CLs will handle media-queries in @import by passing the media
query to the continuation and implementing support in the ES5 parser.
[email protected]
http://code.google.com/p/google-caja/source/detail?r=5503
Modified:
/trunk/src/com/google/caja/plugin/sanitizecss.js
/trunk/tests/com/google/caja/plugin/sanitizecss_test.js
=======================================
--- /trunk/src/com/google/caja/plugin/sanitizecss.js Wed Jul 24 11:20:09
2013
+++ /trunk/src/com/google/caja/plugin/sanitizecss.js Wed Jul 24 11:44:50
2013
@@ -36,12 +36,14 @@
* \@provides sanitizeCssSelectors
* \@provides sanitizeStylesheet
* \@provides sanitizeStylesheetWithExternals
+ * \@provides sanitizeMediaQuery
*/
var sanitizeCssProperty = undefined;
var sanitizeCssSelectors = undefined;
var sanitizeStylesheet = undefined;
var sanitizeStylesheetWithExternals = undefined;
+var sanitizeMediaQuery = undefined;
(function () {
var NOEFFECT_URL = 'url("about:blank")';
@@ -603,18 +605,99 @@
};
(function () {
- var allowed = {};
- var cssMediaTypeWhitelist = {
- 'braille': allowed,
- 'embossed': allowed,
- 'handheld': allowed,
- 'print': allowed,
- 'projection': allowed,
- 'screen': allowed,
- 'speech': allowed,
- 'tty': allowed,
- 'tv': allowed
+ var MEDIA_TYPE =
+ '(?:'
+ + 'all|aural|braille|embossed|handheld|print'
+ + '|projection|screen|speech|tty|tv'
+ + ')';
+
+ // A white-list of media features extracted from the "Pseudo-BNF" in
+ // http://dev.w3.org/csswg/mediaqueries4/#media1 and
+ // https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Media_queries
+ var MEDIA_FEATURE =
+ '(?:'
+ + '(?:min-|max-)?'
+ + '(?:' + (
+ '(?:device-)?'
+ + '(?:aspect-ratio|height|width)'
+ + '|color(?:-index)?'
+ + '|monochrome'
+ + '|orientation'
+ + '|resolution'
+ )
+ + ')'
+ + '|grid'
+ + '|hover'
+ + '|luminosity'
+ + '|pointer'
+ + '|scan'
+ + '|script'
+ + ')';
+
+ var LENGTH_UNIT = '(?:p[cxt]|[cem]m|in|dpi|dppx|dpcm|%)';
+
+ var CSS_VALUE =
+ '-?(?:'
+ + '[a-z]\\w+(?:-\\w+)*' // An identifier
+ // A length or scalar quantity, or a rational number.
+ // dev.w3.org/csswg/mediaqueries4/#values introduces a ratio
value-type
+ // to allow matching aspect ratios like "4 / 3".
+ + '|\\d+(?: / \\d+|(?:\\.\\d+)?' + LENGTH_UNIT + '?)'
+ + ')';
+
+ var MEDIA_EXPR =
+ '\\( ' + MEDIA_FEATURE + ' (?:' + ': ' + CSS_VALUE + ' )?\\)';
+
+ var MEDIA_QUERY =
+ '(?:'
+ + '(?:(?:(?:only|not) )?' + MEDIA_TYPE + '|' + MEDIA_EXPR + ')'
+ // We use 'and ?' since 'and(' is a single CSS function token while
+ // 'and (' parses to two separate tokens -- IDENT "and", DELIM "(".
+ + '(?: and ?' + MEDIA_EXPR + ')*'
+ + ')';
+
+ var STARTS_WITH_KEYWORD_REGEXP = /^\w/;
+
+ var MEDIA_QUERY_LIST_REGEXP = new RegExp(
+ '^' + MEDIA_QUERY + '(?: , ' + MEDIA_QUERY + ')*' + '$',
+ 'i'
+ );
+
+ /**
+ * Sanitizes a media query as defined in
+ * http://dev.w3.org/csswg/mediaqueries4/#syntax
+ * <blockquote>
+ * Media Queries allow authors to adapt the style applied to a document
+ * based on the environment the document is being rendered in.
+ * </blockquote>
+ *
+ * @param {Array.<string>} cssTokens an array of tokens of the kind
produced
+ * by cssLexers.
+ * @return {string} a CSS media query. This may be the empty string,
or if
+ * the input is invalid, then a query that is always false.
+ */
+ sanitizeMediaQuery = function (cssTokens) {
+ cssTokens = cssTokens.slice();
+ // Strip out space tokens.
+ var nTokens = cssTokens.length, k = 0;
+ for (var i = 0; i < nTokens; ++i) {
+ var tok = cssTokens[i];
+ if (tok != ' ') { cssTokens[k++] = tok; }
+ }
+ cssTokens.length = k;
+ var css = cssTokens.join(' ');
+ css = (
+ !css.length ? '' // Always true per the spec.
+ : !(MEDIA_QUERY_LIST_REGEXP.test(css)) ? 'not all' // Always
false.
+ // Emit as-is if it starts with 'only', 'not' or a media type.
+ : STARTS_WITH_KEYWORD_REGEXP.test(css) ? css
+ : 'not all , ' + css // Not ambiguous with a URL.
+ );
+ return css;
};
+ }());
+
+ (function () {
/**
* Given a series of sanitized tokens, removes any properties that
would
@@ -716,19 +799,12 @@
if (elide) {
atIdent = null;
} else if (atIdent === '@media') {
- headerArray = headerArray.filter(
- function (mediaType) {
- return cssMediaTypeWhitelist[mediaType] == allowed;
- });
- if (headerArray.length) {
- safeCss.push(atIdent, ' ', headerArray.join(','));
- } else {
- atIdent = null;
- }
+ safeCss.push('@media', ' ',
sanitizeMediaQuery(headerArray));
} else {
if (atIdent === '@import' && headerArray.length > 0) {
if ('function' === typeof continuation) {
moreToCome = true;
+ // TODO: provide any media query to the continuation.
var cssUrl = safeUri(
resolveUri(baseUri, cssParseUri(headerArray[0])),
function(result) {
@@ -892,4 +968,5 @@
window['sanitizeCssProperty'] = sanitizeCssProperty;
window['sanitizeCssSelectors'] = sanitizeCssSelectors;
window['sanitizeStylesheet'] = sanitizeStylesheet;
+ window['sanitizeMediaQuery'] = sanitizeMediaQuery;
}
=======================================
--- /trunk/tests/com/google/caja/plugin/sanitizecss_test.js Wed Jul 24
11:20:09 2013
+++ /trunk/tests/com/google/caja/plugin/sanitizecss_test.js Wed Jul 24
11:44:50 2013
@@ -396,3 +396,102 @@
assertSelector("a[", "sfx", [[], []]);
jsunit.pass();
});
+
+jsunitRegister('testMediaQueries', function testMediaQueries() {
+ var passing = [
+ '',
+ ' ',
+ 'all',
+ ' all',
+ 'all ',
+ ' all ',
+ 'not all',
+ 'all and(color)',
+ 'screen and (max-width: 6in) and (color), (color)',
+ '(min-orientation:portrait)',
+ '(min-width: 700px)',
+ '(min-width: 700px) and (orientation: landscape)',
+ 'tv and (min-width: 700px) and (orientation: landscape)',
+ '(min-width: 700px), handheld and (orientation: landscape)',
+ 'not all and (monochrome)',
+ 'not screen and (color), print and (color)',
+ 'only screen and (color)',
+ 'all and (color)',
+ 'all and (min-color: 4)',
+ 'all and ( min-color : 4 )',
+ 'all and (color-index)',
+ 'all and (min-color-index: 256)',
+ 'screen and (min-aspect-ratio: 1/1)',
+ 'screen and (device-aspect-ratio: 16/9), screen and
(device-aspect-ratio: 16/10)',
+ 'screen and (max-device-width: 799px)',
+ 'handheld and (grid) and (max-width: 15em)',
+ 'all and (monochrome)',
+ 'all and (min-monochrome: 8)',
+ 'all and (orientation: portrait)',
+ 'print and (min-resolution: 300dpi)',
+ 'screen and (min-resolution: 2dppx)',
+ 'tv and (scan: progressive)',
+ 'handheld and (min-width: 20em), screen and (min-width: 20em)',
+ 'print and (min-width: 8.5in)',
+ 'screen and (min-width: 500px) and (max-width: 800px)',
+ '(min-resolution: 192dpi)',
+ 'print and (min-width: 25cm)',
+ 'screen and (min-width: 400px) and (max-width: 700px)',
+ 'handheld and (min-width: 20em), \n screen and (min-width: 20em)',
+ 'screen and (device-width: 800px)',
+ 'screen and (device-height: 600px)',
+ 'all and (orientation:landscape)',
+ 'screen and (device-aspect-ratio: 16/9)',
+ 'screen and (device-aspect-ratio: 2560 / 1440)',
+ '(min-color: 1)',
+ '(min-color:2)',
+ 'all and (color-index)',
+ 'all and (min-color-index: 1)',
+ 'all and (min-color-index: 256)',
+ '(monochrome)',
+ '(min-monochrome: 1)',
+ 'print and (monochrome)',
+ 'print and (min-resolution: 118dpcm)',
+ 'handheld and (grid) and (max-device-height: 7em)',
+ '(script) and (pointer: fine)',
+ 'only screen and (pointer) and (hover:1)',
+ 'screen and (luminosity:dim)',
+ '(MONOCHROME)',
+ '(Color:1)',
+ 'Screen AND (Min-device-Width: 600PX)'
+ ];
+ var failing = [
+ 'not all',
+ 'only and or',
+ '(bogus)',
+ '(screen)',
+ 'min-width:800px',
+ 'screen and ((color:1))',
+ 'only (color)',
+ 'only (color:1)',
+ 'screen, tv, ',
+ '(min-device-width:6in',
+ '(min-device-width:',
+ '(min-device-width'
+ ];
+ function normalizeQuery(css) {
+ if (/\S/.test(css) && !/^\s*\w/.test(css)) { css = 'not all , ' + css;
}
+ css = css.replace(/\s+/g, ' ');
+ return css.replace(/(^|[^\w\-]) /g, '$1') // Strip spaces after
punctuation.
+ .replace(/ (?=$|[^\w\-])/g, '') // Strip spaces before
punctuation.
+ .toLowerCase();
+ }
+ for (var i = 0, n = passing.length; i < n; ++i) {
+ var css = passing[i];
+ var tokens = lexCss(css);
+ var sanitized = sanitizeMediaQuery(tokens);
+ assertEquals(css, normalizeQuery(css), normalizeQuery(sanitized));
+ }
+ for (var i = 0, n = failing.length; i < n; ++i) {
+ var css = failing[i];
+ var tokens = lexCss(css);
+ var sanitized = sanitizeMediaQuery(tokens);
+ assertEquals(css, 'not all', normalizeQuery(sanitized));
+ }
+ 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.