Package com.google.caja.plugin

Source Code of com.google.caja.plugin.CssDynamicExpressionRewriter

// Copyright (C) 2008 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.caja.plugin;

import com.google.caja.lexer.FilePosition;
import com.google.caja.lexer.TokenConsumer;
import com.google.caja.parser.AncestorChain;
import com.google.caja.parser.MutableParseTreeNode;
import com.google.caja.parser.ParseTreeNode;
import com.google.caja.parser.Visitor;
import com.google.caja.parser.css.CssTree;
import com.google.caja.parser.js.ArrayConstructor;
import com.google.caja.parser.js.Elision;
import com.google.caja.parser.js.Expression;
import com.google.caja.parser.js.Operation;
import com.google.caja.parser.js.Operator;
import com.google.caja.parser.js.StringLiteral;
import com.google.caja.parser.quasiliteral.QuasiBuilder;
import com.google.caja.render.CssPrettyPrinter;
import com.google.caja.reporting.RenderContext;
import com.google.caja.util.Lists;
import java.util.List;

import javax.annotation.Nullable;
import javax.annotation.OverridingMethodsMustInvokeSuper;

/**
* Compiles CSS style-sheets to JavaScript which outputs the same CSS, but with
* rules only affecting nodes that are children of a class whose name contains
* the gadget id.
*
* @author mikesamuel@gmail.com
*/
public final class CssDynamicExpressionRewriter {

  private final @Nullable String gadgetNameSuffix;

  public CssDynamicExpressionRewriter(PluginMeta meta) {
    this.gadgetNameSuffix = meta.getIdClass();
  }

  /**
   * @param ss modified destructively.
   */
  public void rewriteCss(CssTree ss) {
    // Replace suffixed class and ID literals with expressions that include
    // the actual id class suffix.
    //     '#foo {}'                                        ; The original rule
    // =>  '#foo-' + IMPORTS___.getIdClass___() + ' {}'     ; Cajoled rule
    // =>  '#foo-gadget123___ {}'                           ; In the browser
    //     'p { }'                                          ; The original rule
    // =>  '.' + IMPORTS___.getIdClass___() + '___ p { }'   ; Cajoled rule
    // =>  '.gadget123___ p { }'                            ; In the browser
    rewriteSuffixedIdsAndClasses(ss);

    // Rewrite any UnsafeUriLiterals to JavaScript expressions that are
    // presented to the client-side URI policy when the content is rendered.
    //     'p { background: url(unsafe.png) }'              ; The original rule
    // =>  'p { background: url('                           ; Cajoled rule
    //       + IMPORTS___.rewriteUriInCss___('unsafe.png')  ;
    //       + ') }'                                        ;
    // =>  'p { background: url(safe.png) }'                ; In the browser
    rewriteUnsafeUriLiteralsToExpressions(ss);
  }

  private void rewriteSuffixedIdsAndClasses(CssTree ss) {
    ss.acceptPreOrder(new Visitor() {
      public boolean visit(AncestorChain<?> ancestors) {
        ParseTreeNode node = ancestors.node;
        if (node instanceof CssTree.SuffixedSelectorPart) {
          CssTree.SuffixedSelectorPart ssp
              = (CssTree.SuffixedSelectorPart) node;
          MutableParseTreeNode parent = ancestors.parent.cast(
              MutableParseTreeNode.class)
              .node;
          CssTree replacement;
          if (gadgetNameSuffix == null) {
            replacement = new SuffixedClassOrIdLiteral(
                ssp.getFilePosition(),
                ssp.typePrefix() + ssp.suffixedIdentifier(""));
          } else {
            String ident = ssp.suffixedIdentifier(gadgetNameSuffix);
            replacement = ".".equals(ssp.typePrefix())
                ? new CssTree.ClassLiteral(ssp.getFilePosition(), "." + ident)
                : new CssTree.IdLiteral(ssp.getFilePosition(), "#" + ident);
          }
          parent.replaceChild(replacement, ssp);
          return false;
        } else if (node instanceof CssTree.IdLiteral) {
          // An un-suffixed ID literal has snuck in since CssRewriter.
          throw new AssertionError();
        } else {
          return true;
        }
      }
    }, null);
  }

