2 * @license AngularJS v1.3.20
3 * (c) 2010-2014 Google, Inc. http://angularjs.org
6 (function(window
, angular
, undefined) {'use strict';
8 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
9 * Any commits to this file should be reviewed with security in mind. *
10 * Changes to this file can potentially create security vulnerabilities. *
11 * An approval from 2 Core members with history of modifying *
12 * this file is required. *
14 * Does the change somehow allow for arbitrary javascript to be executed? *
15 * Or allows for someone to change the prototype of built-in objects? *
16 * Or gives undesired access to variables likes document or window? *
17 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
19 var $sanitizeMinErr
= angular
.$$minErr('$sanitize');
28 * The `ngSanitize` module provides functionality to sanitize HTML.
31 * <div doc-module-components="ngSanitize"></div>
33 * See {@link ngSanitize.$sanitize `$sanitize`} for usage.
37 * HTML Parser By Misko Hevery (misko@hevery.com)
38 * based on: HTML Parser By John Resig (ejohn.org)
39 * Original code by Erik Arvidsson, Mozilla Public License
40 * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
43 * htmlParser(htmlString, {
44 * start: function(tag, attrs, unary) {},
45 * end: function(tag) {},
46 * chars: function(text) {},
47 * comment: function(text) {}
59 * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are
60 * then serialized back to properly escaped html string. This means that no unsafe input can make
61 * it into the returned string, however, since our parser is more strict than a typical browser
62 * parser, it's possible that some obscure input, which would be recognized as valid HTML by a
63 * browser, won't make it through the sanitizer. The input may also contain SVG markup.
64 * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and
65 * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}.
67 * @param {string} html HTML input.
68 * @returns {string} Sanitized HTML.
71 <example module="sanitizeExample" deps="angular-sanitize.js">
72 <file name="index.html">
74 angular.module('sanitizeExample', ['ngSanitize'])
75 .controller('ExampleController', ['$scope', '$sce', function($scope, $sce) {
77 '<p style="color:blue">an html\n' +
78 '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
80 $scope.deliberatelyTrustDangerousSnippet = function() {
81 return $sce.trustAsHtml($scope.snippet);
85 <div ng-controller="ExampleController">
86 Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
94 <tr id="bind-html-with-sanitize">
96 <td>Automatically uses $sanitize</td>
97 <td><pre><div ng-bind-html="snippet"><br/></div></pre></td>
98 <td><div ng-bind-html="snippet"></div></td>
100 <tr id="bind-html-with-trust">
101 <td>ng-bind-html</td>
102 <td>Bypass $sanitize by explicitly trusting the dangerous value</td>
104 <pre><div ng-bind-html="deliberatelyTrustDangerousSnippet()">
107 <td><div ng-bind-html="deliberatelyTrustDangerousSnippet()"></div></td>
109 <tr id="bind-default">
111 <td>Automatically escapes</td>
112 <td><pre><div ng-bind="snippet"><br/></div></pre></td>
113 <td><div ng-bind="snippet"></div></td>
118 <file name="protractor.js" type="protractor">
119 it('should sanitize the html snippet by default', function() {
120 expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
121 toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
124 it('should inline raw snippet if bound to a trusted value', function() {
125 expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).
126 toBe("<p style=\"color:blue\">an html\n" +
127 "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
131 it('should escape snippet without any filter', function() {
132 expect(element(by.css('#bind-default div')).getInnerHtml()).
133 toBe("<p style=\"color:blue\">an html\n" +
134 "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
135 "snippet</p>");
138 it('should update', function() {
139 element(by.model('snippet')).clear();
140 element(by.model('snippet')).sendKeys('new <b onclick="alert(1)">text</b>');
141 expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
142 toBe('new <b>text</b>');
143 expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe(
144 'new <b onclick="alert(1)">text</b>');
145 expect(element(by.css('#bind-default div')).getInnerHtml()).toBe(
146 "new <b onclick=\"alert(1)\">text</b>");
151 function $SanitizeProvider() {
152 this.$get = ['$$sanitizeUri', function($$sanitizeUri
) {
153 return function(html
) {
155 htmlParser(html
, htmlSanitizeWriter(buf
, function(uri
, isImage
) {
156 return !/^unsafe/.test($$sanitizeUri(uri
, isImage
));
163 function sanitizeText(chars
) {
165 var writer
= htmlSanitizeWriter(buf
, angular
.noop
);
171 // Regular Expressions for parsing tags and attributes
172 var START_TAG_REGEXP
=
173 /^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,
174 END_TAG_REGEXP
= /^<\/\s*([\w:-]+)[^>]*>/,
175 ATTR_REGEXP
= /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,
176 BEGIN_TAG_REGEXP
= /^</,
177 BEGING_END_TAGE_REGEXP
= /^<\//,
178 COMMENT_REGEXP
= /<!--(.*?)-->/g,
179 DOCTYPE_REGEXP
= /<!DOCTYPE([^>]*?)>/i,
180 CDATA_REGEXP
= /<!\[CDATA\[(.*?)]]>/g,
181 SURROGATE_PAIR_REGEXP
= /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
182 // Match everything outside of normal chars and " (quote character)
183 NON_ALPHANUMERIC_REGEXP
= /([^\#-~| |!])/g;
186 // Good source of info about elements and attributes
187 // http://dev.w3.org/html5/spec/Overview.html#semantics
188 // http://simon.html5.org/html-elements
190 // Safe Void Elements - HTML5
191 // http://dev.w3.org/html5/spec/Overview.html#void-elements
192 var voidElements
= makeMap("area,br,col,hr,img,wbr");
194 // Elements that you can, intentionally, leave open (and which close themselves)
195 // http://dev.w3.org/html5/spec/Overview.html#optional-tags
196 var optionalEndTagBlockElements
= makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
197 optionalEndTagInlineElements
= makeMap("rp,rt"),
198 optionalEndTagElements
= angular
.extend({},
199 optionalEndTagInlineElements
,
200 optionalEndTagBlockElements
);
202 // Safe Block Elements - HTML5
203 var blockElements
= angular
.extend({}, optionalEndTagBlockElements
, makeMap("address,article," +
204 "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," +
205 "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul"));
207 // Inline Elements - HTML5
208 var inlineElements
= angular
.extend({}, optionalEndTagInlineElements
, makeMap("a,abbr,acronym,b," +
209 "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," +
210 "samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
213 // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements
214 var svgElements
= makeMap("animate,animateColor,animateMotion,animateTransform,circle,defs," +
215 "desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient," +
216 "line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,set," +
217 "stop,svg,switch,text,title,tspan,use");
219 // Special Elements (can contain anything)
220 var specialElements
= makeMap("script,style");
222 var validElements
= angular
.extend({},
226 optionalEndTagElements
,
229 //Attributes that have href and hence need to be sanitized
230 var uriAttrs
= makeMap("background,cite,href,longdesc,src,usemap,xlink:href");
232 var htmlAttrs
= makeMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
233 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' +
234 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' +
235 'scope,scrolling,shape,size,span,start,summary,target,title,type,' +
236 'valign,value,vspace,width');
238 // SVG attributes (without "id" and "name" attributes)
239 // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes
240 var svgAttrs
= makeMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
241 'attributeName,attributeType,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,' +
242 'color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,' +
243 'font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,' +
244 'gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,' +
245 'keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,' +
246 'markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,' +
247 'overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,' +
248 'repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,' +
249 'stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,' +
250 'stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,' +
251 'stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,' +
252 'underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,' +
253 'viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,' +
254 'xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,' +
257 var validAttrs
= angular
.extend({},
262 function makeMap(str
) {
263 var obj
= {}, items
= str
.split(','), i
;
264 for (i
= 0; i
< items
.length
; i
++) obj
[items
[i
]] = true;
271 * htmlParser(htmlString, {
272 * start: function(tag, attrs, unary) {},
273 * end: function(tag) {},
274 * chars: function(text) {},
275 * comment: function(text) {}
278 * @param {string} html string
279 * @param {object} handler
281 function htmlParser(html
, handler
) {
282 if (typeof html
!== 'string') {
283 if (html
=== null || typeof html
=== 'undefined') {
289 var index
, chars
, match
, stack
= [], last
= html
, text
;
290 stack
.last = function() { return stack
[stack
.length
- 1]; };
296 // Make sure we're not in a script or style element
297 if (!stack
.last() || !specialElements
[stack
.last()]) {
300 if (html
.indexOf("<!--") === 0) {
301 // comments containing -- are not allowed unless they terminate the comment
302 index
= html
.indexOf("--", 4);
304 if (index
>= 0 && html
.lastIndexOf("-->", index
) === index
) {
305 if (handler
.comment
) handler
.comment(html
.substring(4, index
));
306 html
= html
.substring(index
+ 3);
310 } else if (DOCTYPE_REGEXP
.test(html
)) {
311 match
= html
.match(DOCTYPE_REGEXP
);
314 html
= html
.replace(match
[0], '');
318 } else if (BEGING_END_TAGE_REGEXP
.test(html
)) {
319 match
= html
.match(END_TAG_REGEXP
);
322 html
= html
.substring(match
[0].length
);
323 match
[0].replace(END_TAG_REGEXP
, parseEndTag
);
328 } else if (BEGIN_TAG_REGEXP
.test(html
)) {
329 match
= html
.match(START_TAG_REGEXP
);
332 // We only have a valid start-tag if there is a '>'.
334 html
= html
.substring(match
[0].length
);
335 match
[0].replace(START_TAG_REGEXP
, parseStartTag
);
339 // no ending tag found --- this piece should be encoded as an entity.
341 html
= html
.substring(1);
346 index
= html
.indexOf("<");
348 text
+= index
< 0 ? html
: html
.substring(0, index
);
349 html
= index
< 0 ? "" : html
.substring(index
);
351 if (handler
.chars
) handler
.chars(decodeEntities(text
));
355 // IE versions 9 and 10 do not understand the regex '[^]', so using a workaround with [\W\w].
356 html
= html
.replace(new RegExp("([\\W\\w]*)<\\s*\\/\\s*" + stack
.last() + "[^>]*>", 'i'),
357 function(all
, text
) {
358 text
= text
.replace(COMMENT_REGEXP
, "$1").replace(CDATA_REGEXP
, "$1");
360 if (handler
.chars
) handler
.chars(decodeEntities(text
));
365 parseEndTag("", stack
.last());
369 throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " +
370 "of html: {0}", html
);
375 // Clean up any remaining tags
378 function parseStartTag(tag
, tagName
, rest
, unary
) {
379 tagName
= angular
.lowercase(tagName
);
380 if (blockElements
[tagName
]) {
381 while (stack
.last() && inlineElements
[stack
.last()]) {
382 parseEndTag("", stack
.last());
386 if (optionalEndTagElements
[tagName
] && stack
.last() == tagName
) {
387 parseEndTag("", tagName
);
390 unary
= voidElements
[tagName
] || !!unary
;
397 rest
.replace(ATTR_REGEXP
,
398 function(match
, name
, doubleQuotedValue
, singleQuotedValue
, unquotedValue
) {
399 var value
= doubleQuotedValue
404 attrs
[name
] = decodeEntities(value
);
406 if (handler
.start
) handler
.start(tagName
, attrs
, unary
);
409 function parseEndTag(tag
, tagName
) {
411 tagName
= angular
.lowercase(tagName
);
413 // Find the closest opened tag of the same type
414 for (pos
= stack
.length
- 1; pos
>= 0; pos
--)
415 if (stack
[pos
] == tagName
)
419 // Close all the open elements, up the stack
420 for (i
= stack
.length
- 1; i
>= pos
; i
--)
421 if (handler
.end
) handler
.end(stack
[i
]);
423 // Remove the open elements from the stack
429 var hiddenPre
=document
.createElement("pre");
431 * decodes all entities into regular string
433 * @returns {string} A string with decoded entities.
435 function decodeEntities(value
) {
436 if (!value
) { return ''; }
438 hiddenPre
.innerHTML
= value
.replace(/</g
,"<");
439 // innerText depends on styling as it doesn't display hidden elements.
440 // Therefore, it's better to use textContent not to cause unnecessary reflows.
441 return hiddenPre
.textContent
;
445 * Escapes all potentially dangerous characters, so that the
446 * resulting string can be safely inserted into attribute or
449 * @returns {string} escaped text
451 function encodeEntities(value
) {
453 replace(/&/g
, '&').
454 replace(SURROGATE_PAIR_REGEXP
, function(value
) {
455 var hi
= value
.charCodeAt(0);
456 var low
= value
.charCodeAt(1);
457 return '&#' + (((hi
- 0xD800) * 0x400) + (low
- 0xDC00) + 0x10000) + ';';
459 replace(NON_ALPHANUMERIC_REGEXP
, function(value
) {
460 return '&#' + value
.charCodeAt(0) + ';';
462 replace(/</g
, '<').
463 replace(/>/g
, '>');
467 * create an HTML/XML writer which writes to buffer
468 * @param {Array} buf use buf.jain('') to get out sanitized html string
469 * @returns {object} in the form of {
470 * start: function(tag, attrs, unary) {},
471 * end: function(tag) {},
472 * chars: function(text) {},
473 * comment: function(text) {}
476 function htmlSanitizeWriter(buf
, uriValidator
) {
478 var out
= angular
.bind(buf
, buf
.push
);
480 start: function(tag
, attrs
, unary
) {
481 tag
= angular
.lowercase(tag
);
482 if (!ignore
&& specialElements
[tag
]) {
485 if (!ignore
&& validElements
[tag
] === true) {
488 angular
.forEach(attrs
, function(value
, key
) {
489 var lkey
=angular
.lowercase(key
);
490 var isImage
= (tag
=== 'img' && lkey
=== 'src') || (lkey
=== 'background');
491 if (validAttrs
[lkey
] === true &&
492 (uriAttrs
[lkey
] !== true || uriValidator(value
, isImage
))) {
496 out(encodeEntities(value
));
500 out(unary
? '/>' : '>');
504 tag
= angular
.lowercase(tag
);
505 if (!ignore
&& validElements
[tag
] === true) {
514 chars: function(chars
) {
516 out(encodeEntities(chars
));
523 // define ngSanitize module and register $sanitize service
524 angular
.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider
);
526 /* global sanitizeText: false */
534 * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
535 * plain email address links.
537 * Requires the {@link ngSanitize `ngSanitize`} module to be installed.
539 * @param {string} text Input text.
540 * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in.
541 * @returns {string} Html-linkified text.
544 <span ng-bind-html="linky_expression | linky"></span>
547 <example module="linkyExample" deps="angular-sanitize.js">
548 <file name="index.html">
550 angular.module('linkyExample', ['ngSanitize'])
551 .controller('ExampleController', ['$scope', function($scope) {
553 'Pretty text with some links:\n'+
554 'http://angularjs.org/,\n'+
555 'mailto:us@somewhere.org,\n'+
556 'another@somewhere.org,\n'+
557 'and one more: ftp://127.0.0.1/.';
558 $scope.snippetWithTarget = 'http://angularjs.org/';
561 <div ng-controller="ExampleController">
562 Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
569 <tr id="linky-filter">
570 <td>linky filter</td>
572 <pre><div ng-bind-html="snippet | linky"><br></div></pre>
575 <div ng-bind-html="snippet | linky"></div>
578 <tr id="linky-target">
579 <td>linky target</td>
581 <pre><div ng-bind-html="snippetWithTarget | linky:'_blank'"><br></div></pre>
584 <div ng-bind-html="snippetWithTarget | linky:'_blank'"></div>
587 <tr id="escaped-html">
589 <td><pre><div ng-bind="snippet"><br></div></pre></td>
590 <td><div ng-bind="snippet"></div></td>
594 <file name="protractor.js" type="protractor">
595 it('should linkify the snippet with urls', function() {
596 expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
597 toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' +
598 'another@somewhere.org, and one more: ftp://127.0.0.1/.');
599 expect(element.all(by.css('#linky-filter a')).count()).toEqual(4);
602 it('should not linkify snippet without the linky filter', function() {
603 expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()).
604 toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' +
605 'another@somewhere.org, and one more: ftp://127.0.0.1/.');
606 expect(element.all(by.css('#escaped-html a')).count()).toEqual(0);
609 it('should update', function() {
610 element(by.model('snippet')).clear();
611 element(by.model('snippet')).sendKeys('new http://link.');
612 expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
613 toBe('new http://link.');
614 expect(element.all(by.css('#linky-filter a')).count()).toEqual(1);
615 expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText())
616 .toBe('new http://link.');
619 it('should work with the target property', function() {
620 expect(element(by.id('linky-target')).
621 element(by.binding("snippetWithTarget | linky:'_blank'")).getText()).
622 toBe('http://angularjs.org/');
623 expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
628 angular
.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize
) {
629 var LINKY_URL_REGEXP
=
630 /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"”’]/i,
631 MAILTO_REGEXP
= /^mailto:/i;
633 return function(text
, target
) {
634 if (!text
) return text
;
640 while ((match
= raw
.match(LINKY_URL_REGEXP
))) {
641 // We can not end in these as they are sometimes found at the end of the sentence
643 // if we did not match ftp/http/www/mailto then assume mailto
644 if (!match
[2] && !match
[4]) {
645 url
= (match
[3] ? 'http://' : 'mailto:') + url
;
648 addText(raw
.substr(0, i
));
649 addLink(url
, match
[0].replace(MAILTO_REGEXP
, ''));
650 raw
= raw
.substring(i
+ match
[0].length
);
653 return $sanitize(html
.join(''));
655 function addText(text
) {
659 html
.push(sanitizeText(text
));
662 function addLink(url
, text
) {
664 if (angular
.isDefined(target
)) {
665 html
.push('target="',
670 url
.replace(/"/g, '"'),
679 })(window, window.angular);