From a995fc09cb56757ab0b82f68d11a25b825769a0e Mon Sep 17 00:00:00 2001 From: gbrodman Date: Fri, 8 Feb 2019 07:50:29 -0800 Subject: [PATCH] Add the Closure Compiler/Library/Templates dependencies This CL does a few things: - Adds the template Soy-to-JS compilation (note: this requires the extra soyutils_usegoog.js file separately so that the compiled *.soy.js files work - Adds the Closure Compiler to compile and check our JS - Adds an NPM task to allow us to download dependencies - Adds the Closure library as an NPM package Note: this probably won't compile until we fix the test JS files ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=233059414 --- gradle/build.gradle | 27 + gradle/core/build.gradle | 91 +- gradle/dependencies.gradle | 1 + gradle/node_modules/soyutils_usegoog.js | 2457 +++++++++++++++++++++++ gradle/package.json | 22 + 5 files changed, 2583 insertions(+), 15 deletions(-) create mode 100644 gradle/node_modules/soyutils_usegoog.js create mode 100644 gradle/package.json diff --git a/gradle/build.gradle b/gradle/build.gradle index 798f9cd92..79fb31007 100644 --- a/gradle/build.gradle +++ b/gradle/build.gradle @@ -26,6 +26,7 @@ plugins { id 'com.bmuschko.docker-java-application' version '4.0.4' apply false id 'net.ltgt.errorprone' version '0.6.1' id 'checkstyle' + id "com.moowork.node" version "1.2.0" } apply from: 'dependencies.gradle' @@ -211,6 +212,32 @@ subprojects { } } + if (project.path.contains("default")) { + def coreResourcesDir = "${rootDir}/core/build/resources/main" + war { + from("${coreResourcesDir}/google/registry/ui") { + include "registrar_bin*" + into("assets/js") + rename { String filename -> filename.replace("bin", "dbg")} + } + from("${coreResourcesDir}/google/registry/ui") { + include "registrar_bin*" + into("assets/js") + } + from("${coreResourcesDir}/google/registry/ui/css") { + include "registrar*" + into("assets/css") + } + from("${coreResourcesDir}/google/registry/ui/assets/images") { + include "**/*" + into("assets/images") + } + from("${coreResourcesDir}/google/registry/ui/html") { + include "*.html" + } + } + } + appengine { deploy { // TODO: change this to a variable. diff --git a/gradle/core/build.gradle b/gradle/core/build.gradle index 8947ad94c..b3effcfc5 100644 --- a/gradle/core/build.gradle +++ b/gradle/core/build.gradle @@ -5,6 +5,7 @@ plugins { // Path to code generated by ad hoc tasks in this project. A separate path is // used for easy inspection. def generatedDir = "${project.buildDir}/generated/source/custom/main" +def resourcesDir = "${project.buildDir}/resources/main" // Tests that conflict with (mostly unidentified) members of the main test // suite. It is unclear if they are offenders (i.e., those that pollute global @@ -59,6 +60,7 @@ configurations { // either compile or testRuntime. However, they may be needed at runtime. // TODO(weiminyu): identify runtime dependencies and remove the rest. maybeRuntime + closureCompiler } // Known issues: @@ -269,6 +271,8 @@ dependencies { // Tool dependencies. used for doc generation. compile files("${System.properties['java.home']}/../lib/tools.jar") + + closureCompiler deps['com.google.javascript:closure-compiler'] } task jaxbToJava { @@ -384,9 +388,43 @@ task soyToJava { } } +task soyToJS { + ext.soyToJS = { outputDirectory, soyFiles , deps-> + javaexec { + main = "com.google.template.soy.SoyToJsSrcCompiler" + classpath configurations.soy + + args "--outputPathFormat", "${outputDirectory}/{INPUT_FILE_NAME}.js", + "--allowExternalCalls", "false", + "--srcs", "${soyFiles.join(',')}", + "--shouldProvideRequireSoyNamespaces", "true", + "--compileTimeGlobalsFile", "${javaDir}/google/registry/ui/globals.txt", + "--deps", "${deps.join(',')}" + } + } + + doLast { + def rootSoyFiles = + fileTree( + dir: "${javaDir}/google/registry/ui/soy", + include: ['*.soy']) + + soyToJS("${generatedDir}/google/registry/ui/soy", rootSoyFiles, "") + soyToJS("${generatedDir}/google/registry/ui/soy/registrar", + files { + file("${javaDir}/google/registry/ui/soy/registrar").listFiles() + }.filter { + it.name.endsWith(".soy") + }.filter{ + // TODO(b/123653579): add this back in when it compiles + !it.name.equals("OteSetupConsole.soy") + }, rootSoyFiles) + } +} + task stylesheetsToJavascript { def cssSourceDir = "${javaDir}/google/registry/ui/css" - def outputDir = "${project.buildDir}/resources/main/google/registry/ui/css" + def outputDir = "${resourcesDir}/google/registry/ui/css" inputs.dir cssSourceDir outputs.dir outputDir @@ -416,23 +454,40 @@ task stylesheetsToJavascript { doLast { file("${outputDir}").mkdirs() - def srcFiles = [ - "${cssSourceDir}/console.css", - "${cssSourceDir}/contact-settings.css", - "${cssSourceDir}/contact-us.css", - "${cssSourceDir}/dashboard.css", - "${cssSourceDir}/epp.css", - "${cssSourceDir}/forms.css", - "${cssSourceDir}/kd_components.css", - "${cssSourceDir}/registry.css", - "${cssSourceDir}/resources.css", - "${cssSourceDir}/security-settings.css" - ] - cssCompile("${outputDir}/registrar_bin", false, srcFiles) - cssCompile("${outputDir}/registrar_dbg", true, srcFiles) + def ignoredFiles = ["demo_css.css", "registrar_imports_raw.css"] + def sourceFiles = [] + // include all CSS files that we find except for the ones explicitly ignored + fileTree(cssSourceDir).each { + if (it.name.endsWith(".css") && !ignoredFiles.contains(it.name)) { + sourceFiles << (cssSourceDir + "/" + it.name) + } + } + cssCompile("${outputDir}/registrar_bin", false, sourceFiles) + cssCompile("${outputDir}/registrar_dbg", true, sourceFiles) } } +task compileProdJS(type: JavaExec) { + // TODO(b/123647609) clean this up in general + // - have two separate tasks for dev/prod + // - the prod task uses the ADVANCED_OPTIMIZATION flag. + // - have this be a configurable task so we can change the inputs/outputs + classpath configurations.closureCompiler + main = 'com.google.javascript.jscomp.CommandLineRunner' + + def closureArgs = [] + closureArgs << "--compilation_level=SIMPLE_OPTIMIZATIONS" + closureArgs << "--js_output_file=${resourcesDir}/google/registry/ui/registrar_bin.js" + closureArgs << "--js=${rootDir}/node_modules/google-closure-library/**.js" + closureArgs << "--js=${rootDir}/node_modules/soyutils_usegoog.js" + closureArgs << "--entry_point=goog:registry.registrar.main" + closureArgs << "--externs=${javaDir}/google/registry/ui/externs/json.js" + closureArgs << "--js=${resourcesDir}/google/registry/ui/css/registrar_bin.css.js" + closureArgs << "--js=${javaDir}/google/registry/ui/js/**.js" + closureArgs << "--js=${generatedDir}/google/registry/ui/soy/**.js" + args closureArgs +} + compileJava.dependsOn jaxbToJava compileJava.dependsOn soyToJava @@ -440,6 +495,12 @@ compileJava.dependsOn soyToJava // resources folder before copying data into it. stylesheetsToJavascript.dependsOn processResources classes.dependsOn stylesheetsToJavascript +compileProdJS.dependsOn stylesheetsToJavascript +compileProdJS.dependsOn rootProject.npmInstall +compileProdJS.dependsOn processResources +compileProdJS.dependsOn processTestResources +compileProdJS.dependsOn soyToJS +assemble.dependsOn compileProdJS // Make testing artifacts available to be depended up on by other projects. // TODO: factor out google.registry.testing to be a separate project. diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 36bcf1c2d..949a2160b 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -57,6 +57,7 @@ ext { 'com.google.http-client:google-http-client:1.28.0', 'com.google.http-client:google-http-client-appengine:1.22.0', 'com.google.http-client:google-http-client-jackson2:1.25.0', + 'com.google.javascript:closure-compiler:v20190121', 'com.google.monitoring-client:contrib:1.0.4', 'com.google.monitoring-client:metrics:1.0.4', 'com.google.monitoring-client:stackdriver:1.0.4', diff --git a/gradle/node_modules/soyutils_usegoog.js b/gradle/node_modules/soyutils_usegoog.js new file mode 100644 index 000000000..a5b40ac7d --- /dev/null +++ b/gradle/node_modules/soyutils_usegoog.js @@ -0,0 +1,2457 @@ +/* + * Copyright 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. + */ + +/** + * @fileoverview + * + * NOTE (gbrodman) this file is taken from the open source version located at + * https://github.com/google/closure-templates/blob/63f34802b543dadb72a078b006a0e7e4dbc71d7e/javascript/soyutils_usegoog.js + * and slightly modified so that it compiles. + * + * Utility functions and classes for Soy gencode + * + *

+ * This file contains utilities that should only be called by Soy-generated + * JS code. Please do not use these functions directly from + * your hand-written code. Their names all start with '$$', or exist within the + * soydata.VERY_UNSAFE namespace. + * + *

TODO(lukes): ensure that the above pattern is actually followed + * consistently. + * + */ +goog.provide('soy'); +goog.provide('soy.asserts'); +goog.provide('soy.esc'); +goog.provide('soydata'); +goog.provide('soydata.SanitizedHtml'); +goog.provide('soydata.VERY_UNSAFE'); + +goog.require('goog.array'); +goog.require('goog.asserts'); +goog.require('goog.debug'); +goog.require('goog.format'); +goog.require('goog.html.SafeHtml'); +goog.require('goog.html.SafeScript'); +goog.require('goog.html.SafeStyle'); +goog.require('goog.html.SafeStyleSheet'); +goog.require('goog.html.SafeUrl'); +goog.require('goog.html.TrustedResourceUrl'); +goog.require('goog.html.uncheckedconversions'); +goog.require('goog.i18n.BidiFormatter'); +goog.require('goog.i18n.bidi'); +goog.require('goog.object'); +goog.require('goog.soy.data.SanitizedContent'); +goog.require('goog.soy.data.SanitizedContentKind'); +goog.require('goog.soy.data.SanitizedCss'); +goog.require('goog.soy.data.SanitizedHtml'); +goog.require('goog.soy.data.SanitizedHtmlAttribute'); +goog.require('goog.soy.data.SanitizedJs'); +goog.require('goog.soy.data.SanitizedTrustedResourceUri'); +goog.require('goog.soy.data.SanitizedUri'); +goog.require('goog.soy.data.UnsanitizedText'); +goog.require('goog.string'); +goog.require('goog.string.Const'); + +// ----------------------------------------------------------------------------- +// soydata: Defines typed strings, e.g. an HTML string {@code "ac"} is +// semantically distinct from the plain text string {@code "ac"} and smart +// templates can take that distinction into account. + +/** + * Checks whether a given value is of a given content kind. + * + * @param {*} value The value to be examined. + * @param {goog.soy.data.SanitizedContentKind} contentKind The desired content + * kind. + * @return {boolean} Whether the given value is of the given kind. + * @private + */ +soydata.isContentKind_ = function(value, contentKind) { + // TODO(user): This function should really include the assert on + // value.constructor that is currently sprinkled at most of the call sites. + // Unfortunately, that would require a (debug-mode-only) switch statement. + // TODO(user): Perhaps we should get rid of the contentKind property + // altogether and only at the constructor. + return value != null && value.contentKind === contentKind; +}; + + +/** + * Returns a given value's contentDir property, constrained to a + * goog.i18n.bidi.Dir value or null. Returns null if the value is null, + * undefined, a primitive or does not have a contentDir property, or the + * property's value is not 1 (for LTR), -1 (for RTL), or 0 (for neutral). + * + * @param {*} value The value whose contentDir property, if any, is to + * be returned. + * @return {?goog.i18n.bidi.Dir} The contentDir property. + */ +soydata.getContentDir = function(value) { + if (value != null) { + switch (value.contentDir) { + case goog.i18n.bidi.Dir.LTR: + return goog.i18n.bidi.Dir.LTR; + case goog.i18n.bidi.Dir.RTL: + return goog.i18n.bidi.Dir.RTL; + case goog.i18n.bidi.Dir.NEUTRAL: + return goog.i18n.bidi.Dir.NEUTRAL; + } + } + return null; +}; + + +/** + * This class is only a holder for `soydata.SanitizedHtml.from`. Do not + * instantiate or extend it. Use `goog.soy.data.SanitizedHtml` instead. + * + * @constructor + * @extends {goog.soy.data.SanitizedHtml} + * @abstract + */ +soydata.SanitizedHtml = function() { + soydata.SanitizedHtml.base(this, 'constructor'); // Throws an exception. +}; +goog.inherits(soydata.SanitizedHtml, goog.soy.data.SanitizedHtml); + +/** + * Returns a SanitizedHtml object for a particular value. The content direction + * is preserved. + * + * This HTML-escapes the value unless it is already SanitizedHtml or SafeHtml. + * + * @param {*} value The value to convert. If it is already a SanitizedHtml + * object, it is left alone. + * @return {!goog.soy.data.SanitizedHtml} A SanitizedHtml object derived from + * the stringified value. It is escaped unless the input is SanitizedHtml or + * SafeHtml. + */ +soydata.SanitizedHtml.from = function(value) { + // The check is soydata.isContentKind_() inlined for performance. + if (value != null && + value.contentKind === goog.soy.data.SanitizedContentKind.HTML) { + goog.asserts.assert(value.constructor === goog.soy.data.SanitizedHtml); + return /** @type {!goog.soy.data.SanitizedHtml} */ (value); + } + if (value instanceof goog.html.SafeHtml) { + return soydata.VERY_UNSAFE.ordainSanitizedHtml( + goog.html.SafeHtml.unwrap(value), value.getDirection()); + } + return soydata.VERY_UNSAFE.ordainSanitizedHtml( + soy.esc.$$escapeHtmlHelper(String(value)), soydata.getContentDir(value)); +}; + + +/** + * Empty string, used as a type in Soy templates. + * @enum {string} + * @private + */ +soydata.$$EMPTY_STRING_ = { + VALUE: '' +}; + + +/** + * Creates a factory for SanitizedContent types. + * + * This is a hack so that the soydata.VERY_UNSAFE.ordainSanitized* can + * instantiate Sanitized* classes, without making the Sanitized* constructors + * publicly usable. Requiring all construction to use the VERY_UNSAFE names + * helps callers and their reviewers easily tell that creating SanitizedContent + * is not always safe and calls for careful review. + * + * @param {function(new: T)} ctor A constructor. + * @return {!function(*, ?goog.i18n.bidi.Dir=): T} A factory that takes + * content and an optional content direction and returns a new instance. If + * the content direction is undefined, ctor.prototype.contentDir is used. + * @template T + * @private + */ +soydata.$$makeSanitizedContentFactory_ = function(ctor) { + /** + * @param {string} content + * @constructor + * @extends {goog.soy.data.SanitizedContent} + */ + function InstantiableCtor(content) { + /** @override */ + this.content = content; + } + InstantiableCtor.prototype = ctor.prototype; + /** + * Creates a ctor-type SanitizedContent instance. + * + * @param {*} content The content to put in the instance. + * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction. If + * undefined, ctor.prototype.contentDir is used. + * @return {!goog.soy.data.SanitizedContent} The new instance. It is actually + * of type T above (ctor's type, a descendant of SanitizedContent), but + * there is no way to express that here. + */ + function sanitizedContentFactory(content, opt_contentDir) { + var result = new InstantiableCtor(String(content)); + if (opt_contentDir !== undefined) { + result.contentDir = opt_contentDir; + } + return result; + } + return sanitizedContentFactory; +}; + + +/** + * Creates a factory for SanitizedContent types that should always have their + * default directionality. + * + * This is a hack so that the soydata.VERY_UNSAFE.ordainSanitized* can + * instantiate Sanitized* classes, without making the Sanitized* constructors + * publicly usable. Requiring all construction to use the VERY_UNSAFE names + * helps callers and their reviewers easily tell that creating SanitizedContent + * is not always safe and calls for careful review. + * + * @param {function(new: T, string)} ctor A constructor. + * @return {!function(*): T} A factory that takes content and returns a new + * instance (with default directionality, i.e. ctor.prototype.contentDir). + * @template T + * @private + */ +soydata.$$makeSanitizedContentFactoryWithDefaultDirOnly_ = function(ctor) { + /** + * @param {string} content + * @constructor + * @extends {goog.soy.data.SanitizedContent} + */ + function InstantiableCtor(content) { + /** @override */ + this.content = content; + } + InstantiableCtor.prototype = ctor.prototype; + /** + * Creates a ctor-type SanitizedContent instance. + * + * @param {*} content The content to put in the instance. + * @return {!goog.soy.data.SanitizedContent} The new instance. It is actually + * of type T above (ctor's type, a descendant of SanitizedContent), but + * there is no way to express that here. + */ + function sanitizedContentFactory(content) { + var result = new InstantiableCtor(String(content)); + return result; + } + return sanitizedContentFactory; +}; + + +// ----------------------------------------------------------------------------- +// Sanitized content ordainers. Please use these with extreme caution (with the +// exception of markUnsanitizedText). A good recommendation is to limit usage +// of these to just a handful of files in your source tree where usages can be +// carefully audited. + + +/** + * Protects a string from being used in an noAutoescaped context. + * + * This is useful for content where there is significant risk of accidental + * unescaped usage in a Soy template. A great case is for user-controlled + * data that has historically been a source of vulernabilities. + * + * @param {*} content Text to protect. + * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction; null if + * unknown and thus to be estimated when necessary. Default: null. + * @return {!goog.soy.data.UnsanitizedText} A wrapper that is rejected by the + * Soy noAutoescape print directive. + */ +soydata.markUnsanitizedText = function(content, opt_contentDir) { + return new goog.soy.data.UnsanitizedText(content, opt_contentDir); +}; + + +/** + * Takes a leap of faith that the provided content is "safe" HTML. + * + * @param {*} content A string of HTML that can safely be embedded in + * a PCDATA context in your app. If you would be surprised to find that an + * HTML sanitizer produced `s` (e.g. it runs code or fetches bad URLs) + * and you wouldn't write a template that produces `s` on security or + * privacy grounds, then don't pass `s` here. + * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction; null if + * unknown and thus to be estimated when necessary. Default: null. + * @return {!goog.soy.data.SanitizedHtml} Sanitized content wrapper that + * indicates to Soy not to escape when printed as HTML. + */ +soydata.VERY_UNSAFE.ordainSanitizedHtml = + soydata.$$makeSanitizedContentFactory_(goog.soy.data.SanitizedHtml); + + +/** + * Takes a leap of faith that the provided content is "safe" (non-attacker- + * controlled, XSS-free) Javascript. + * + * @param {*} content Javascript source that when evaluated does not + * execute any attacker-controlled scripts. + * @return {!goog.soy.data.SanitizedJs} Sanitized content wrapper that indicates + * to Soy not to escape when printed as Javascript source. + */ +soydata.VERY_UNSAFE.ordainSanitizedJs = + soydata.$$makeSanitizedContentFactoryWithDefaultDirOnly_( + goog.soy.data.SanitizedJs); + + +/** + * Takes a leap of faith that the provided content is "safe" to use as a URI + * in a Soy template. + * + * This creates a Soy SanitizedContent object which indicates to Soy there is + * no need to escape it when printed as a URI (e.g. in an href or src + * attribute), such as if it's already been encoded or if it's a Javascript: + * URI. + * + * @param {*} content A chunk of URI that the caller knows is safe to + * emit in a template. + * @return {!goog.soy.data.SanitizedUri} Sanitized content wrapper that + * indicates to Soy not to escape or filter when printed in URI context. + */ +soydata.VERY_UNSAFE.ordainSanitizedUri = + soydata.$$makeSanitizedContentFactoryWithDefaultDirOnly_( + goog.soy.data.SanitizedUri); + + +/** + * Takes a leap of faith that the provided content is "safe" to use as a + * TrustedResourceUri in a Soy template. + * + * This creates a Soy SanitizedContent object which indicates to Soy there is + * no need to filter it when printed as a TrustedResourceUri. + * + * @param {*} content A chunk of TrustedResourceUri such as that the caller + * knows is safe to emit in a template. + * @return {!goog.soy.data.SanitizedTrustedResourceUri} Sanitized content + * wrapper that indicates to Soy not to escape or filter when printed in + * TrustedResourceUri context. + */ +soydata.VERY_UNSAFE.ordainSanitizedTrustedResourceUri = + soydata.$$makeSanitizedContentFactoryWithDefaultDirOnly_( + goog.soy.data.SanitizedTrustedResourceUri); + + +/** + * Takes a leap of faith that the provided content is "safe" to use as an + * HTML attribute. + * + * @param {*} content An attribute name and value, such as + * {@code dir="ltr"}. + * @return {!goog.soy.data.SanitizedHtmlAttribute} Sanitized content wrapper + * that indicates to Soy not to escape when printed as an HTML attribute. + */ +soydata.VERY_UNSAFE.ordainSanitizedHtmlAttribute = + soydata.$$makeSanitizedContentFactoryWithDefaultDirOnly_( + goog.soy.data.SanitizedHtmlAttribute); + + +/** + * Takes a leap of faith that the provided content is "safe" to use as CSS + * in a style block. + * + * @param {*} content CSS, such as `color:#c3d9ff`. + * @return {!goog.soy.data.SanitizedCss} Sanitized CSS wrapper that indicates to + * Soy there is no need to escape or filter when printed in CSS context. + */ +soydata.VERY_UNSAFE.ordainSanitizedCss = + soydata.$$makeSanitizedContentFactoryWithDefaultDirOnly_( + goog.soy.data.SanitizedCss); + + +// ----------------------------------------------------------------------------- +// Soy-generated utilities in the soy namespace. Contains implementations for +// common soyfunctions (e.g. keys()) and escaping/print directives. + + +/** + * Whether the locale is right-to-left. + * + * @type {boolean} + */ +soy.$$IS_LOCALE_RTL = goog.i18n.bidi.IS_RTL; + + +/** + * Builds an augmented map. The returned map will contain mappings from both + * the base map and the additional map. If the same key appears in both, then + * the value from the additional map will be visible, while the value from the + * base map will be hidden. The base map will be used, but not modified. + * + * @param {!Object} baseMap The original map to augment. + * @param {!Object} additionalMap A map containing the additional mappings. + * @return {!Object} An augmented map containing both the original and + * additional mappings. + */ +soy.$$augmentMap = function(baseMap, additionalMap) { + return soy.$$assignDefaults(soy.$$assignDefaults({}, additionalMap), baseMap); +}; + + +/** + * Copies extra properties into an object if they do not already exist. The + * destination object is mutated in the process. + * + * @param {!Object} obj The destination object to update. + * @param {!Object} defaults An object with default properties to apply. + * @return {!Object} The destination object for convenience. + */ +soy.$$assignDefaults = function(obj, defaults) { + for (var key in defaults) { + if (!(key in obj)) { + obj[key] = defaults[key]; + } + } + + return obj; +}; + + +/** + * Checks that the given map key is a string. + * + *

This is used to validate keys for legacy object map literals. + * + * @param {*} key Key to check. + * @return {string} The given key. + */ +soy.$$checkLegacyObjectMapLiteralKey = function(key) { + if ((typeof key) != 'string') { + throw Error( + 'Map literal\'s key expression must evaluate to string' + + ' (encountered type "' + (typeof key) + '").'); + } + return key; +}; + + +/** + * Gets the keys in a map as an array. There are no guarantees on the order. + * @param {Object} map The map to get the keys of. + * @return {!Array} The array of keys in the given map. + */ +soy.$$getMapKeys = function(map) { + var mapKeys = []; + for (var key in map) { + mapKeys.push(key); + } + return mapKeys; +}; + + +/** + * Returns the argument if it is not null. + * + * @param {T} val The value to check + * @return {T} val if is isn't null + * @template T + */ +soy.$$checkNotNull = function(val) { + if (val == null) { + throw Error('unexpected null value'); + } + return val; +}; + + +/** + * Parses the given string into a base 10 integer. Returns null if parse is + * unsuccessful. + * @param {string} str The string to parse + * @return {?number} The string parsed as a base 10 integer, or null if + * unsuccessful + */ +soy.$$parseInt = function(str) { + var parsed = parseInt(str, 10); + return isNaN(parsed) ? null : parsed; +}; + + +/** + * Parses the given string into a float. Returns null if parse is unsuccessful. + * @param {string} str The string to parse + * @return {?number} The string parsed as a float, or null if unsuccessful. + */ +soy.$$parseFloat = function(str) { + var parsed = parseFloat(str); + return isNaN(parsed) ? null : parsed; +}; + + +/** + * Gets a consistent unique id for the given delegate template name. Two calls + * to this function will return the same id if and only if the input names are + * the same. + * + *

Important: This function must always be called with a string constant. + * + *

If Closure Compiler is not being used, then this is just this identity + * function. If Closure Compiler is being used, then each call to this function + * will be replaced with a short string constant, which will be consistent per + * input name. + * + * @param {string} delTemplateName The delegate template name for which to get a + * consistent unique id. + * @return {string} A unique id that is consistent per input name. + * + * @idGenerator {consistent} + */ +soy.$$getDelTemplateId = function(delTemplateName) { + return delTemplateName; +}; + + +/** + * Map from registered delegate template key to the priority of the + * implementation. + * @type {Object} + * @private + */ +soy.$$DELEGATE_REGISTRY_PRIORITIES_ = {}; + +/** + * Map from registered delegate template key to the implementation function. + * @type {Object} + * @private + */ +soy.$$DELEGATE_REGISTRY_FUNCTIONS_ = {}; + + +/** + * Registers a delegate implementation. If the same delegate template key (id + * and variant) has been registered previously, then priority values are + * compared and only the higher priority implementation is stored (if + * priorities are equal, an error is thrown). + * + * @param {string} delTemplateId The delegate template id. + * @param {string} delTemplateVariant The delegate template variant (can be + * empty string). + * @param {number} delPriority The implementation's priority value. + * @param {Function} delFn The implementation function. + */ +soy.$$registerDelegateFn = function( + delTemplateId, delTemplateVariant, delPriority, delFn) { + var mapKey = 'key_' + delTemplateId + ':' + delTemplateVariant; + var currPriority = soy.$$DELEGATE_REGISTRY_PRIORITIES_[mapKey]; + if (currPriority === undefined || delPriority > currPriority) { + // Registering new or higher-priority function: replace registry entry. + soy.$$DELEGATE_REGISTRY_PRIORITIES_[mapKey] = delPriority; + soy.$$DELEGATE_REGISTRY_FUNCTIONS_[mapKey] = delFn; + } else if (delPriority == currPriority) { + // Registering same-priority function: error. + throw Error( + 'Encountered two active delegates with the same priority ("' + + delTemplateId + ':' + delTemplateVariant + '").'); + } else { + // Registering lower-priority function: do nothing. + } +}; + + +/** + * Retrieves the (highest-priority) implementation that has been registered for + * a given delegate template key (id and variant). If no implementation has + * been registered for the key, then the fallback is the same id with empty + * variant. If the fallback is also not registered, and allowsEmptyDefault is + * true, then returns an implementation that is equivalent to an empty template + * (i.e. rendered output would be empty string). + * + * @param {string} delTemplateId The delegate template id. + * @param {string} delTemplateVariant The delegate template variant (can be + * empty string). + * @param {boolean} allowsEmptyDefault Whether to default to the empty template + * function if there's no active implementation. + * @return {Function} The retrieved implementation function. + */ +soy.$$getDelegateFn = function( + delTemplateId, delTemplateVariant, allowsEmptyDefault) { + var delFn = + soy.$$DELEGATE_REGISTRY_FUNCTIONS_['key_' + delTemplateId + ':' + delTemplateVariant]; + if (!delFn && delTemplateVariant != '') { + // Fallback to empty variant. + delFn = soy.$$DELEGATE_REGISTRY_FUNCTIONS_['key_' + delTemplateId + ':']; + } + + if (delFn) { + return delFn; + } else if (allowsEmptyDefault) { + return soy.$$EMPTY_TEMPLATE_FN_; + } else { + throw Error( + 'Found no active impl for delegate call to "' + delTemplateId + + (delTemplateVariant ? ':' + delTemplateVariant : '') + + '" (and delcall does not set allowemptydefault="true").'); + } +}; + + +/** + * Private helper soy.$$getDelegateFn(). This is the empty template function + * that is returned whenever there's no delegate implementation found. + * + * @param {Object=} opt_data + * @param {Object=} opt_ijData + * @param {Object=} opt_ijData_deprecated TODO(b/36644846): remove + * @return {string} + * @private + */ +soy.$$EMPTY_TEMPLATE_FN_ = function( + opt_data, opt_ijData, opt_ijData_deprecated) { + return ''; +}; + + +// ----------------------------------------------------------------------------- +// Internal sanitized content wrappers. + + +/** + * Creates a SanitizedContent factory for SanitizedContent types for internal + * Soy let and param blocks. + * + * This is a hack within Soy so that SanitizedContent objects created via let + * and param blocks will truth-test as false if they are empty string. + * Tricking the Javascript runtime to treat empty SanitizedContent as falsey is + * not possible, and changing the Soy compiler to wrap every boolean statement + * for just this purpose is impractical. Instead, we just avoid wrapping empty + * string as SanitizedContent, since it's a no-op for empty strings anyways. + * + * @param {function(new: T)} ctor A constructor. + * @return {!function(*, ?goog.i18n.bidi.Dir=): (T|soydata.$$EMPTY_STRING_)} + * A factory that takes content and an optional content direction and + * returns a new instance, or an empty string. If the content direction is + * undefined, ctor.prototype.contentDir is used. + * @template T + * @private + */ +soydata.$$makeSanitizedContentFactoryForInternalBlocks_ = function(ctor) { + /** + * @param {string} content + * @constructor + * @extends {goog.soy.data.SanitizedContent} + */ + function InstantiableCtor(content) { + /** @override */ + this.content = content; + } + InstantiableCtor.prototype = ctor.prototype; + /** + * Creates a ctor-type SanitizedContent instance. + * + * @param {*} content The content to put in the instance. + * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction. If + * undefined, ctor.prototype.contentDir is used. + * @return {!goog.soy.data.SanitizedContent|soydata.$$EMPTY_STRING_} The new + * instance, or an empty string. A new instance is actually of type T + * above (ctor's type, a descendant of SanitizedContent), but there's no + * way to express that here. + */ + function sanitizedContentFactory(content, opt_contentDir) { + var contentString = String(content); + if (!contentString) { + return soydata.$$EMPTY_STRING_.VALUE; + } + var result = new InstantiableCtor(contentString); + if (opt_contentDir !== undefined) { + result.contentDir = opt_contentDir; + } + return result; + } + return sanitizedContentFactory; +}; + + +/** + * Creates a SanitizedContent factory for SanitizedContent types that should + * always have their default directionality for internal Soy let and param + * blocks. + * + * This is a hack within Soy so that SanitizedContent objects created via let + * and param blocks will truth-test as false if they are empty string. + * Tricking the Javascript runtime to treat empty SanitizedContent as falsey is + * not possible, and changing the Soy compiler to wrap every boolean statement + * for just this purpose is impractical. Instead, we just avoid wrapping empty + * string as SanitizedContent, since it's a no-op for empty strings anyways. + * + * @param {function(new: T)} ctor A constructor. + * @return {!function(*): (T|soydata.$$EMPTY_STRING_)} A + * factory that takes content and returns a + * new instance (with default directionality, i.e. + * ctor.prototype.contentDir), or an empty string. + * @template T + * @private + */ +soydata.$$makeSanitizedContentFactoryWithDefaultDirOnlyForInternalBlocks_ = + function(ctor) { + /** + * @param {string} content + * @constructor + * @extends {goog.soy.data.SanitizedContent} + */ + function InstantiableCtor(content) { + /** @override */ + this.content = content; + } + InstantiableCtor.prototype = ctor.prototype; + /** + * Creates a ctor-type SanitizedContent instance. + * + * @param {*} content The content to put in the instance. + * @return {!goog.soy.data.SanitizedContent|soydata.$$EMPTY_STRING_} The new + * instance, or an empty string. A new instance is actually of type T + * above (ctor's type, a descendant of SanitizedContent), but there's no + * way to express that here. + */ + function sanitizedContentFactory(content) { + var contentString = String(content); + if (!contentString) { + return soydata.$$EMPTY_STRING_.VALUE; + } + var result = new InstantiableCtor(contentString); + return result; + } + return sanitizedContentFactory; +}; + + +/** + * Creates kind="text" block contents (internal use only). + * + * @param {*} content Text. + * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction; null if + * unknown and thus to be estimated when necessary. Default: null. + * @return {!goog.soy.data.UnsanitizedText|soydata.$$EMPTY_STRING_} Wrapped + * result. + */ +soydata.$$markUnsanitizedTextForInternalBlocks = function( + content, opt_contentDir) { + var contentString = String(content); + if (!contentString) { + return soydata.$$EMPTY_STRING_.VALUE; + } + return new goog.soy.data.UnsanitizedText(contentString, opt_contentDir); +}; + + +/** + * Creates kind="html" block contents (internal use only). + * + * @param {*} content Text. + * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction; null if + * unknown and thus to be estimated when necessary. Default: null. + * @return {!goog.soy.data.SanitizedHtml|soydata.$$EMPTY_STRING_} Wrapped + * result. + */ +soydata.VERY_UNSAFE.$$ordainSanitizedHtmlForInternalBlocks = + soydata.$$makeSanitizedContentFactoryForInternalBlocks_( + goog.soy.data.SanitizedHtml); + + +/** + * Creates kind="js" block contents (internal use only). + * + * @param {*} content Text. + * @return {!goog.soy.data.SanitizedJs|soydata.$$EMPTY_STRING_} Wrapped result. + */ +soydata.VERY_UNSAFE.$$ordainSanitizedJsForInternalBlocks = + soydata.$$makeSanitizedContentFactoryWithDefaultDirOnlyForInternalBlocks_( + goog.soy.data.SanitizedJs); + + +/** + * Creates kind="trustedResourceUri" block contents (internal use only). + * + * @param {*} content Text. + * @return {goog.soy.data.SanitizedTrustedResourceUri|soydata.$$EMPTY_STRING_} + * Wrapped result. + */ +soydata.VERY_UNSAFE.$$ordainSanitizedTrustedResourceUriForInternalBlocks = + soydata.$$makeSanitizedContentFactoryWithDefaultDirOnlyForInternalBlocks_( + goog.soy.data.SanitizedTrustedResourceUri); + + +/** + * Creates kind="uri" block contents (internal use only). + * + * @param {*} content Text. + * @return {goog.soy.data.SanitizedUri|soydata.$$EMPTY_STRING_} Wrapped result. + */ +soydata.VERY_UNSAFE.$$ordainSanitizedUriForInternalBlocks = + soydata.$$makeSanitizedContentFactoryWithDefaultDirOnlyForInternalBlocks_( + goog.soy.data.SanitizedUri); + + +/** + * Creates kind="attributes" block contents (internal use only). + * + * @param {*} content Text. + * @return {goog.soy.data.SanitizedHtmlAttribute|soydata.$$EMPTY_STRING_} + * Wrapped result. + */ +soydata.VERY_UNSAFE.$$ordainSanitizedAttributesForInternalBlocks = + soydata.$$makeSanitizedContentFactoryWithDefaultDirOnlyForInternalBlocks_( + goog.soy.data.SanitizedHtmlAttribute); + + +/** + * Creates kind="css" block contents (internal use only). + * + * @param {*} content Text. + * @return {goog.soy.data.SanitizedCss|soydata.$$EMPTY_STRING_} Wrapped result. + */ +soydata.VERY_UNSAFE.$$ordainSanitizedCssForInternalBlocks = + soydata.$$makeSanitizedContentFactoryWithDefaultDirOnlyForInternalBlocks_( + goog.soy.data.SanitizedCss); + + +// ----------------------------------------------------------------------------- +// Escape/filter/normalize. + + +/** + * Returns a SanitizedHtml object for a particular value. The content direction + * is preserved. + * + * This HTML-escapes the value unless it is already SanitizedHtml. Escapes + * double quote '"' in addition to '&', '<', and '>' so that a string can be + * included in an HTML tag attribute value within double quotes. + * + * @param {*} value The value to convert. If it is already a SanitizedHtml + * object, it is left alone. + * @return {!goog.soy.data.SanitizedHtml} An escaped version of value. + */ +soy.$$escapeHtml = function(value) { + return soydata.SanitizedHtml.from(value); +}; + + +/** + * Strips unsafe tags to convert a string of untrusted HTML into HTML that + * is safe to embed. The content direction is preserved. + * + * @param {?} value The string-like value to be escaped. May not be a string, + * but the value will be coerced to a string. + * @param {Array=} opt_safeTags Additional tag names to whitelist. + * @return {!goog.soy.data.SanitizedHtml} A sanitized and normalized version of + * value. + */ +soy.$$cleanHtml = function(value, opt_safeTags) { + if (soydata.isContentKind_(value, goog.soy.data.SanitizedContentKind.HTML)) { + goog.asserts.assert(value.constructor === goog.soy.data.SanitizedHtml); + return /** @type {!goog.soy.data.SanitizedHtml} */ (value); + } + var tagWhitelist; + if (opt_safeTags) { + tagWhitelist = goog.object.createSet(opt_safeTags); + goog.object.extend(tagWhitelist, soy.esc.$$SAFE_TAG_WHITELIST_); + } else { + tagWhitelist = soy.esc.$$SAFE_TAG_WHITELIST_; + } + return soydata.VERY_UNSAFE.ordainSanitizedHtml( + soy.$$stripHtmlTags(value, tagWhitelist), soydata.getContentDir(value)); +}; + + +/** + * Escapes HTML, except preserves entities. + * + * Used mainly internally for escaping message strings in attribute and rcdata + * context, where we explicitly want to preserve any existing entities. + * + * @param {*} value Value to normalize. + * @return {string} A value safe to insert in HTML without any quotes or angle + * brackets. + */ +soy.$$normalizeHtml = function(value) { + return soy.esc.$$normalizeHtmlHelper(value); +}; + + +/** + * Escapes HTML special characters in a string so that it can be embedded in + * RCDATA. + *

+ * Escapes HTML special characters so that the value will not prematurely end + * the body of a tag like {@code }. + *

+ * Will normalize known safe HTML to make sure that sanitized HTML (which could + * contain an innocuous {@code } don't prematurely end an RCDATA + * element. + * + * @param {?} value The string-like value to be escaped. May not be a string, + * but the value will be coerced to a string. + * @return {string} An escaped version of value. + */ +soy.$$escapeHtmlRcdata = function(value) { + if (soydata.isContentKind_(value, goog.soy.data.SanitizedContentKind.HTML)) { + goog.asserts.assert(value.constructor === goog.soy.data.SanitizedHtml); + return soy.esc.$$normalizeHtmlHelper(value.getContent()); + } + return soy.esc.$$escapeHtmlHelper(value); +}; + + +/** + * Matches any/only HTML5 void elements' start tags. + * See http://www.w3.org/TR/html-markup/syntax.html#syntax-elements + * @type {RegExp} + * @private + */ +soy.$$HTML5_VOID_ELEMENTS_ = new RegExp( + '^<(?:area|base|br|col|command|embed|hr|img|input' + + '|keygen|link|meta|param|source|track|wbr)\\b'); + + +/** + * Removes HTML tags from a string of known safe HTML. + * If opt_tagWhitelist is not specified or is empty, then + * the result can be used as an attribute value. + * + * @param {*} value The HTML to be escaped. May not be a string, but the + * value will be coerced to a string. + * @param {Object=} opt_tagWhitelist Has an own property whose + * name is a lower-case tag name and whose value is `1` for + * each element that is allowed in the output. + * @return {string} A representation of value without disallowed tags, + * HTML comments, or other non-text content. + */ +soy.$$stripHtmlTags = function(value, opt_tagWhitelist) { + if (!opt_tagWhitelist) { + // If we have no white-list, then use a fast track which elides all tags. + return String(value) + .replace(soy.esc.$$HTML_TAG_REGEX_, '') + // This is just paranoia since callers should normalize the result + // anyway, but if they didn't, it would be necessary to ensure that + // after the first replace non-tag uses of < do not recombine into + // tags as in "<script>alert(1337)script>". + .replace(soy.esc.$$LT_REGEX_, '<'); + } + + // Escapes '[' so that we can use [123] below to mark places where tags + // have been removed. + var html = String(value).replace(/\[/g, '['); + + // Consider all uses of '<' and replace whitelisted tags with markers like + // [1] which are indices into a list of approved tag names. + // Replace all other uses of < and > with entities. + var tags = []; + var attrs = []; + html = html.replace(soy.esc.$$HTML_TAG_REGEX_, function(tok, tagName) { + if (tagName) { + tagName = tagName.toLowerCase(); + if (opt_tagWhitelist.hasOwnProperty(tagName) && + opt_tagWhitelist[tagName]) { + var isClose = tok.charAt(1) == '/'; + var index = tags.length; + var start = ''; + attrs[index] = attributes; + return '[' + index + ']'; + } + } + return ''; + }); + + // Escape HTML special characters. Now there are no '<' in html that could + // start a tag. + html = soy.esc.$$normalizeHtmlHelper(html); + + var finalCloseTags = soy.$$balanceTags_(tags); + + // Now html contains no tags or less-than characters that could become + // part of a tag via a replacement operation and tags only contains + // approved tags. + // Reinsert the white-listed tags. + html = html.replace(/\[(\d+)\]/g, function(_, index) { + if (attrs[index] && tags[index]) { + return tags[index].substr(0, tags[index].length - 1) + attrs[index] + '>'; + } + return tags[index]; + }); + + // Close any still open tags. + // This prevents unclosed formatting elements like

    and from + // breaking the layout of containing HTML. + return html + finalCloseTags; +}; + + +/** + * Make sure that tag boundaries are not broken by Safe CSS when embedded in a + * {@code