  private void rewriteUnsafeUriLiteralsToExpressions(CssTree ss) {
    ss.acceptPreOrder(new Visitor() {
          public boolean visit(AncestorChain<?> ancestors) {
            ParseTreeNode node = ancestors.node;
            if (node instanceof UnsafeUriLiteral) {
              UnsafeUriLiteral uul = (UnsafeUriLiteral) node;
              CssTree parent = (CssTree) ancestors.parent.node;
              assert(null != parent);
              AncestorChain<?> prop = ancestors;
              while (null != prop &&
                     !(prop.node instanceof CssTree.PropertyDeclaration)) {
                prop = prop.parent;
              }
              assert(null != prop);
              parent.replaceChild(
                  new JsExpressionUriLiteral(
                      uul.getFilePosition(),
                      (Expression) QuasiBuilder.substV(
                          "IMPORTS___./*@synthetic*/rewriteUriInCss___(@u, @p)",
                          "u", StringLiteral.valueOf(
                                   uul.getFilePosition(),
                                   uul.getValue()),
                          "p", StringLiteral.valueOf(
                                   uul.getFilePosition(),
                                   ((CssTree.PropertyDeclaration) prop.node)
                                       .getProperty().getPropertyName()
                                       .getCanonicalForm()))),
                  uul);
            }
            return true;
          }
        }, null);
  }

  /**
   * Returns an array containing chunks of CSS text that can be joined on a
   * CSS identifier to yield sandboxed CSS.
   * This can be used client side with the {@code emitCss} method defined in
   * "domado.js".
   * @param ss a rewritten stylesheet.
   */
  public static ArrayConstructor cssToJs(CssTree ss) {
    // Render the CSS to a string, split it (effectively) on the
    // GADGET_ID_PLACEHOLDER to get an array of strings, and produce JavaScript
    // which joins it on the actual gadget id which is chosen at runtime.

    // The below will, if GADGET_ID_PLACEHOLDER where "X", given the sequence
    // of calls
    //    call            sb        cssParts
    //    consume("a")    "a"       []
    //    consume("bX")   ""        ["ab"]
    //    consume("cX")   ""        ["ab", "c"]
    //    consume("d")    "d"       ["ab", "c"]
    //    noMoreTokens()  ""        ["ab", "c", "d"]
    // Which has he property that the output list joined with the placeholder
    // produces the concatenation of the original string.
    EmbeddedJsExpressionTokenConsumer cssToJsArrayElements
        = new EmbeddedJsExpressionTokenConsumer();
    ss.render(new RenderContext(cssToJsArrayElements));
    cssToJsArrayElements.noMoreTokens();

    return new ArrayConstructor(
        ss.getFilePosition(), cssToJsArrayElements.getArrayMembers());
  }
}


/**
* An ID literal that should have the gadget ID added as a suffix to prevent
* collision with other IDs.
*/
class SuffixedClassOrIdLiteral extends CssTree.CssLiteral {

  SuffixedClassOrIdLiteral(FilePosition pos, String value) {
    super(pos, value);
  }

  @Override
  public void render(RenderContext r) {
    TokenConsumer tc = r.getOut();
    tc.mark(getFilePosition());
    tc.consume(getValue());
    if (tc instanceof EmbeddedJsExpressionTokenConsumer) {
      ((EmbeddedJsExpressionTokenConsumer) tc).endArrayElement();
    } else {
      // Emit a buster that will cause the CSS parser to error out but not out
      // of the current block.
      // This allows us to debug the output of this stage, but does not
      // compromise security if the CSS is rendered by naive code.
      tc.consume("UNSAFE_UNTRANSLATED_SUFFIX:;");
    }
  }

  @Override
  protected boolean checkValue(String value) {
    return (value.startsWith("#") && value.endsWith("-"))
        || value.startsWith(".");
  }

}


/**
* A Uri literal evaluated by calling a JavaScript Expression at run time.
*/
class JsExpressionUriLiteral extends CssTree.CssLiteral {
  private final Expression expr;

  JsExpressionUriLiteral(FilePosition pos, Expression expr) {
    super(pos, null);
    this.expr = expr;
  }

  @Override
  public boolean makeImmutable() {
    if (!expr.makeImmutable()) { return false; }
    return super.makeImmutable();
  }

  @Override
  public void render(RenderContext r) {
    TokenConsumer tc = r.getOut();
    tc.mark(getFilePosition());
    tc.consume("url(");
    if (tc instanceof EmbeddedJsExpressionTokenConsumer) {
      ((EmbeddedJsExpressionTokenConsumer) tc).consume(expr);
    } else {
      // Emit a buster that will cause the CSS parser to error out but not out
      // of the current block.
      // This allows us to debug the output of this stage, but does not
      // compromise security if the CSS is rendered by naive code.
      tc.consume("UNSAFE_JS_EXPRESSION_LITERAL:;");
    }
    tc.consume(")");
  }

