/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */


/**
 * @fileoverview A sanitizer for CSS property values. It is intended
 * to be used on the result of {@code CSSStyleDeclaration.getPropertyValue},
 * which has already been parsed and validated by the browser out of stylesheets
 * and inline style attributes. At the moment, it's only purpose is to detect
 * CSS functions to apply a whitelist and support rewriting of URLs.
 * @package
 */

goog.module('goog.html.sanitizer.CssPropertySanitizer');
goog.module.declareLegacyNamespace();

var SafeUrl = goog.require('goog.html.SafeUrl');
var googAsserts = goog.require('goog.asserts');
var googObject = goog.require('goog.object');
var googString = goog.require('goog.string');


/**
 * Allowed CSS functions
 * @const {!Object<string,boolean>}
 */
var ALLOWED_FUNCTIONS = googObject.createSet(
    'rgb', 'rgba', 'alpha', 'rect', 'image', 'linear-gradient',
    'radial-gradient', 'repeating-linear-gradient', 'repeating-radial-gradient',
    'cubic-bezier', 'matrix', 'perspective', 'rotate', 'rotate3d', 'rotatex',
    'rotatey', 'steps', 'rotatez', 'scale', 'scale3d', 'scalex', 'scaley',
    'scalez', 'skew', 'skewx', 'skewy', 'translate', 'translate3d',
    'translatex', 'translatey', 'translatez');

/**
 * The set of characters that need to be normalized inside url("...").
 * We normalize newlines because they are not allowed inside quoted strings,
 * normalize quote characters, angle-brackets, and asterisks because they
 * could be used to break out of the URL or introduce targets for CSS
 * error recovery.  We normalize parentheses since they delimit unquoted
 * URLs and calls and could be a target for error recovery.
 * @const {!RegExp}
 */
var NORM_URL_REGEXP = /[\n\f\r\"\'()*<>]/g;

/**
 * The replacements for NORM_URL_REGEXP.
 * @const {!Object<string, string>}
 */
var NORM_URL_REPLACEMENTS = {
  '\n': '%0a',
  '\f': '%0c',
  '\r': '%0d',
  '"': '%22',
  '\'': '%27',
  '(': '%28',
  ')': '%29',
  '*': '%2a',
  '<': '%3c',
  '>': '%3e'
};

/**
 * Normalizes a character for use in a url() directive.
 * @param {string} ch Character to be normalized.
 * @return {string} Normalized character.
 */
function normalizeUrlChar(ch) {
  return googAsserts.assert(NORM_URL_REPLACEMENTS[ch]);
}

/**
 * Constructs a safe URI from a given URI and prop using a given uriRewriter
 * function.
 * @param {string} uri URI to be sanitized.
 * @param {string} propName Property name which contained the URI.
 * @param {?function(string, string):?SafeUrl} uriRewriter A URI rewriter that
 *     returns a {@link SafeUrl}.
 * @return {?string} Safe URI for use in CSS.
 */
function getSafeUri(uri, propName, uriRewriter) {
  if (!uriRewriter) {
    return null;
  }
  var safeUri = uriRewriter(uri, propName);
  if (safeUri && SafeUrl.unwrap(safeUri) != SafeUrl.INNOCUOUS_STRING) {
    return 'url("' +
        SafeUrl.unwrap(safeUri).replace(NORM_URL_REGEXP, normalizeUrlChar) +
        '")';
  }
  return null;
}

/**
 * Sanitizes the value for a given a browser-parsed CSS value.
 * @param {string} propName A property name.
 * @param {string} propValue Value of the property as parsed by the browser.
 * @param {function(string, string):?SafeUrl=} opt_uriRewriter A URI
 *     rewriter that returns an unwrapped goog.html.SafeUrl.
 * @return {?string} Sanitized property value or null if the property should be
 *     rejected altogether.
 */
exports.sanitizeProperty = function(propName, propValue, opt_uriRewriter) {
  propValue = googString.trim(propValue);
  if (propValue == '') {
    return null;
  }

  if (googString.caseInsensitiveStartsWith(propValue, 'url(')) {
    // Urls can only appear as the only function call in the property value, and
    // are rewritten according to the policy implemented in opt_uriRewriter.
    if (!propValue.endsWith(')') || googString.countOf(propValue, '(') > 1 ||
        googString.countOf(propValue, ')') > 1) {
      // This is a little stricter than it needs to be (e.g. it will refuse
      // url("http://foo.com/a(b"), but it's better to err on the side of
      // caution (even though getSafeUri is guaranteed to yield a single,
      // SafeHtml-compliant url(...) value).
      return null;
    }
    // TODO(pelizzi): use HtmlSanitizerUrlPolicy for opt_uriRewriter.
    if (!opt_uriRewriter) {
      return null;
    }
    // TODO(danesh): Check if we need to resolve this URI.
    var uri = googString.stripQuotes(
        propValue.substring(4, propValue.length - 1), '"\'');

    return getSafeUri(uri, propName, opt_uriRewriter);
  } else if (propValue.indexOf('(') > 0) {
    // Functions are filtered through a whitelist. String arguments (e.g.
    // url("...")) are not supported, because IE/EDGE can feed back malformed
    // output when given malformed input (e.g. url("ab"c")). We would need a
    // full parser to address this.
    if (/"|'/.test(propValue)) {
      return null;
    }
    var regex = /([\-\w]+)\(/g;
    var match;
    while (match = regex.exec(propValue)) {
      if (!(match[1].toLowerCase() in ALLOWED_FUNCTIONS)) {
        return null;
      }
    }
    return propValue;
  } else {
    // Everything else is allowed.
    // TODO(pelizzi): This was kept as-is during refactoring to maintain the
    // existing behavior. In particular we allow 'quotes: "xx" "yy"'. But
    // ideally we should only allow values without quotes and parentheses here.
    return propValue;
  }
};
