angular-sanitize.js 25 KB

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