angular-sanitize.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806
  1. /**
  2. * @license AngularJS v1.6.5
  3. * (c) 2010-2017 Google, Inc. http://angularjs.org
  4. * License: MIT
  5. */
  6. (function (window, angular) {
  7. '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. *
  13. * *
  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. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
  18. var $sanitizeMinErr = angular.$$minErr('$sanitize');
  19. var bind;
  20. var extend;
  21. var forEach;
  22. var isDefined;
  23. var lowercase;
  24. var noop;
  25. var nodeContains;
  26. var htmlParser;
  27. var htmlSanitizeWriter;
  28. /**
  29. * @ngdoc module
  30. * @name ngSanitize
  31. * @description
  32. *
  33. * # ngSanitize
  34. *
  35. * The `ngSanitize` module provides functionality to sanitize HTML.
  36. *
  37. *
  38. * <div doc-module-components="ngSanitize"></div>
  39. *
  40. * See {@link ngSanitize.$sanitize `$sanitize`} for usage.
  41. */
  42. /**
  43. * @ngdoc service
  44. * @name $sanitize
  45. * @kind function
  46. *
  47. * @description
  48. * Sanitizes an html string by stripping all potentially dangerous tokens.
  49. *
  50. * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are
  51. * then serialized back to properly escaped html string. This means that no unsafe input can make
  52. * it into the returned string.
  53. *
  54. * The whitelist for URL sanitization of attribute values is configured using the functions
  55. * `aHrefSanitizationWhitelist` and `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider
  56. * `$compileProvider`}.
  57. *
  58. * The input may also contain SVG markup if this is enabled via {@link $sanitizeProvider}.
  59. *
  60. * @param {string} html HTML input.
  61. * @returns {string} Sanitized HTML.
  62. *
  63. * @example
  64. <example module="sanitizeExample" deps="angular-sanitize.js" name="sanitize-service">
  65. <file name="index.html">
  66. <script>
  67. angular.module('sanitizeExample', ['ngSanitize'])
  68. .controller('ExampleController', ['$scope', '$sce', function($scope, $sce) {
  69. $scope.snippet =
  70. '<p style="color:blue">an html\n' +
  71. '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
  72. 'snippet</p>';
  73. $scope.deliberatelyTrustDangerousSnippet = function() {
  74. return $sce.trustAsHtml($scope.snippet);
  75. };
  76. }]);
  77. </script>
  78. <div ng-controller="ExampleController">
  79. Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
  80. <table>
  81. <tr>
  82. <td>Directive</td>
  83. <td>How</td>
  84. <td>Source</td>
  85. <td>Rendered</td>
  86. </tr>
  87. <tr id="bind-html-with-sanitize">
  88. <td>ng-bind-html</td>
  89. <td>Automatically uses $sanitize</td>
  90. <td><pre>&lt;div ng-bind-html="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
  91. <td><div ng-bind-html="snippet"></div></td>
  92. </tr>
  93. <tr id="bind-html-with-trust">
  94. <td>ng-bind-html</td>
  95. <td>Bypass $sanitize by explicitly trusting the dangerous value</td>
  96. <td>
  97. <pre>&lt;div ng-bind-html="deliberatelyTrustDangerousSnippet()"&gt;
  98. &lt;/div&gt;</pre>
  99. </td>
  100. <td><div ng-bind-html="deliberatelyTrustDangerousSnippet()"></div></td>
  101. </tr>
  102. <tr id="bind-default">
  103. <td>ng-bind</td>
  104. <td>Automatically escapes</td>
  105. <td><pre>&lt;div ng-bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
  106. <td><div ng-bind="snippet"></div></td>
  107. </tr>
  108. </table>
  109. </div>
  110. </file>
  111. <file name="protractor.js" type="protractor">
  112. it('should sanitize the html snippet by default', function() {
  113. expect(element(by.css('#bind-html-with-sanitize div')).getAttribute('innerHTML')).
  114. toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
  115. });
  116. it('should inline raw snippet if bound to a trusted value', function() {
  117. expect(element(by.css('#bind-html-with-trust div')).getAttribute('innerHTML')).
  118. toBe("<p style=\"color:blue\">an html\n" +
  119. "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
  120. "snippet</p>");
  121. });
  122. it('should escape snippet without any filter', function() {
  123. expect(element(by.css('#bind-default div')).getAttribute('innerHTML')).
  124. toBe("&lt;p style=\"color:blue\"&gt;an html\n" +
  125. "&lt;em onmouseover=\"this.textContent='PWN3D!'\"&gt;click here&lt;/em&gt;\n" +
  126. "snippet&lt;/p&gt;");
  127. });
  128. it('should update', function() {
  129. element(by.model('snippet')).clear();
  130. element(by.model('snippet')).sendKeys('new <b onclick="alert(1)">text</b>');
  131. expect(element(by.css('#bind-html-with-sanitize div')).getAttribute('innerHTML')).
  132. toBe('new <b>text</b>');
  133. expect(element(by.css('#bind-html-with-trust div')).getAttribute('innerHTML')).toBe(
  134. 'new <b onclick="alert(1)">text</b>');
  135. expect(element(by.css('#bind-default div')).getAttribute('innerHTML')).toBe(
  136. "new &lt;b onclick=\"alert(1)\"&gt;text&lt;/b&gt;");
  137. });
  138. </file>
  139. </example>
  140. */
  141. /**
  142. * @ngdoc provider
  143. * @name $sanitizeProvider
  144. * @this
  145. *
  146. * @description
  147. * Creates and configures {@link $sanitize} instance.
  148. */
  149. function $SanitizeProvider() {
  150. var svgEnabled = false;
  151. this.$get = ['$$sanitizeUri', function ($$sanitizeUri) {
  152. if (svgEnabled) {
  153. extend(validElements, svgElements);
  154. }
  155. return function (html) {
  156. var buf = [];
  157. htmlParser(html, htmlSanitizeWriter(buf, function (uri, isImage) {
  158. return !/^unsafe:/.test($$sanitizeUri(uri, isImage));
  159. }));
  160. return buf.join('');
  161. };
  162. }];
  163. /**
  164. * @ngdoc method
  165. * @name $sanitizeProvider#enableSvg
  166. * @kind function
  167. *
  168. * @description
  169. * Enables a subset of svg to be supported by the sanitizer.
  170. *
  171. * <div class="alert alert-warning">
  172. * <p>By enabling this setting without taking other precautions, you might expose your
  173. * application to click-hijacking attacks. In these attacks, sanitized svg elements could be positioned
  174. * outside of the containing element and be rendered over other elements on the page (e.g. a login
  175. * link). Such behavior can then result in phishing incidents.</p>
  176. *
  177. * <p>To protect against these, explicitly setup `overflow: hidden` css rule for all potential svg
  178. * tags within the sanitized content:</p>
  179. *
  180. * <br>
  181. *
  182. * <pre><code>
  183. * .rootOfTheIncludedContent svg {
  184. * overflow: hidden !important;
  185. * }
  186. * </code></pre>
  187. * </div>
  188. *
  189. * @param {boolean=} flag Enable or disable SVG support in the sanitizer.
  190. * @returns {boolean|ng.$sanitizeProvider} Returns the currently configured value if called
  191. * without an argument or self for chaining otherwise.
  192. */
  193. this.enableSvg = function (enableSvg) {
  194. if (isDefined(enableSvg)) {
  195. svgEnabled = enableSvg;
  196. return this;
  197. } else {
  198. return svgEnabled;
  199. }
  200. };
  201. //////////////////////////////////////////////////////////////////////////////////////////////////
  202. // Private stuff
  203. //////////////////////////////////////////////////////////////////////////////////////////////////
  204. bind = angular.bind;
  205. extend = angular.extend;
  206. forEach = angular.forEach;
  207. isDefined = angular.isDefined;
  208. lowercase = angular.lowercase;
  209. noop = angular.noop;
  210. htmlParser = htmlParserImpl;
  211. htmlSanitizeWriter = htmlSanitizeWriterImpl;
  212. nodeContains = window.Node.prototype.contains || /** @this */ function (arg) {
  213. // eslint-disable-next-line no-bitwise
  214. return !!(this.compareDocumentPosition(arg) & 16);
  215. };
  216. // Regular Expressions for parsing tags and attributes
  217. var SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
  218. // Match everything outside of normal chars and " (quote character)
  219. NON_ALPHANUMERIC_REGEXP = /([^#-~ |!])/g;
  220. // Good source of info about elements and attributes
  221. // http://dev.w3.org/html5/spec/Overview.html#semantics
  222. // http://simon.html5.org/html-elements
  223. // Safe Void Elements - HTML5
  224. // http://dev.w3.org/html5/spec/Overview.html#void-elements
  225. var voidElements = toMap('area,br,col,hr,img,wbr');
  226. // Elements that you can, intentionally, leave open (and which close themselves)
  227. // http://dev.w3.org/html5/spec/Overview.html#optional-tags
  228. var optionalEndTagBlockElements = toMap('colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr'),
  229. optionalEndTagInlineElements = toMap('rp,rt'),
  230. optionalEndTagElements = extend({},
  231. optionalEndTagInlineElements,
  232. optionalEndTagBlockElements);
  233. // Safe Block Elements - HTML5
  234. var blockElements = extend({}, optionalEndTagBlockElements, toMap('address,article,' +
  235. 'aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,' +
  236. 'h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul'));
  237. // Inline Elements - HTML5
  238. var inlineElements = extend({}, optionalEndTagInlineElements, toMap('a,abbr,acronym,b,' +
  239. 'bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,' +
  240. 'samp,small,span,strike,strong,sub,sup,time,tt,u,var'));
  241. // SVG Elements
  242. // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements
  243. // Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted.
  244. // They can potentially allow for arbitrary javascript to be executed. See #11290
  245. var svgElements = toMap('circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,' +
  246. 'hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,' +
  247. 'radialGradient,rect,stop,svg,switch,text,title,tspan');
  248. // Blocked Elements (will be stripped)
  249. var blockedElements = toMap('script,style');
  250. var validElements = extend({},
  251. voidElements,
  252. blockElements,
  253. inlineElements,
  254. optionalEndTagElements);
  255. //Attributes that have href and hence need to be sanitized
  256. var uriAttrs = toMap('background,cite,href,longdesc,src,xlink:href');
  257. var htmlAttrs = toMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
  258. 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' +
  259. 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' +
  260. 'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' +
  261. 'valign,value,vspace,width');
  262. // SVG attributes (without "id" and "name" attributes)
  263. // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes
  264. var svgAttrs = toMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
  265. 'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' +
  266. 'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' +
  267. 'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' +
  268. 'height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,' +
  269. 'marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,' +
  270. 'max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,' +
  271. 'path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,' +
  272. 'requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,' +
  273. 'stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,' +
  274. 'stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,' +
  275. 'stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,' +
  276. 'underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,' +
  277. 'width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,' +
  278. 'xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan', true);
  279. var validAttrs = extend({},
  280. uriAttrs,
  281. svgAttrs,
  282. htmlAttrs);
  283. function toMap(str, lowercaseKeys) {
  284. var obj = {}, items = str.split(','), i;
  285. for (i = 0; i < items.length; i++) {
  286. obj[lowercaseKeys ? lowercase(items[i]) : items[i]] = true;
  287. }
  288. return obj;
  289. }
  290. /**
  291. * Create an inert document that contains the dirty HTML that needs sanitizing
  292. * Depending upon browser support we use one of three strategies for doing this.
  293. * Support: Safari 10.x -> XHR strategy
  294. * Support: Firefox -> DomParser strategy
  295. */
  296. var getInertBodyElement /* function(html: string): HTMLBodyElement */ = (function (window, document) {
  297. var inertDocument;
  298. if (document && document.implementation) {
  299. inertDocument = document.implementation.createHTMLDocument('inert');
  300. } else {
  301. throw $sanitizeMinErr('noinert', 'Can\'t create an inert html document');
  302. }
  303. var inertBodyElement = (inertDocument.documentElement || inertDocument.getDocumentElement()).querySelector('body');
  304. // Check for the Safari 10.1 bug - which allows JS to run inside the SVG G element
  305. inertBodyElement.innerHTML = '<svg><g onload="this.parentNode.remove()"></g></svg>';
  306. if (!inertBodyElement.querySelector('svg')) {
  307. return getInertBodyElement_XHR;
  308. } else {
  309. // Check for the Firefox bug - which prevents the inner img JS from being sanitized
  310. inertBodyElement.innerHTML = '<svg><p><style><img src="</style><img src=x onerror=alert(1)//">';
  311. if (inertBodyElement.querySelector('svg img')) {
  312. return getInertBodyElement_DOMParser;
  313. } else {
  314. return getInertBodyElement_InertDocument;
  315. }
  316. }
  317. function getInertBodyElement_XHR(html) {
  318. // We add this dummy element to ensure that the rest of the content is parsed as expected
  319. // e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the `<head>` tag.
  320. html = '<remove></remove>' + html;
  321. try {
  322. html = encodeURI(html);
  323. } catch (e) {
  324. return undefined;
  325. }
  326. var xhr = new window.XMLHttpRequest();
  327. xhr.responseType = 'document';
  328. xhr.open('GET', 'data:text/html;charset=utf-8,' + html, false);
  329. xhr.send(null);
  330. var body = xhr.response.body;
  331. body.firstChild.remove();
  332. return body;
  333. }
  334. function getInertBodyElement_DOMParser(html) {
  335. // We add this dummy element to ensure that the rest of the content is parsed as expected
  336. // e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the `<head>` tag.
  337. html = '<remove></remove>' + html;
  338. try {
  339. var body = new window.DOMParser().parseFromString(html, 'text/html').body;
  340. body.firstChild.remove();
  341. return body;
  342. } catch (e) {
  343. return undefined;
  344. }
  345. }
  346. function getInertBodyElement_InertDocument(html) {
  347. inertBodyElement.innerHTML = html;
  348. // Support: IE 9-11 only
  349. // strip custom-namespaced attributes on IE<=11
  350. if (document.documentMode) {
  351. stripCustomNsAttrs(inertBodyElement);
  352. }
  353. return inertBodyElement;
  354. }
  355. })(window, window.document);
  356. /**
  357. * @example
  358. * htmlParser(htmlString, {
  359. * start: function(tag, attrs) {},
  360. * end: function(tag) {},
  361. * chars: function(text) {},
  362. * comment: function(text) {}
  363. * });
  364. *
  365. * @param {string} html string
  366. * @param {object} handler
  367. */
  368. function htmlParserImpl(html, handler) {
  369. if (html === null || html === undefined) {
  370. html = '';
  371. } else if (typeof html !== 'string') {
  372. html = '' + html;
  373. }
  374. var inertBodyElement = getInertBodyElement(html);
  375. if (!inertBodyElement) return '';
  376. //mXSS protection
  377. var mXSSAttempts = 5;
  378. do {
  379. if (mXSSAttempts === 0) {
  380. throw $sanitizeMinErr('uinput', 'Failed to sanitize html because the input is unstable');
  381. }
  382. mXSSAttempts--;
  383. // trigger mXSS if it is going to happen by reading and writing the innerHTML
  384. html = inertBodyElement.innerHTML;
  385. inertBodyElement = getInertBodyElement(html);
  386. } while (html !== inertBodyElement.innerHTML);
  387. var node = inertBodyElement.firstChild;
  388. while (node) {
  389. switch (node.nodeType) {
  390. case 1: // ELEMENT_NODE
  391. handler.start(node.nodeName.toLowerCase(), attrToMap(node.attributes));
  392. break;
  393. case 3: // TEXT NODE
  394. handler.chars(node.textContent);
  395. break;
  396. }
  397. var nextNode;
  398. if (!(nextNode = node.firstChild)) {
  399. if (node.nodeType === 1) {
  400. handler.end(node.nodeName.toLowerCase());
  401. }
  402. nextNode = getNonDescendant('nextSibling', node);
  403. if (!nextNode) {
  404. while (nextNode == null) {
  405. node = getNonDescendant('parentNode', node);
  406. if (node === inertBodyElement) break;
  407. nextNode = getNonDescendant('nextSibling', node);
  408. if (node.nodeType === 1) {
  409. handler.end(node.nodeName.toLowerCase());
  410. }
  411. }
  412. }
  413. }
  414. node = nextNode;
  415. }
  416. while ((node = inertBodyElement.firstChild)) {
  417. inertBodyElement.removeChild(node);
  418. }
  419. }
  420. function attrToMap(attrs) {
  421. var map = {};
  422. for (var i = 0, ii = attrs.length; i < ii; i++) {
  423. var attr = attrs[i];
  424. map[attr.name] = attr.value;
  425. }
  426. return map;
  427. }
  428. /**
  429. * Escapes all potentially dangerous characters, so that the
  430. * resulting string can be safely inserted into attribute or
  431. * element text.
  432. * @param value
  433. * @returns {string} escaped text
  434. */
  435. function encodeEntities(value) {
  436. return value.replace(/&/g, '&amp;').replace(SURROGATE_PAIR_REGEXP, function (value) {
  437. var hi = value.charCodeAt(0);
  438. var low = value.charCodeAt(1);
  439. return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
  440. }).replace(NON_ALPHANUMERIC_REGEXP, function (value) {
  441. return '&#' + value.charCodeAt(0) + ';';
  442. }).replace(/</g, '&lt;').replace(/>/g, '&gt;');
  443. }
  444. /**
  445. * create an HTML/XML writer which writes to buffer
  446. * @param {Array} buf use buf.join('') to get out sanitized html string
  447. * @returns {object} in the form of {
  448. * start: function(tag, attrs) {},
  449. * end: function(tag) {},
  450. * chars: function(text) {},
  451. * comment: function(text) {}
  452. * }
  453. */
  454. function htmlSanitizeWriterImpl(buf, uriValidator) {
  455. var ignoreCurrentElement = false;
  456. var out = bind(buf, buf.push);
  457. return {
  458. start: function (tag, attrs) {
  459. tag = lowercase(tag);
  460. if (!ignoreCurrentElement && blockedElements[tag]) {
  461. ignoreCurrentElement = tag;
  462. }
  463. if (!ignoreCurrentElement && validElements[tag] === true) {
  464. out('<');
  465. out(tag);
  466. forEach(attrs, function (value, key) {
  467. var lkey = lowercase(key);
  468. var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');
  469. if (validAttrs[lkey] === true &&
  470. (uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
  471. out(' ');
  472. out(key);
  473. out('="');
  474. out(encodeEntities(value));
  475. out('"');
  476. }
  477. });
  478. out('>');
  479. }
  480. },
  481. end: function (tag) {
  482. tag = lowercase(tag);
  483. if (!ignoreCurrentElement && validElements[tag] === true && voidElements[tag] !== true) {
  484. out('</');
  485. out(tag);
  486. out('>');
  487. }
  488. // eslint-disable-next-line eqeqeq
  489. if (tag == ignoreCurrentElement) {
  490. ignoreCurrentElement = false;
  491. }
  492. },
  493. chars: function (chars) {
  494. if (!ignoreCurrentElement) {
  495. out(encodeEntities(chars));
  496. }
  497. }
  498. };
  499. }
  500. /**
  501. * When IE9-11 comes across an unknown namespaced attribute e.g. 'xlink:foo' it adds 'xmlns:ns1' attribute to declare
  502. * ns1 namespace and prefixes the attribute with 'ns1' (e.g. 'ns1:xlink:foo'). This is undesirable since we don't want
  503. * to allow any of these custom attributes. This method strips them all.
  504. *
  505. * @param node Root element to process
  506. */
  507. function stripCustomNsAttrs(node) {
  508. while (node) {
  509. if (node.nodeType === window.Node.ELEMENT_NODE) {
  510. var attrs = node.attributes;
  511. for (var i = 0, l = attrs.length; i < l; i++) {
  512. var attrNode = attrs[i];
  513. var attrName = attrNode.name.toLowerCase();
  514. if (attrName === 'xmlns:ns1' || attrName.lastIndexOf('ns1:', 0) === 0) {
  515. node.removeAttributeNode(attrNode);
  516. i--;
  517. l--;
  518. }
  519. }
  520. }
  521. var nextNode = node.firstChild;
  522. if (nextNode) {
  523. stripCustomNsAttrs(nextNode);
  524. }
  525. node = getNonDescendant('nextSibling', node);
  526. }
  527. }
  528. function getNonDescendant(propName, node) {
  529. // An element is clobbered if its `propName` property points to one of its descendants
  530. var nextNode = node[propName];
  531. if (nextNode && nodeContains.call(node, nextNode)) {
  532. throw $sanitizeMinErr('elclob', 'Failed to sanitize html because the element is clobbered: {0}', node.outerHTML || node.outerText);
  533. }
  534. return nextNode;
  535. }
  536. }
  537. function sanitizeText(chars) {
  538. var buf = [];
  539. var writer = htmlSanitizeWriter(buf, noop);
  540. writer.chars(chars);
  541. return buf.join('');
  542. }
  543. // define ngSanitize module and register $sanitize service
  544. angular.module('ngSanitize', [])
  545. .provider('$sanitize', $SanitizeProvider)
  546. .info({angularVersion: '1.6.5'});
  547. /**
  548. * @ngdoc filter
  549. * @name linky
  550. * @kind function
  551. *
  552. * @description
  553. * Finds links in text input and turns them into html links. Supports `http/https/ftp/mailto` and
  554. * plain email address links.
  555. *
  556. * Requires the {@link ngSanitize `ngSanitize`} module to be installed.
  557. *
  558. * @param {string} text Input text.
  559. * @param {string} target Window (`_blank|_self|_parent|_top`) or named frame to open links in.
  560. * @param {object|function(url)} [attributes] Add custom attributes to the link element.
  561. *
  562. * Can be one of:
  563. *
  564. * - `object`: A map of attributes
  565. * - `function`: Takes the url as a parameter and returns a map of attributes
  566. *
  567. * If the map of attributes contains a value for `target`, it overrides the value of
  568. * the target parameter.
  569. *
  570. *
  571. * @returns {string} Html-linkified and {@link $sanitize sanitized} text.
  572. *
  573. * @usage
  574. <span ng-bind-html="linky_expression | linky"></span>
  575. *
  576. * @example
  577. <example module="linkyExample" deps="angular-sanitize.js" name="linky-filter">
  578. <file name="index.html">
  579. <div ng-controller="ExampleController">
  580. Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
  581. <table>
  582. <tr>
  583. <th>Filter</th>
  584. <th>Source</th>
  585. <th>Rendered</th>
  586. </tr>
  587. <tr id="linky-filter">
  588. <td>linky filter</td>
  589. <td>
  590. <pre>&lt;div ng-bind-html="snippet | linky"&gt;<br>&lt;/div&gt;</pre>
  591. </td>
  592. <td>
  593. <div ng-bind-html="snippet | linky"></div>
  594. </td>
  595. </tr>
  596. <tr id="linky-target">
  597. <td>linky target</td>
  598. <td>
  599. <pre>&lt;div ng-bind-html="snippetWithSingleURL | linky:'_blank'"&gt;<br>&lt;/div&gt;</pre>
  600. </td>
  601. <td>
  602. <div ng-bind-html="snippetWithSingleURL | linky:'_blank'"></div>
  603. </td>
  604. </tr>
  605. <tr id="linky-custom-attributes">
  606. <td>linky custom attributes</td>
  607. <td>
  608. <pre>&lt;div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"&gt;<br>&lt;/div&gt;</pre>
  609. </td>
  610. <td>
  611. <div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"></div>
  612. </td>
  613. </tr>
  614. <tr id="escaped-html">
  615. <td>no filter</td>
  616. <td><pre>&lt;div ng-bind="snippet"&gt;<br>&lt;/div&gt;</pre></td>
  617. <td><div ng-bind="snippet"></div></td>
  618. </tr>
  619. </table>
  620. </file>
  621. <file name="script.js">
  622. angular.module('linkyExample', ['ngSanitize'])
  623. .controller('ExampleController', ['$scope', function($scope) {
  624. $scope.snippet =
  625. 'Pretty text with some links:\n' +
  626. 'http://angularjs.org/,\n' +
  627. 'mailto:us@somewhere.org,\n' +
  628. 'another@somewhere.org,\n' +
  629. 'and one more: ftp://127.0.0.1/.';
  630. $scope.snippetWithSingleURL = 'http://angularjs.org/';
  631. }]);
  632. </file>
  633. <file name="protractor.js" type="protractor">
  634. it('should linkify the snippet with urls', function() {
  635. expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
  636. toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' +
  637. 'another@somewhere.org, and one more: ftp://127.0.0.1/.');
  638. expect(element.all(by.css('#linky-filter a')).count()).toEqual(4);
  639. });
  640. it('should not linkify snippet without the linky filter', function() {
  641. expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()).
  642. toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' +
  643. 'another@somewhere.org, and one more: ftp://127.0.0.1/.');
  644. expect(element.all(by.css('#escaped-html a')).count()).toEqual(0);
  645. });
  646. it('should update', function() {
  647. element(by.model('snippet')).clear();
  648. element(by.model('snippet')).sendKeys('new http://link.');
  649. expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
  650. toBe('new http://link.');
  651. expect(element.all(by.css('#linky-filter a')).count()).toEqual(1);
  652. expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText())
  653. .toBe('new http://link.');
  654. });
  655. it('should work with the target property', function() {
  656. expect(element(by.id('linky-target')).
  657. element(by.binding("snippetWithSingleURL | linky:'_blank'")).getText()).
  658. toBe('http://angularjs.org/');
  659. expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
  660. });
  661. it('should optionally add custom attributes', function() {
  662. expect(element(by.id('linky-custom-attributes')).
  663. element(by.binding("snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}")).getText()).
  664. toBe('http://angularjs.org/');
  665. expect(element(by.css('#linky-custom-attributes a')).getAttribute('rel')).toEqual('nofollow');
  666. });
  667. </file>
  668. </example>
  669. */
  670. angular.module('ngSanitize').filter('linky', ['$sanitize', function ($sanitize) {
  671. var LINKY_URL_REGEXP =
  672. /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i,
  673. MAILTO_REGEXP = /^mailto:/i;
  674. var linkyMinErr = angular.$$minErr('linky');
  675. var isDefined = angular.isDefined;
  676. var isFunction = angular.isFunction;
  677. var isObject = angular.isObject;
  678. var isString = angular.isString;
  679. return function (text, target, attributes) {
  680. if (text == null || text === '') return text;
  681. if (!isString(text)) throw linkyMinErr('notstring', 'Expected string but received: {0}', text);
  682. var attributesFn =
  683. isFunction(attributes) ? attributes :
  684. isObject(attributes) ? function getAttributesObject() {
  685. return attributes;
  686. } :
  687. function getEmptyAttributesObject() {
  688. return {};
  689. };
  690. var match;
  691. var raw = text;
  692. var html = [];
  693. var url;
  694. var i;
  695. while ((match = raw.match(LINKY_URL_REGEXP))) {
  696. // We can not end in these as they are sometimes found at the end of the sentence
  697. url = match[0];
  698. // if we did not match ftp/http/www/mailto then assume mailto
  699. if (!match[2] && !match[4]) {
  700. url = (match[3] ? 'http://' : 'mailto:') + url;
  701. }
  702. i = match.index;
  703. addText(raw.substr(0, i));
  704. addLink(url, match[0].replace(MAILTO_REGEXP, ''));
  705. raw = raw.substring(i + match[0].length);
  706. }
  707. addText(raw);
  708. return $sanitize(html.join(''));
  709. function addText(text) {
  710. if (!text) {
  711. return;
  712. }
  713. html.push(sanitizeText(text));
  714. }
  715. function addLink(url, text) {
  716. var key, linkAttributes = attributesFn(url);
  717. html.push('<a ');
  718. for (key in linkAttributes) {
  719. html.push(key + '="' + linkAttributes[key] + '" ');
  720. }
  721. if (isDefined(target) && !('target' in linkAttributes)) {
  722. html.push('target="',
  723. target,
  724. '" ');
  725. }
  726. html.push('href="',
  727. url.replace(/"/g, '&quot;'),
  728. '">');
  729. addText(text);
  730. html.push('</a>');
  731. }
  732. };
  733. }]);
  734. })(window, window.angular);