UNPKG

10.5 kBJavaScriptView Raw
1/**
2 * default settings
3 *
4 * @author Zongmin Lei<leizongmin@gmail.com>
5 */
6
7var FilterCSS = require("cssfilter").FilterCSS;
8var getDefaultCSSWhiteList = require("cssfilter").getDefaultWhiteList;
9var _ = require("./util");
10
11function getDefaultWhiteList() {
12 return {
13 a: ["target", "href", "title"],
14 abbr: ["title"],
15 address: [],
16 area: ["shape", "coords", "href", "alt"],
17 article: [],
18 aside: [],
19 audio: [
20 "autoplay",
21 "controls",
22 "crossorigin",
23 "loop",
24 "muted",
25 "preload",
26 "src",
27 ],
28 b: [],
29 bdi: ["dir"],
30 bdo: ["dir"],
31 big: [],
32 blockquote: ["cite"],
33 br: [],
34 caption: [],
35 center: [],
36 cite: [],
37 code: [],
38 col: ["align", "valign", "span", "width"],
39 colgroup: ["align", "valign", "span", "width"],
40 dd: [],
41 del: ["datetime"],
42 details: ["open"],
43 div: [],
44 dl: [],
45 dt: [],
46 em: [],
47 figcaption: [],
48 figure: [],
49 font: ["color", "size", "face"],
50 footer: [],
51 h1: [],
52 h2: [],
53 h3: [],
54 h4: [],
55 h5: [],
56 h6: [],
57 header: [],
58 hr: [],
59 i: [],
60 img: ["src", "alt", "title", "width", "height", "loading"],
61 ins: ["datetime"],
62 kbd: [],
63 li: [],
64 mark: [],
65 nav: [],
66 ol: [],
67 p: [],
68 pre: [],
69 s: [],
70 section: [],
71 small: [],
72 span: [],
73 sub: [],
74 summary: [],
75 sup: [],
76 strong: [],
77 strike: [],
78 table: ["width", "border", "align", "valign"],
79 tbody: ["align", "valign"],
80 td: ["width", "rowspan", "colspan", "align", "valign"],
81 tfoot: ["align", "valign"],
82 th: ["width", "rowspan", "colspan", "align", "valign"],
83 thead: ["align", "valign"],
84 tr: ["rowspan", "align", "valign"],
85 tt: [],
86 u: [],
87 ul: [],
88 video: [
89 "autoplay",
90 "controls",
91 "crossorigin",
92 "loop",
93 "muted",
94 "playsinline",
95 "poster",
96 "preload",
97 "src",
98 "height",
99 "width",
100 ],
101 };
102}
103
104var defaultCSSFilter = new FilterCSS();
105
106/**
107 * default onTag function
108 *
109 * @param {String} tag
110 * @param {String} html
111 * @param {Object} options
112 * @return {String}
113 */
114function onTag(tag, html, options) {
115 // do nothing
116}
117
118/**
119 * default onIgnoreTag function
120 *
121 * @param {String} tag
122 * @param {String} html
123 * @param {Object} options
124 * @return {String}
125 */
126function onIgnoreTag(tag, html, options) {
127 // do nothing
128}
129
130/**
131 * default onTagAttr function
132 *
133 * @param {String} tag
134 * @param {String} name
135 * @param {String} value
136 * @return {String}
137 */
138function onTagAttr(tag, name, value) {
139 // do nothing
140}
141
142/**
143 * default onIgnoreTagAttr function
144 *
145 * @param {String} tag
146 * @param {String} name
147 * @param {String} value
148 * @return {String}
149 */
150function onIgnoreTagAttr(tag, name, value) {
151 // do nothing
152}
153
154/**
155 * default escapeHtml function
156 *
157 * @param {String} html
158 */
159function escapeHtml(html) {
160 return html.replace(REGEXP_LT, "&lt;").replace(REGEXP_GT, "&gt;");
161}
162
163/**
164 * default safeAttrValue function
165 *
166 * @param {String} tag
167 * @param {String} name
168 * @param {String} value
169 * @param {Object} cssFilter
170 * @return {String}
171 */
172function safeAttrValue(tag, name, value, cssFilter) {
173 // unescape attribute value firstly
174 value = friendlyAttrValue(value);
175
176 if (name === "href" || name === "src") {
177 // filter `href` and `src` attribute
178 // only allow the value that starts with `http://` | `https://` | `mailto:` | `/` | `#`
179 value = _.trim(value);
180 if (value === "#") return "#";
181 if (
182 !(
183 value.substr(0, 7) === "http://" ||
184 value.substr(0, 8) === "https://" ||
185 value.substr(0, 7) === "mailto:" ||
186 value.substr(0, 4) === "tel:" ||
187 value.substr(0, 11) === "data:image/" ||
188 value.substr(0, 6) === "ftp://" ||
189 value.substr(0, 2) === "./" ||
190 value.substr(0, 3) === "../" ||
191 value[0] === "#" ||
192 value[0] === "/"
193 )
194 ) {
195 return "";
196 }
197 } else if (name === "background") {
198 // filter `background` attribute (maybe no use)
199 // `javascript:`
200 REGEXP_DEFAULT_ON_TAG_ATTR_4.lastIndex = 0;
201 if (REGEXP_DEFAULT_ON_TAG_ATTR_4.test(value)) {
202 return "";
203 }
204 } else if (name === "style") {
205 // `expression()`
206 REGEXP_DEFAULT_ON_TAG_ATTR_7.lastIndex = 0;
207 if (REGEXP_DEFAULT_ON_TAG_ATTR_7.test(value)) {
208 return "";
209 }
210 // `url()`
211 REGEXP_DEFAULT_ON_TAG_ATTR_8.lastIndex = 0;
212 if (REGEXP_DEFAULT_ON_TAG_ATTR_8.test(value)) {
213 REGEXP_DEFAULT_ON_TAG_ATTR_4.lastIndex = 0;
214 if (REGEXP_DEFAULT_ON_TAG_ATTR_4.test(value)) {
215 return "";
216 }
217 }
218 if (cssFilter !== false) {
219 cssFilter = cssFilter || defaultCSSFilter;
220 value = cssFilter.process(value);
221 }
222 }
223
224 // escape `<>"` before returns
225 value = escapeAttrValue(value);
226 return value;
227}
228
229// RegExp list
230var REGEXP_LT = /</g;
231var REGEXP_GT = />/g;
232var REGEXP_QUOTE = /"/g;
233var REGEXP_QUOTE_2 = /&quot;/g;
234var REGEXP_ATTR_VALUE_1 = /&#([a-zA-Z0-9]*);?/gim;
235var REGEXP_ATTR_VALUE_COLON = /&colon;?/gim;
236var REGEXP_ATTR_VALUE_NEWLINE = /&newline;?/gim;
237// var REGEXP_DEFAULT_ON_TAG_ATTR_3 = /\/\*|\*\//gm;
238var REGEXP_DEFAULT_ON_TAG_ATTR_4 =
239 /((j\s*a\s*v\s*a|v\s*b|l\s*i\s*v\s*e)\s*s\s*c\s*r\s*i\s*p\s*t\s*|m\s*o\s*c\s*h\s*a):/gi;
240// var REGEXP_DEFAULT_ON_TAG_ATTR_5 = /^[\s"'`]*(d\s*a\s*t\s*a\s*)\:/gi;
241// var REGEXP_DEFAULT_ON_TAG_ATTR_6 = /^[\s"'`]*(d\s*a\s*t\s*a\s*)\:\s*image\//gi;
242var REGEXP_DEFAULT_ON_TAG_ATTR_7 =
243 /e\s*x\s*p\s*r\s*e\s*s\s*s\s*i\s*o\s*n\s*\(.*/gi;
244var REGEXP_DEFAULT_ON_TAG_ATTR_8 = /u\s*r\s*l\s*\(.*/gi;
245
246/**
247 * escape double quote
248 *
249 * @param {String} str
250 * @return {String} str
251 */
252function escapeQuote(str) {
253 return str.replace(REGEXP_QUOTE, "&quot;");
254}
255
256/**
257 * unescape double quote
258 *
259 * @param {String} str
260 * @return {String} str
261 */
262function unescapeQuote(str) {
263 return str.replace(REGEXP_QUOTE_2, '"');
264}
265
266/**
267 * escape html entities
268 *
269 * @param {String} str
270 * @return {String}
271 */
272function escapeHtmlEntities(str) {
273 return str.replace(REGEXP_ATTR_VALUE_1, function replaceUnicode(str, code) {
274 return code[0] === "x" || code[0] === "X"
275 ? String.fromCharCode(parseInt(code.substr(1), 16))
276 : String.fromCharCode(parseInt(code, 10));
277 });
278}
279
280/**
281 * escape html5 new danger entities
282 *
283 * @param {String} str
284 * @return {String}
285 */
286function escapeDangerHtml5Entities(str) {
287 return str
288 .replace(REGEXP_ATTR_VALUE_COLON, ":")
289 .replace(REGEXP_ATTR_VALUE_NEWLINE, " ");
290}
291
292/**
293 * clear nonprintable characters
294 *
295 * @param {String} str
296 * @return {String}
297 */
298function clearNonPrintableCharacter(str) {
299 var str2 = "";
300 for (var i = 0, len = str.length; i < len; i++) {
301 str2 += str.charCodeAt(i) < 32 ? " " : str.charAt(i);
302 }
303 return _.trim(str2);
304}
305
306/**
307 * get friendly attribute value
308 *
309 * @param {String} str
310 * @return {String}
311 */
312function friendlyAttrValue(str) {
313 str = unescapeQuote(str);
314 str = escapeHtmlEntities(str);
315 str = escapeDangerHtml5Entities(str);
316 str = clearNonPrintableCharacter(str);
317 return str;
318}
319
320/**
321 * unescape attribute value
322 *
323 * @param {String} str
324 * @return {String}
325 */
326function escapeAttrValue(str) {
327 str = escapeQuote(str);
328 str = escapeHtml(str);
329 return str;
330}
331
332/**
333 * `onIgnoreTag` function for removing all the tags that are not in whitelist
334 */
335function onIgnoreTagStripAll() {
336 return "";
337}
338
339/**
340 * remove tag body
341 * specify a `tags` list, if the tag is not in the `tags` list then process by the specify function (optional)
342 *
343 * @param {array} tags
344 * @param {function} next
345 */
346function StripTagBody(tags, next) {
347 if (typeof next !== "function") {
348 next = function () {};
349 }
350
351 var isRemoveAllTag = !Array.isArray(tags);
352 function isRemoveTag(tag) {
353 if (isRemoveAllTag) return true;
354 return _.indexOf(tags, tag) !== -1;
355 }
356
357 var removeList = [];
358 var posStart = false;
359
360 return {
361 onIgnoreTag: function (tag, html, options) {
362 if (isRemoveTag(tag)) {
363 if (options.isClosing) {
364 var ret = "[/removed]";
365 var end = options.position + ret.length;
366 removeList.push([
367 posStart !== false ? posStart : options.position,
368 end,
369 ]);
370 posStart = false;
371 return ret;
372 } else {
373 if (!posStart) {
374 posStart = options.position;
375 }
376 return "[removed]";
377 }
378 } else {
379 return next(tag, html, options);
380 }
381 },
382 remove: function (html) {
383 var rethtml = "";
384 var lastPos = 0;
385 _.forEach(removeList, function (pos) {
386 rethtml += html.slice(lastPos, pos[0]);
387 lastPos = pos[1];
388 });
389 rethtml += html.slice(lastPos);
390 return rethtml;
391 },
392 };
393}
394
395/**
396 * remove html comments
397 *
398 * @param {String} html
399 * @return {String}
400 */
401function stripCommentTag(html) {
402 var retHtml = "";
403 var lastPos = 0;
404 while (lastPos < html.length) {
405 var i = html.indexOf("<!--", lastPos);
406 if (i === -1) {
407 retHtml += html.slice(lastPos);
408 break;
409 }
410 retHtml += html.slice(lastPos, i);
411 var j = html.indexOf("-->", i);
412 if (j === -1) {
413 break;
414 }
415 lastPos = j + 3;
416 }
417 return retHtml;
418}
419
420/**
421 * remove invisible characters
422 *
423 * @param {String} html
424 * @return {String}
425 */
426function stripBlankChar(html) {
427 var chars = html.split("");
428 chars = chars.filter(function (char) {
429 var c = char.charCodeAt(0);
430 if (c === 127) return false;
431 if (c <= 31) {
432 if (c === 10 || c === 13) return true;
433 return false;
434 }
435 return true;
436 });
437 return chars.join("");
438}
439
440exports.whiteList = getDefaultWhiteList();
441exports.getDefaultWhiteList = getDefaultWhiteList;
442exports.onTag = onTag;
443exports.onIgnoreTag = onIgnoreTag;
444exports.onTagAttr = onTagAttr;
445exports.onIgnoreTagAttr = onIgnoreTagAttr;
446exports.safeAttrValue = safeAttrValue;
447exports.escapeHtml = escapeHtml;
448exports.escapeQuote = escapeQuote;
449exports.unescapeQuote = unescapeQuote;
450exports.escapeHtmlEntities = escapeHtmlEntities;
451exports.escapeDangerHtml5Entities = escapeDangerHtml5Entities;
452exports.clearNonPrintableCharacter = clearNonPrintableCharacter;
453exports.friendlyAttrValue = friendlyAttrValue;
454exports.escapeAttrValue = escapeAttrValue;
455exports.onIgnoreTagStripAll = onIgnoreTagStripAll;
456exports.StripTagBody = StripTagBody;
457exports.stripCommentTag = stripCommentTag;
458exports.stripBlankChar = stripBlankChar;
459exports.attributeWrapSign = '"';
460exports.cssFilter = defaultCSSFilter;
461exports.getDefaultCSSWhiteList = getDefaultCSSWhiteList;