Nav apraksta

angular-sanitize.js 29KB

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