angular-sanitize.js 33 KB

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