soy.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. // Copyright 2011 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Provides utility methods to render soy template.
  16. * @author chrishenry@google.com (Chris Henry)
  17. */
  18. goog.provide('goog.soy');
  19. goog.require('goog.asserts');
  20. goog.require('goog.dom');
  21. goog.require('goog.dom.NodeType');
  22. goog.require('goog.dom.TagName');
  23. goog.require('goog.html.legacyconversions');
  24. goog.require('goog.soy.data.SanitizedContent');
  25. goog.require('goog.soy.data.SanitizedContentKind');
  26. goog.require('goog.string');
  27. /**
  28. * @define {boolean} Whether to require all Soy templates to be "strict html".
  29. * Soy templates that use strict autoescaping forbid noAutoescape along with
  30. * many dangerous directives, and return a runtime type SanitizedContent that
  31. * marks them as safe.
  32. *
  33. * If this flag is enabled, Soy templates will fail to render if a template
  34. * returns plain text -- indicating it is a non-strict template.
  35. */
  36. goog.define('goog.soy.REQUIRE_STRICT_AUTOESCAPE', false);
  37. /**
  38. * Type definition for strict Soy templates. Very useful when passing a template
  39. * as an argument.
  40. * @typedef {function(?, null=, ?Object<string, *>=):
  41. * !goog.soy.data.SanitizedContent}
  42. */
  43. goog.soy.StrictTemplate;
  44. /**
  45. * Type definition for strict Soy HTML templates. Very useful when passing
  46. * a template as an argument.
  47. * @typedef {function(?, null=, ?Object<string, *>=):
  48. * !goog.soy.data.SanitizedHtml}
  49. */
  50. goog.soy.StrictHtmlTemplate;
  51. /**
  52. * Sets the processed template as the innerHTML of an element. It is recommended
  53. * to use this helper function instead of directly setting innerHTML in your
  54. * hand-written code, so that it will be easier to audit the code for cross-site
  55. * scripting vulnerabilities.
  56. *
  57. * @param {?Element} element The element whose content we are rendering into.
  58. * @param {!goog.soy.data.SanitizedContent} templateResult The processed
  59. * template of kind HTML or TEXT (which will be escaped).
  60. * @template ARG_TYPES
  61. */
  62. goog.soy.renderHtml = function(element, templateResult) {
  63. element.innerHTML = goog.soy.ensureTemplateOutputHtml_(templateResult);
  64. };
  65. /**
  66. * Renders a Soy template and then set the output string as
  67. * the innerHTML of an element. It is recommended to use this helper function
  68. * instead of directly setting innerHTML in your hand-written code, so that it
  69. * will be easier to audit the code for cross-site scripting vulnerabilities.
  70. *
  71. * @param {Element} element The element whose content we are rendering into.
  72. * @param {?function(ARG_TYPES, Object<string, *>=):*|
  73. * ?function(ARG_TYPES, null=, Object<string, *>=):*} template
  74. * The Soy template defining the element's content.
  75. * @param {ARG_TYPES=} opt_templateData The data for the template.
  76. * @param {Object=} opt_injectedData The injected data for the template.
  77. * @template ARG_TYPES
  78. */
  79. goog.soy.renderElement = function(
  80. element, template, opt_templateData, opt_injectedData) {
  81. // Soy template parameter is only nullable for historical reasons.
  82. goog.asserts.assert(template, 'Soy template may not be null.');
  83. element.innerHTML = goog.soy.ensureTemplateOutputHtml_(
  84. template(
  85. opt_templateData || goog.soy.defaultTemplateData_, undefined,
  86. opt_injectedData));
  87. };
  88. /**
  89. * Renders a Soy template into a single node or a document
  90. * fragment. If the rendered HTML string represents a single node, then that
  91. * node is returned (note that this is *not* a fragment, despite them name of
  92. * the method). Otherwise a document fragment is returned containing the
  93. * rendered nodes.
  94. *
  95. * @param {?function(ARG_TYPES, Object<string, *>=):*|
  96. * ?function(ARG_TYPES, null=, Object<string, *>=):*} template
  97. * The Soy template defining the element's content.
  98. * @param {ARG_TYPES=} opt_templateData The data for the template.
  99. * @param {Object=} opt_injectedData The injected data for the template.
  100. * @param {goog.dom.DomHelper=} opt_domHelper The DOM helper used to
  101. * create DOM nodes; defaults to {@code goog.dom.getDomHelper}.
  102. * @return {!Node} The resulting node or document fragment.
  103. * @template ARG_TYPES
  104. */
  105. goog.soy.renderAsFragment = function(
  106. template, opt_templateData, opt_injectedData, opt_domHelper) {
  107. // Soy template parameter is only nullable for historical reasons.
  108. goog.asserts.assert(template, 'Soy template may not be null.');
  109. var dom = opt_domHelper || goog.dom.getDomHelper();
  110. var output = template(
  111. opt_templateData || goog.soy.defaultTemplateData_, undefined,
  112. opt_injectedData);
  113. var html = goog.soy.ensureTemplateOutputHtml_(output);
  114. goog.soy.assertFirstTagValid_(html);
  115. var safeHtml = output instanceof goog.soy.data.SanitizedContent ?
  116. output.toSafeHtml() :
  117. goog.html.legacyconversions.safeHtmlFromString(html);
  118. return dom.safeHtmlToNode(safeHtml);
  119. };
  120. /**
  121. * Renders a Soy template into a single node. If the rendered
  122. * HTML string represents a single node, then that node is returned. Otherwise,
  123. * a DIV element is returned containing the rendered nodes.
  124. *
  125. * @param {?function(ARG_TYPES, Object<string, *>=):*|
  126. * ?function(ARG_TYPES, null=, Object<string, *>=):*} template
  127. * The Soy template defining the element's content.
  128. * @param {ARG_TYPES=} opt_templateData The data for the template.
  129. * @param {Object=} opt_injectedData The injected data for the template.
  130. * @param {goog.dom.DomHelper=} opt_domHelper The DOM helper used to
  131. * create DOM nodes; defaults to {@code goog.dom.getDomHelper}.
  132. * @return {!Element} Rendered template contents, wrapped in a parent DIV
  133. * element if necessary.
  134. * @template ARG_TYPES
  135. */
  136. goog.soy.renderAsElement = function(
  137. template, opt_templateData, opt_injectedData, opt_domHelper) {
  138. // Soy template parameter is only nullable for historical reasons.
  139. goog.asserts.assert(template, 'Soy template may not be null.');
  140. return goog.soy.convertToElement_(
  141. template(
  142. opt_templateData || goog.soy.defaultTemplateData_, undefined,
  143. opt_injectedData),
  144. opt_domHelper);
  145. };
  146. /**
  147. * Converts a processed Soy template into a single node. If the rendered
  148. * HTML string represents a single node, then that node is returned. Otherwise,
  149. * a DIV element is returned containing the rendered nodes.
  150. *
  151. * @param {!goog.soy.data.SanitizedContent} templateResult The processed
  152. * template of kind HTML or TEXT (which will be escaped).
  153. * @param {?goog.dom.DomHelper=} opt_domHelper The DOM helper used to
  154. * create DOM nodes; defaults to {@code goog.dom.getDomHelper}.
  155. * @return {!Element} Rendered template contents, wrapped in a parent DIV
  156. * element if necessary.
  157. */
  158. goog.soy.convertToElement = function(templateResult, opt_domHelper) {
  159. return goog.soy.convertToElement_(templateResult, opt_domHelper);
  160. };
  161. /**
  162. * Non-strict version of {@code goog.soy.convertToElement}.
  163. *
  164. * @param {*} templateResult The processed template.
  165. * @param {?goog.dom.DomHelper=} opt_domHelper The DOM helper used to
  166. * create DOM nodes; defaults to {@code goog.dom.getDomHelper}.
  167. * @return {!Element} Rendered template contents, wrapped in a parent DIV
  168. * element if necessary.
  169. * @private
  170. */
  171. goog.soy.convertToElement_ = function(templateResult, opt_domHelper) {
  172. var dom = opt_domHelper || goog.dom.getDomHelper();
  173. var wrapper = dom.createElement(goog.dom.TagName.DIV);
  174. var html = goog.soy.ensureTemplateOutputHtml_(templateResult);
  175. goog.soy.assertFirstTagValid_(html);
  176. wrapper.innerHTML = html;
  177. // If the template renders as a single element, return it.
  178. if (wrapper.childNodes.length == 1) {
  179. var firstChild = wrapper.firstChild;
  180. if (firstChild.nodeType == goog.dom.NodeType.ELEMENT) {
  181. return /** @type {!Element} */ (firstChild);
  182. }
  183. }
  184. // Otherwise, return the wrapper DIV.
  185. return wrapper;
  186. };
  187. /**
  188. * Ensures the result is "safe" to insert as HTML.
  189. *
  190. * Note if the template has non-strict autoescape, the guarantees here are very
  191. * weak. It is recommended applications switch to requiring strict
  192. * autoescaping over time by tweaking goog.soy.REQUIRE_STRICT_AUTOESCAPE.
  193. *
  194. * In the case the argument is a SanitizedContent object, it either must
  195. * already be of kind HTML, or if it is kind="text", the output will be HTML
  196. * escaped.
  197. *
  198. * @param {*} templateResult The template result.
  199. * @return {string} The assumed-safe HTML output string.
  200. * @private
  201. */
  202. goog.soy.ensureTemplateOutputHtml_ = function(templateResult) {
  203. // Allow strings as long as strict autoescaping is not mandated. Note we
  204. // allow everything that isn't an object, because some non-escaping templates
  205. // end up returning non-strings if their only print statement is a
  206. // non-escaped argument, plus some unit tests spoof templates.
  207. // TODO(gboyer): Track down and fix these cases.
  208. if (!goog.soy.REQUIRE_STRICT_AUTOESCAPE && !goog.isObject(templateResult)) {
  209. return String(templateResult);
  210. }
  211. // Allow SanitizedContent of kind HTML.
  212. if (templateResult instanceof goog.soy.data.SanitizedContent) {
  213. templateResult =
  214. /** @type {!goog.soy.data.SanitizedContent} */ (templateResult);
  215. var ContentKind = goog.soy.data.SanitizedContentKind;
  216. if (templateResult.contentKind === ContentKind.HTML) {
  217. return goog.asserts.assertString(templateResult.getContent());
  218. }
  219. if (templateResult.contentKind === ContentKind.TEXT) {
  220. // Allow text to be rendered, as long as we escape it. Other content
  221. // kinds will fail, since we don't know what to do with them.
  222. // TODO(gboyer): Perhaps also include URI in this case.
  223. return goog.string.htmlEscape(templateResult.getContent());
  224. }
  225. }
  226. goog.asserts.fail(
  227. 'Soy template output is unsafe for use as HTML: ' + templateResult);
  228. // In production, return a safe string, rather than failing hard.
  229. return 'zSoyz';
  230. };
  231. /**
  232. * Checks that the rendered HTML does not start with an invalid tag that would
  233. * likely cause unexpected output from renderAsElement or renderAsFragment.
  234. * See {@link http://www.w3.org/TR/html5/semantics.html#semantics} for reference
  235. * as to which HTML elements can be parents of each other.
  236. * @param {string} html The output of a template.
  237. * @private
  238. */
  239. goog.soy.assertFirstTagValid_ = function(html) {
  240. if (goog.asserts.ENABLE_ASSERTS) {
  241. var matches = html.match(goog.soy.INVALID_TAG_TO_RENDER_);
  242. goog.asserts.assert(
  243. !matches, 'This template starts with a %s, which ' +
  244. 'cannot be a child of a <div>, as required by soy internals. ' +
  245. 'Consider using goog.soy.renderElement instead.\nTemplate output: %s',
  246. matches && matches[0], html);
  247. }
  248. };
  249. /**
  250. * A pattern to find templates that cannot be rendered by renderAsElement or
  251. * renderAsFragment, as these elements cannot exist as the child of a <div>.
  252. * @type {!RegExp}
  253. * @private
  254. */
  255. goog.soy.INVALID_TAG_TO_RENDER_ =
  256. /^<(body|caption|col|colgroup|head|html|tr|td|th|tbody|thead|tfoot)>/i;
  257. /**
  258. * Immutable object that is passed into templates that are rendered
  259. * without any data.
  260. * @private @const
  261. */
  262. goog.soy.defaultTemplateData_ = {};