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