template_expressions.cc 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. // Copyright 2015 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. #include "ui/base/template_expressions.h"
  5. #include <stddef.h>
  6. #include <ostream>
  7. #include "base/check_op.h"
  8. #include "base/strings/escape.h"
  9. #include "base/strings/string_piece.h"
  10. #include "base/values.h"
  11. #include "build/chromeos_buildflags.h"
  12. #include "third_party/abseil-cpp/absl/types/optional.h"
  13. #if DCHECK_IS_ON()
  14. #include "third_party/re2/src/re2/re2.h" // nogncheck
  15. #endif
  16. namespace {
  17. const char kLeader[] = "$i18n";
  18. const size_t kLeaderSize = std::size(kLeader) - 1;
  19. const char kKeyOpen = '{';
  20. const char kKeyClose = '}';
  21. const char kHtmlTemplateEnd[] = "<!--_html_template_end_-->";
  22. const char kHtmlTemplateStart[] = "<!--_html_template_start_-->";
  23. const size_t kHtmlTemplateStartSize = std::size(kHtmlTemplateStart) - 1;
  24. enum HtmlTemplateType { INVALID = 0, NONE = 1, VALID = 2 };
  25. struct HtmlTemplate {
  26. base::StringPiece::size_type start;
  27. base::StringPiece::size_type length;
  28. HtmlTemplateType type;
  29. };
  30. HtmlTemplate FindHtmlTemplate(const base::StringPiece& source) {
  31. HtmlTemplate out;
  32. base::StringPiece::size_type found = source.find(kHtmlTemplateStart);
  33. // No template found, return early.
  34. if (found == base::StringPiece::npos) {
  35. out.type = NONE;
  36. return out;
  37. }
  38. out.start = found + kHtmlTemplateStartSize;
  39. base::StringPiece::size_type found_end =
  40. source.find(kHtmlTemplateEnd, out.start);
  41. // Template is not terminated.
  42. if (found_end == base::StringPiece::npos) {
  43. out.type = INVALID;
  44. return out;
  45. }
  46. out.length = found_end - out.start;
  47. // Check for a nested template
  48. if (source.substr(out.start, out.length).find(kHtmlTemplateStart) !=
  49. base::StringPiece::npos) {
  50. out.type = INVALID;
  51. return out;
  52. }
  53. out.type = VALID;
  54. return out;
  55. }
  56. // Escape quotes and backslashes ('"\).
  57. std::string PolymerParameterEscape(const std::string& in_string,
  58. bool is_javascript) {
  59. std::string out;
  60. out.reserve(in_string.size() * 2);
  61. for (const char c : in_string) {
  62. switch (c) {
  63. case '\\':
  64. out.append(is_javascript ? R"(\\\\)" : R"(\\)");
  65. break;
  66. case '\'':
  67. out.append(is_javascript ? R"(\\')" : R"(\')");
  68. break;
  69. case '"':
  70. out.append("&quot;");
  71. break;
  72. case ',':
  73. out.append(is_javascript ? R"(\\,)" : R"(\,)");
  74. break;
  75. default:
  76. out += c;
  77. }
  78. }
  79. return out;
  80. }
  81. bool EscapeForJS(const std::string& in_string,
  82. absl::optional<char> in_previous,
  83. std::string* out_string) {
  84. out_string->reserve(in_string.size() * 2);
  85. bool last_was_dollar = in_previous && in_previous.value() == '$';
  86. for (const char c : in_string) {
  87. switch (c) {
  88. case '`':
  89. out_string->append("\\`");
  90. break;
  91. case '{':
  92. // Do not allow "${".
  93. if (last_was_dollar)
  94. return false;
  95. *out_string += c;
  96. break;
  97. default:
  98. *out_string += c;
  99. }
  100. last_was_dollar = c == '$';
  101. }
  102. return true;
  103. }
  104. #if DCHECK_IS_ON()
  105. // Checks whether the replacement has an unsubstituted placeholder, e.g. "$1".
  106. bool HasUnexpectedPlaceholder(const std::string& key,
  107. const std::string& replacement) {
  108. // TODO(crbug.com/988031): Fix display aria labels.
  109. #if BUILDFLAG(IS_CHROMEOS_ASH)
  110. if (key == "displayResolutionText")
  111. return false;
  112. #endif
  113. return re2::RE2::PartialMatch(replacement, re2::RE2(R"(\$\d)"));
  114. }
  115. #endif // DCHECK_IS_ON()
  116. bool ReplaceTemplateExpressionsInternal(
  117. base::StringPiece source,
  118. const ui::TemplateReplacements& replacements,
  119. bool is_javascript,
  120. std::string* formatted,
  121. bool skip_unexpected_placeholder_check = false) {
  122. const size_t kValueLengthGuess = 16;
  123. formatted->reserve(source.length() + replacements.size() * kValueLengthGuess);
  124. // Two position markers are used as cursors through the |source|.
  125. // The |current_pos| will follow behind |next_pos|.
  126. size_t current_pos = 0;
  127. while (true) {
  128. size_t next_pos = source.find(kLeader, current_pos);
  129. if (next_pos == std::string::npos) {
  130. formatted->append(source.begin() + current_pos, source.end());
  131. break;
  132. }
  133. formatted->append(source.data() + current_pos, next_pos - current_pos);
  134. current_pos = next_pos + kLeaderSize;
  135. size_t context_end = source.find(kKeyOpen, current_pos);
  136. CHECK_NE(context_end, std::string::npos);
  137. std::string context(source.substr(current_pos, context_end - current_pos));
  138. current_pos = context_end + sizeof(kKeyOpen);
  139. size_t key_end = source.find(kKeyClose, current_pos);
  140. CHECK_NE(key_end, std::string::npos);
  141. std::string key(source.substr(current_pos, key_end - current_pos));
  142. CHECK(!key.empty());
  143. auto value = replacements.find(key);
  144. CHECK(value != replacements.end()) << "$i18n replacement key \"" << key
  145. << "\" not found";
  146. std::string replacement = value->second;
  147. if (is_javascript) {
  148. // Run JS escaping first.
  149. absl::optional<char> last = formatted->empty()
  150. ? absl::nullopt
  151. : absl::make_optional(formatted->back());
  152. std::string escaped_replacement;
  153. if (!EscapeForJS(replacement, last, &escaped_replacement))
  154. return false;
  155. replacement = escaped_replacement;
  156. }
  157. if (context.empty()) {
  158. // Make the replacement HTML safe.
  159. replacement = base::EscapeForHTML(replacement);
  160. } else if (context == "Raw") {
  161. // Pass the replacement through unchanged.
  162. } else if (context == "Polymer") {
  163. // Escape quotes and backslash for '$i18nPolymer{}' use (i.e. quoted).
  164. replacement = PolymerParameterEscape(replacement, is_javascript);
  165. } else {
  166. CHECK(false) << "Unknown context " << context;
  167. }
  168. #if DCHECK_IS_ON()
  169. // Replacements in Polymer WebUI may invoke JavaScript to replace string
  170. // placeholders. In other contexts, placeholders should already be replaced.
  171. if (!skip_unexpected_placeholder_check && context != "Polymer") {
  172. DCHECK(!HasUnexpectedPlaceholder(key, replacement))
  173. << "Dangling placeholder found in " << key;
  174. }
  175. #endif
  176. formatted->append(replacement);
  177. current_pos = key_end + sizeof(kKeyClose);
  178. }
  179. return true;
  180. }
  181. } // namespace
  182. namespace ui {
  183. void TemplateReplacementsFromDictionaryValue(
  184. const base::Value::Dict& dictionary,
  185. TemplateReplacements* replacements) {
  186. for (auto pair : dictionary) {
  187. const std::string* value = pair.second.GetIfString();
  188. if (value)
  189. (*replacements)[pair.first] = pair.second.GetString();
  190. }
  191. }
  192. bool ReplaceTemplateExpressionsInJS(base::StringPiece source,
  193. const TemplateReplacements& replacements,
  194. std::string* formatted) {
  195. CHECK(formatted->empty());
  196. base::StringPiece remaining = source;
  197. while (true) {
  198. // Replacement is only done in JS for the contents of HTML _template
  199. // strings.
  200. HtmlTemplate current_template = FindHtmlTemplate(remaining);
  201. // If there was an error finding a template, return false.
  202. if (current_template.type == INVALID)
  203. return false;
  204. // If there are no more templates, copy the remaining JS to the output and
  205. // return true.
  206. if (current_template.type == NONE) {
  207. formatted->append(std::string(remaining));
  208. return true;
  209. }
  210. // Copy the JS before the template to the output.
  211. formatted->append(std::string(remaining.substr(0, current_template.start)));
  212. // Retrieve the HTML portion of the source.
  213. base::StringPiece html_template =
  214. remaining.substr(current_template.start, current_template.length);
  215. // Perform replacements with JS escaping.
  216. std::string formatted_html;
  217. if (!ReplaceTemplateExpressionsInternal(html_template, replacements, true,
  218. &formatted_html)) {
  219. return false;
  220. }
  221. // Append the formatted HTML template.
  222. formatted->append(formatted_html);
  223. // Increment to the end of the current template.
  224. remaining =
  225. remaining.substr(current_template.start + current_template.length);
  226. }
  227. }
  228. std::string ReplaceTemplateExpressions(base::StringPiece source,
  229. const TemplateReplacements& replacements,
  230. bool skip_unexpected_placeholder_check) {
  231. std::string formatted;
  232. ReplaceTemplateExpressionsInternal(source, replacements, false, &formatted,
  233. skip_unexpected_placeholder_check);
  234. return formatted;
  235. }
  236. } // namespace ui