  @Override
  protected boolean checkValue(String value) { return true; }
}


/**
* Consumes CSS tokens and calls to substitute JS expressions to produce a JS
* array constructor that can be joined on a gadget ID to produce a string of
* name-spaced CSS.
*/
class EmbeddedJsExpressionTokenConsumer implements TokenConsumer {
  private final StringBuilder partialJsStringLiteral = new StringBuilder();
  private boolean inJsString;
  private FilePosition positionAtStartOfStringLiteral, last;
  private final CssPrettyPrinter cssTokenConsumer =
      new CssPrettyPrinter(partialJsStringLiteral);
  private Expression pendingExpression;
  private final List<Expression> arrayElements = Lists.newArrayList();


  /**
   * Appends the given JS expression to the current array element.
   */
  public void consume(Expression jsExpression) {
    if (jsExpression instanceof StringLiteral) {
      if (!inJsString) {
        startPartialJsStringLiteral();
      }
      mark(jsExpression.getFilePosition());
      partialJsStringLiteral.append(
          ((StringLiteral) jsExpression).getUnquotedValue());
    } else {
      if (inJsString) {
        pendingExpression = combine(
            pendingExpression, endPartialJsStringLiteral());
      }
      pendingExpression = combine(pendingExpression, jsExpression);
    }
  }

  /**
   * Introduces a break between array elements.
   * So if the following calls happen in-sequence:
   * {@code this.consume("foo"); this.endArrayElement(); this.consume("bar");}
   * then the array elements will be {@code ['foo', 'bar']}.
   */
  public void endArrayElement() {
    endArrayElement(true);
  }

  private void endArrayElement(boolean requireHole) {
    if (inJsString) {
      pendingExpression = combine(
          pendingExpression, endPartialJsStringLiteral());
    }
    if (pendingExpression != null) {
      arrayElements.add(pendingExpression);
      pendingExpression = null;
    } else if (requireHole) {
      // If there are no calls to consume or
      arrayElements.add(
          new Elision(last != null ? last : FilePosition.UNKNOWN));
    }
  }

  /**
   * The members of the array of JS string expressions built by prior calls to
   * {@link #consume(String)}, {@link #consume(Expression)}, and
   * {@link #endArrayElement}.
   */
  public List<Expression> getArrayMembers() {
    if (inJsString || pendingExpression != null) {
      // Call noMoreTokens() before sampling the array members.
      throw new IllegalStateException();
    }
    return arrayElements;
  }

  @Override
  public void mark(FilePosition pos) {
    cssTokenConsumer.mark(pos);
    if (inJsString && positionAtStartOfStringLiteral == null) {
      positionAtStartOfStringLiteral = pos;
    }
    last = pos;
  }

  @Override
  public void consume(String text) {
    if (!inJsString) {
      startPartialJsStringLiteral();
    }
    cssTokenConsumer.consume(text);
  }

  /** May be overridden to do something before starting a string literal. */
  @OverridingMethodsMustInvokeSuper
  protected void startPartialJsStringLiteral() {
    inJsString = true;
  }

  @Override
  public void noMoreTokens() {
    cssTokenConsumer.noMoreTokens();
    endArrayElement(false);
  }

  /** Constructs a string expression. */
  @OverridingMethodsMustInvokeSuper
  protected Expression endPartialJsStringLiteral() {
    String s = partialJsStringLiteral.toString();
    partialJsStringLiteral.setLength(0);
    FilePosition pos = positionAtStartOfStringLiteral != null
        ? FilePosition.span(positionAtStartOfStringLiteral, last)
        : FilePosition.UNKNOWN;
    positionAtStartOfStringLiteral = null;
    inJsString = false;
    return StringLiteral.valueOf(pos, s);
  }

  /** (null, a) -> a, but (a, b) -> (a + b) */
  protected Expression combine(
      @Nullable Expression prefixExpression, Expression suffixExpression) {
    if (prefixExpression != null) {
      return Operation.createInfix(
          Operator.ADDITION, prefixExpression, suffixExpression);
    }
    return suffixExpression;
  }
}
TOP

Related Classes of com.google.caja.plugin.CssDynamicExpressionRewriter

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.