safehtmlformatter.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. // Copyright 2016 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. goog.provide('goog.html.SafeHtmlFormatter');
  15. goog.require('goog.asserts');
  16. goog.require('goog.dom.tags');
  17. goog.require('goog.html.SafeHtml');
  18. goog.require('goog.string');
  19. /**
  20. * Formatter producing SafeHtml from a plain text format and HTML fragments.
  21. *
  22. * Example usage:
  23. *
  24. * var formatter = new goog.html.SafeHtmlFormatter();
  25. * var safeHtml = formatter.format(
  26. * formatter.startTag('b') +
  27. * 'User input:' +
  28. * formatter.endTag('b') +
  29. * ' ' +
  30. * formatter.text(userInput));
  31. *
  32. * The most common usage is with goog.getMsg:
  33. *
  34. * var MSG_USER_INPUT = goog.getMsg(
  35. * '{$startLink}Learn more{$endLink} about {$userInput}', {
  36. * 'startLink': formatter.startTag('a', {'href': url}),
  37. * 'endLink': formatter.endTag('a'),
  38. * 'userInput': formatter.text(userInput)
  39. * });
  40. * var safeHtml = formatter.format(MSG_USER_INPUT);
  41. *
  42. * The formatting string should be constant with all variables processed by
  43. * formatter.text().
  44. *
  45. * @constructor
  46. * @struct
  47. * @final
  48. */
  49. goog.html.SafeHtmlFormatter = function() {
  50. /**
  51. * Mapping from a marker to a replacement.
  52. * @private {!Object<string, !goog.html.SafeHtmlFormatter.Replacement>}
  53. */
  54. this.replacements_ = {};
  55. /** @private {number} Number of stored replacements. */
  56. this.replacementsCount_ = 0;
  57. };
  58. /**
  59. * @typedef {?{
  60. * startTag: (string|undefined),
  61. * attributes: (string|undefined),
  62. * endTag: (string|undefined),
  63. * html: (string|undefined)
  64. * }}
  65. */
  66. goog.html.SafeHtmlFormatter.Replacement;
  67. /**
  68. * Formats a plain text string with markers holding HTML fragments to SafeHtml.
  69. * @param {string} format Plain text format, will be HTML-escaped.
  70. * @return {!goog.html.SafeHtml}
  71. */
  72. goog.html.SafeHtmlFormatter.prototype.format = function(format) {
  73. var openedTags = [];
  74. var html = goog.string.htmlEscape(format).replace(
  75. /\{SafeHtmlFormatter:\w+\}/g,
  76. goog.bind(this.replaceFormattingString_, this, openedTags));
  77. goog.asserts.assert(openedTags.length == 0,
  78. 'Expected no unclosed tags, got <' + openedTags.join('>, <') + '>.');
  79. return goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
  80. html, null);
  81. };
  82. /**
  83. * Replaces found formatting strings with saved tags.
  84. * @param {!Array<string>} openedTags The tags opened so far, modified by this
  85. * function.
  86. * @param {string} match
  87. * @return {string}
  88. * @private
  89. */
  90. goog.html.SafeHtmlFormatter.prototype.replaceFormattingString_ =
  91. function(openedTags, match) {
  92. var replacement = this.replacements_[match];
  93. if (!replacement) {
  94. // Someone included a string looking like our internal marker in the format.
  95. return match;
  96. }
  97. var result = '';
  98. if (replacement.startTag) {
  99. result += '<' + replacement.startTag + replacement.attributes + '>';
  100. if (goog.asserts.ENABLE_ASSERTS) {
  101. if (!goog.dom.tags.isVoidTag(replacement.startTag.toLowerCase())) {
  102. openedTags.push(replacement.startTag.toLowerCase());
  103. }
  104. }
  105. }
  106. if (replacement.html) {
  107. result += replacement.html;
  108. }
  109. if (replacement.endTag) {
  110. result += '</' + replacement.endTag + '>';
  111. if (goog.asserts.ENABLE_ASSERTS) {
  112. var lastTag = openedTags.pop();
  113. goog.asserts.assert(lastTag == replacement.endTag.toLowerCase(),
  114. 'Expected </' + lastTag + '>, got </' + replacement.endTag + '>.');
  115. }
  116. }
  117. return result;
  118. };
  119. /**
  120. * Saves a start tag and returns its marker.
  121. * @param {string} tagName
  122. * @param {?Object<string, ?goog.html.SafeHtml.AttributeValue>=} opt_attributes
  123. * Mapping from attribute names to their values. Only attribute names
  124. * consisting of [a-zA-Z0-9-] are allowed. Value of null or undefined causes
  125. * the attribute to be omitted.
  126. * @return {string} Marker.
  127. * @throws {Error} If invalid tag name, attribute name, or attribute value is
  128. * provided. This function accepts the same tags and attributes as
  129. * {@link goog.html.SafeHtml.create}.
  130. */
  131. goog.html.SafeHtmlFormatter.prototype.startTag = function(
  132. tagName, opt_attributes) {
  133. goog.html.SafeHtml.verifyTagName(tagName);
  134. return this.storeReplacement_({
  135. startTag: tagName,
  136. attributes: goog.html.SafeHtml.stringifyAttributes(tagName, opt_attributes)
  137. });
  138. };
  139. /**
  140. * Saves an end tag and returns its marker.
  141. * @param {string} tagName
  142. * @return {string} Marker.
  143. * @throws {Error} If invalid tag name, attribute name, or attribute value is
  144. * provided. This function accepts the same tags and attributes as
  145. * {@link goog.html.SafeHtml.create}.
  146. */
  147. goog.html.SafeHtmlFormatter.prototype.endTag = function(tagName) {
  148. goog.html.SafeHtml.verifyTagName(tagName);
  149. return this.storeReplacement_({endTag: tagName});
  150. };
  151. /**
  152. * Escapes a text, saves it and returns its marker.
  153. *
  154. * Wrapping any user input to .text() prevents the attacker with access to
  155. * the random number generator to duplicate tags used elsewhere in the format.
  156. *
  157. * @param {string} text
  158. * @return {string} Marker.
  159. */
  160. goog.html.SafeHtmlFormatter.prototype.text = function(text) {
  161. return this.storeReplacement_({html: goog.string.htmlEscape(text)});
  162. };
  163. /**
  164. * Saves SafeHtml and returns its marker.
  165. * @param {!goog.html.SafeHtml} safeHtml
  166. * @return {string} Marker.
  167. */
  168. goog.html.SafeHtmlFormatter.prototype.safeHtml = function(safeHtml) {
  169. return this.storeReplacement_({
  170. html: goog.html.SafeHtml.unwrap(safeHtml)
  171. });
  172. };
  173. /**
  174. * Stores a replacement and returns its marker.
  175. * @param {!goog.html.SafeHtmlFormatter.Replacement} replacement
  176. * @return {string} Marker.
  177. * @private
  178. */
  179. goog.html.SafeHtmlFormatter.prototype.storeReplacement_ = function(
  180. replacement) {
  181. this.replacementsCount_++;
  182. var marker = '{SafeHtmlFormatter:' + this.replacementsCount_ + '_' +
  183. goog.string.getRandomString() + '}';
  184. this.replacements_[marker] = replacement;
  185. return marker;
  186. };