angular-sanitize.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686
  1. /* */
  2. "format global";
  3. "deps angular";
  4. /**
  5. * @license AngularJS v1.4.5
  6. * (c) 2010-2015 Google, Inc. http://angularjs.org
  7. * License: MIT
  8. */
  9. (function(window, angular, undefined) {'use strict';
  10. /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
  11. * Any commits to this file should be reviewed with security in mind. *
  12. * Changes to this file can potentially create security vulnerabilities. *
  13. * An approval from 2 Core members with history of modifying *
  14. * this file is required. *
  15. * *
  16. * Does the change somehow allow for arbitrary javascript to be executed? *
  17. * Or allows for someone to change the prototype of built-in objects? *
  18. * Or gives undesired access to variables likes document or window? *
  19. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
  20. var $sanitizeMinErr = angular.$$minErr('$sanitize');
  21. /**
  22. * @ngdoc module
  23. * @name ngSanitize
  24. * @description
  25. *
  26. * # ngSanitize
  27. *
  28. * The `ngSanitize` module provides functionality to sanitize HTML.
  29. *
  30. *
  31. * <div doc-module-components="ngSanitize"></div>
  32. *
  33. * See {@link ngSanitize.$sanitize `$sanitize`} for usage.
  34. */
  35. /*
  36. * HTML Parser By Misko Hevery (misko@hevery.com)
  37. * based on: HTML Parser By John Resig (ejohn.org)
  38. * Original code by Erik Arvidsson, Mozilla Public License
  39. * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
  40. *
  41. * // Use like so:
  42. * htmlParser(htmlString, {
  43. * start: function(tag, attrs, unary) {},
  44. * end: function(tag) {},
  45. * chars: function(text) {},
  46. * comment: function(text) {}
  47. * });
  48. *
  49. */
  50. /**
  51. * @ngdoc service
  52. * @name $sanitize
  53. * @kind function
  54. *
  55. * @description
  56. * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are
  57. * then serialized back to properly escaped html string. This means that no unsafe input can make
  58. * it into the returned string, however, since our parser is more strict than a typical browser
  59. * parser, it's possible that some obscure input, which would be recognized as valid HTML by a
  60. * browser, won't make it through the sanitizer. The input may also contain SVG markup.
  61. * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and
  62. * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}.
  63. *
  64. * @param {string} html HTML input.
  65. * @returns {string} Sanitized HTML.
  66. *
  67. * @example
  68. <example module="sanitizeExample" deps="angular-sanitize.js">
  69. <file name="index.html">
  70. <script>
  71. angular.module('sanitizeExample', ['ngSanitize'])
  72. .controller('ExampleController', ['$scope', '$sce', function($scope, $sce) {
  73. $scope.snippet =
  74. '<p style="color:blue">an html\n' +
  75. '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
  76. 'snippet</p>';
  77. $scope.deliberatelyTrustDangerousSnippet = function() {
  78. return $sce.trustAsHtml($scope.snippet);
  79. };
  80. }]);
  81. </script>
  82. <div ng-controller="ExampleController">
  83. Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
  84. <table>
  85. <tr>
  86. <td>Directive</td>
  87. <td>How</td>
  88. <td>Source</td>
  89. <td>Rendered</td>
  90. </tr>
  91. <tr id="bind-html-with-sanitize">
  92. <td>ng-bind-html</td>
  93. <td>Automatically uses $sanitize</td>
  94. <td><pre>&lt;div ng-bind-html="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
  95. <td><div ng-bind-html="snippet"></div></td>
  96. </tr>
  97. <tr id="bind-html-with-trust">
  98. <td>ng-bind-html</td>
  99. <td>Bypass $sanitize by explicitly trusting the dangerous value</td>
  100. <td>
  101. <pre>&lt;div ng-bind-html="deliberatelyTrustDangerousSnippet()"&gt;
  102. &lt;/div&gt;</pre>
  103. </td>
  104. <td><div ng-bind-html="deliberatelyTrustDangerousSnippet()"></div></td>
  105. </tr>
  106. <tr id="bind-default">
  107. <td>ng-bind</td>
  108. <td>Automatically escapes</td>
  109. <td><pre>&lt;div ng-bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
  110. <td><div ng-bind="snippet"></div></td>
  111. </tr>
  112. </table>
  113. </div>
  114. </file>
  115. <file name="protractor.js" type="protractor">
  116. it('should sanitize the html snippet by default', function() {
  117. expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
  118. toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
  119. });
  120. it('should inline raw snippet if bound to a trusted value', function() {
  121. expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).
  122. toBe("<p style=\"color:blue\">an html\n" +
  123. "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
  124. "snippet</p>");
  125. });
  126. it('should escape snippet without any filter', function() {
  127. expect(element(by.css('#bind-default div')).getInnerHtml()).
  128. toBe("&lt;p style=\"color:blue\"&gt;an html\n" +
  129. "&lt;em onmouseover=\"this.textContent='PWN3D!'\"&gt;click here&lt;/em&gt;\n" +
  130. "snippet&lt;/p&gt;");
  131. });
  132. it('should update', function() {
  133. element(by.model('snippet')).clear();
  134. element(by.model('snippet')).sendKeys('new <b onclick="alert(1)">text</b>');
  135. expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
  136. toBe('new <b>text</b>');
  137. expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe(
  138. 'new <b onclick="alert(1)">text</b>');
  139. expect(element(by.css('#bind-default div')).getInnerHtml()).toBe(
  140. "new &lt;b onclick=\"alert(1)\"&gt;text&lt;/b&gt;");
  141. });
  142. </file>
  143. </example>
  144. */
  145. function $SanitizeProvider() {
  146. this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
  147. return function(html) {
  148. var buf = [];
  149. htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) {
  150. return !/^unsafe/.test($$sanitizeUri(uri, isImage));
  151. }));
  152. return buf.join('');
  153. };
  154. }];
  155. }
  156. function sanitizeText(chars) {
  157. var buf = [];
  158. var writer = htmlSanitizeWriter(buf, angular.noop);
  159. writer.chars(chars);
  160. return buf.join('');
  161. }
  162. // Regular Expressions for parsing tags and attributes
  163. var START_TAG_REGEXP =
  164. /^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,
  165. END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/,
  166. ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,
  167. BEGIN_TAG_REGEXP = /^</,
  168. BEGING_END_TAGE_REGEXP = /^<\//,
  169. COMMENT_REGEXP = /<!--(.*?)-->/g,
  170. DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i,
  171. CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g,
  172. SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
  173. // Match everything outside of normal chars and " (quote character)
  174. NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g;
  175. // Good source of info about elements and attributes
  176. // http://dev.w3.org/html5/spec/Overview.html#semantics
  177. // http://simon.html5.org/html-elements
  178. // Safe Void Elements - HTML5
  179. // http://dev.w3.org/html5/spec/Overview.html#void-elements
  180. var voidElements = makeMap("area,br,col,hr,img,wbr");
  181. // Elements that you can, intentionally, leave open (and which close themselves)
  182. // http://dev.w3.org/html5/spec/Overview.html#optional-tags
  183. var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
  184. optionalEndTagInlineElements = makeMap("rp,rt"),
  185. optionalEndTagElements = angular.extend({},
  186. optionalEndTagInlineElements,
  187. optionalEndTagBlockElements);
  188. // Safe Block Elements - HTML5
  189. var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," +
  190. "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," +
  191. "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul"));
  192. // Inline Elements - HTML5
  193. var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," +
  194. "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," +
  195. "samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
  196. // SVG Elements
  197. // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements
  198. // Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted.
  199. // They can potentially allow for arbitrary javascript to be executed. See #11290
  200. var svgElements = makeMap("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph," +
  201. "hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline," +
  202. "radialGradient,rect,stop,svg,switch,text,title,tspan,use");
  203. // Special Elements (can contain anything)
  204. var specialElements = makeMap("script,style");
  205. var validElements = angular.extend({},
  206. voidElements,
  207. blockElements,
  208. inlineElements,
  209. optionalEndTagElements,
  210. svgElements);
  211. //Attributes that have href and hence need to be sanitized
  212. var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap,xlink:href");
  213. var htmlAttrs = makeMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
  214. 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' +
  215. 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' +
  216. 'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' +
  217. 'valign,value,vspace,width');
  218. // SVG attributes (without "id" and "name" attributes)
  219. // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes
  220. var svgAttrs = makeMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
  221. 'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' +
  222. 'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' +
  223. 'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' +
  224. 'height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,' +
  225. 'marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,' +
  226. 'max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,' +
  227. 'path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,' +
  228. 'requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,' +
  229. 'stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,' +
  230. 'stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,' +
  231. 'stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,' +
  232. 'underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,' +
  233. 'width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,' +
  234. 'xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan', true);
  235. var validAttrs = angular.extend({},
  236. uriAttrs,
  237. svgAttrs,
  238. htmlAttrs);
  239. function makeMap(str, lowercaseKeys) {
  240. var obj = {}, items = str.split(','), i;
  241. for (i = 0; i < items.length; i++) {
  242. obj[lowercaseKeys ? angular.lowercase(items[i]) : items[i]] = true;
  243. }
  244. return obj;
  245. }
  246. /**
  247. * @example
  248. * htmlParser(htmlString, {
  249. * start: function(tag, attrs, unary) {},
  250. * end: function(tag) {},
  251. * chars: function(text) {},
  252. * comment: function(text) {}
  253. * });
  254. *
  255. * @param {string} html string
  256. * @param {object} handler
  257. */
  258. function htmlParser(html, handler) {
  259. if (typeof html !== 'string') {
  260. if (html === null || typeof html === 'undefined') {
  261. html = '';
  262. } else {
  263. html = '' + html;
  264. }
  265. }
  266. var index, chars, match, stack = [], last = html, text;
  267. stack.last = function() { return stack[stack.length - 1]; };
  268. while (html) {
  269. text = '';
  270. chars = true;
  271. // Make sure we're not in a script or style element
  272. if (!stack.last() || !specialElements[stack.last()]) {
  273. // Comment
  274. if (html.indexOf("<!--") === 0) {
  275. // comments containing -- are not allowed unless they terminate the comment
  276. index = html.indexOf("--", 4);
  277. if (index >= 0 && html.lastIndexOf("-->", index) === index) {
  278. if (handler.comment) handler.comment(html.substring(4, index));
  279. html = html.substring(index + 3);
  280. chars = false;
  281. }
  282. // DOCTYPE
  283. } else if (DOCTYPE_REGEXP.test(html)) {
  284. match = html.match(DOCTYPE_REGEXP);
  285. if (match) {
  286. html = html.replace(match[0], '');
  287. chars = false;
  288. }
  289. // end tag
  290. } else if (BEGING_END_TAGE_REGEXP.test(html)) {
  291. match = html.match(END_TAG_REGEXP);
  292. if (match) {
  293. html = html.substring(match[0].length);
  294. match[0].replace(END_TAG_REGEXP, parseEndTag);
  295. chars = false;
  296. }
  297. // start tag
  298. } else if (BEGIN_TAG_REGEXP.test(html)) {
  299. match = html.match(START_TAG_REGEXP);
  300. if (match) {
  301. // We only have a valid start-tag if there is a '>'.
  302. if (match[4]) {
  303. html = html.substring(match[0].length);
  304. match[0].replace(START_TAG_REGEXP, parseStartTag);
  305. }
  306. chars = false;
  307. } else {
  308. // no ending tag found --- this piece should be encoded as an entity.
  309. text += '<';
  310. html = html.substring(1);
  311. }
  312. }
  313. if (chars) {
  314. index = html.indexOf("<");
  315. text += index < 0 ? html : html.substring(0, index);
  316. html = index < 0 ? "" : html.substring(index);
  317. if (handler.chars) handler.chars(decodeEntities(text));
  318. }
  319. } else {
  320. // IE versions 9 and 10 do not understand the regex '[^]', so using a workaround with [\W\w].
  321. html = html.replace(new RegExp("([\\W\\w]*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'),
  322. function(all, text) {
  323. text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1");
  324. if (handler.chars) handler.chars(decodeEntities(text));
  325. return "";
  326. });
  327. parseEndTag("", stack.last());
  328. }
  329. if (html == last) {
  330. throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " +
  331. "of html: {0}", html);
  332. }
  333. last = html;
  334. }
  335. // Clean up any remaining tags
  336. parseEndTag();
  337. function parseStartTag(tag, tagName, rest, unary) {
  338. tagName = angular.lowercase(tagName);
  339. if (blockElements[tagName]) {
  340. while (stack.last() && inlineElements[stack.last()]) {
  341. parseEndTag("", stack.last());
  342. }
  343. }
  344. if (optionalEndTagElements[tagName] && stack.last() == tagName) {
  345. parseEndTag("", tagName);
  346. }
  347. unary = voidElements[tagName] || !!unary;
  348. if (!unary) {
  349. stack.push(tagName);
  350. }
  351. var attrs = {};
  352. rest.replace(ATTR_REGEXP,
  353. function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) {
  354. var value = doubleQuotedValue
  355. || singleQuotedValue
  356. || unquotedValue
  357. || '';
  358. attrs[name] = decodeEntities(value);
  359. });
  360. if (handler.start) handler.start(tagName, attrs, unary);
  361. }
  362. function parseEndTag(tag, tagName) {
  363. var pos = 0, i;
  364. tagName = angular.lowercase(tagName);
  365. if (tagName) {
  366. // Find the closest opened tag of the same type
  367. for (pos = stack.length - 1; pos >= 0; pos--) {
  368. if (stack[pos] == tagName) break;
  369. }
  370. }
  371. if (pos >= 0) {
  372. // Close all the open elements, up the stack
  373. for (i = stack.length - 1; i >= pos; i--)
  374. if (handler.end) handler.end(stack[i]);
  375. // Remove the open elements from the stack
  376. stack.length = pos;
  377. }
  378. }
  379. }
  380. var hiddenPre=document.createElement("pre");
  381. /**
  382. * decodes all entities into regular string
  383. * @param value
  384. * @returns {string} A string with decoded entities.
  385. */
  386. function decodeEntities(value) {
  387. if (!value) { return ''; }
  388. hiddenPre.innerHTML = value.replace(/</g,"&lt;");
  389. // innerText depends on styling as it doesn't display hidden elements.
  390. // Therefore, it's better to use textContent not to cause unnecessary reflows.
  391. return hiddenPre.textContent;
  392. }
  393. /**
  394. * Escapes all potentially dangerous characters, so that the
  395. * resulting string can be safely inserted into attribute or
  396. * element text.
  397. * @param value
  398. * @returns {string} escaped text
  399. */
  400. function encodeEntities(value) {
  401. return value.
  402. replace(/&/g, '&amp;').
  403. replace(SURROGATE_PAIR_REGEXP, function(value) {
  404. var hi = value.charCodeAt(0);
  405. var low = value.charCodeAt(1);
  406. return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
  407. }).
  408. replace(NON_ALPHANUMERIC_REGEXP, function(value) {
  409. return '&#' + value.charCodeAt(0) + ';';
  410. }).
  411. replace(/</g, '&lt;').
  412. replace(/>/g, '&gt;');
  413. }
  414. /**
  415. * create an HTML/XML writer which writes to buffer
  416. * @param {Array} buf use buf.jain('') to get out sanitized html string
  417. * @returns {object} in the form of {
  418. * start: function(tag, attrs, unary) {},
  419. * end: function(tag) {},
  420. * chars: function(text) {},
  421. * comment: function(text) {}
  422. * }
  423. */
  424. function htmlSanitizeWriter(buf, uriValidator) {
  425. var ignore = false;
  426. var out = angular.bind(buf, buf.push);
  427. return {
  428. start: function(tag, attrs, unary) {
  429. tag = angular.lowercase(tag);
  430. if (!ignore && specialElements[tag]) {
  431. ignore = tag;
  432. }
  433. if (!ignore && validElements[tag] === true) {
  434. out('<');
  435. out(tag);
  436. angular.forEach(attrs, function(value, key) {
  437. var lkey=angular.lowercase(key);
  438. var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');
  439. if (validAttrs[lkey] === true &&
  440. (uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
  441. out(' ');
  442. out(key);
  443. out('="');
  444. out(encodeEntities(value));
  445. out('"');
  446. }
  447. });
  448. out(unary ? '/>' : '>');
  449. }
  450. },
  451. end: function(tag) {
  452. tag = angular.lowercase(tag);
  453. if (!ignore && validElements[tag] === true) {
  454. out('</');
  455. out(tag);
  456. out('>');
  457. }
  458. if (tag == ignore) {
  459. ignore = false;
  460. }
  461. },
  462. chars: function(chars) {
  463. if (!ignore) {
  464. out(encodeEntities(chars));
  465. }
  466. }
  467. };
  468. }
  469. // define ngSanitize module and register $sanitize service
  470. angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
  471. /* global sanitizeText: false */
  472. /**
  473. * @ngdoc filter
  474. * @name linky
  475. * @kind function
  476. *
  477. * @description
  478. * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
  479. * plain email address links.
  480. *
  481. * Requires the {@link ngSanitize `ngSanitize`} module to be installed.
  482. *
  483. * @param {string} text Input text.
  484. * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in.
  485. * @returns {string} Html-linkified text.
  486. *
  487. * @usage
  488. <span ng-bind-html="linky_expression | linky"></span>
  489. *
  490. * @example
  491. <example module="linkyExample" deps="angular-sanitize.js">
  492. <file name="index.html">
  493. <script>
  494. angular.module('linkyExample', ['ngSanitize'])
  495. .controller('ExampleController', ['$scope', function($scope) {
  496. $scope.snippet =
  497. 'Pretty text with some links:\n'+
  498. 'http://angularjs.org/,\n'+
  499. 'mailto:us@somewhere.org,\n'+
  500. 'another@somewhere.org,\n'+
  501. 'and one more: ftp://127.0.0.1/.';
  502. $scope.snippetWithTarget = 'http://angularjs.org/';
  503. }]);
  504. </script>
  505. <div ng-controller="ExampleController">
  506. Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
  507. <table>
  508. <tr>
  509. <td>Filter</td>
  510. <td>Source</td>
  511. <td>Rendered</td>
  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="snippetWithTarget | linky:'_blank'"&gt;<br>&lt;/div&gt;</pre>
  526. </td>
  527. <td>
  528. <div ng-bind-html="snippetWithTarget | linky:'_blank'"></div>
  529. </td>
  530. </tr>
  531. <tr id="escaped-html">
  532. <td>no filter</td>
  533. <td><pre>&lt;div ng-bind="snippet"&gt;<br>&lt;/div&gt;</pre></td>
  534. <td><div ng-bind="snippet"></div></td>
  535. </tr>
  536. </table>
  537. </file>
  538. <file name="protractor.js" type="protractor">
  539. it('should linkify the snippet with urls', function() {
  540. expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
  541. toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' +
  542. 'another@somewhere.org, and one more: ftp://127.0.0.1/.');
  543. expect(element.all(by.css('#linky-filter a')).count()).toEqual(4);
  544. });
  545. it('should not linkify snippet without the linky filter', function() {
  546. expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()).
  547. toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' +
  548. 'another@somewhere.org, and one more: ftp://127.0.0.1/.');
  549. expect(element.all(by.css('#escaped-html a')).count()).toEqual(0);
  550. });
  551. it('should update', function() {
  552. element(by.model('snippet')).clear();
  553. element(by.model('snippet')).sendKeys('new http://link.');
  554. expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
  555. toBe('new http://link.');
  556. expect(element.all(by.css('#linky-filter a')).count()).toEqual(1);
  557. expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText())
  558. .toBe('new http://link.');
  559. });
  560. it('should work with the target property', function() {
  561. expect(element(by.id('linky-target')).
  562. element(by.binding("snippetWithTarget | linky:'_blank'")).getText()).
  563. toBe('http://angularjs.org/');
  564. expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
  565. });
  566. </file>
  567. </example>
  568. */
  569. angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
  570. var LINKY_URL_REGEXP =
  571. /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i,
  572. MAILTO_REGEXP = /^mailto:/i;
  573. return function(text, target) {
  574. if (!text) return text;
  575. var match;
  576. var raw = text;
  577. var html = [];
  578. var url;
  579. var i;
  580. while ((match = raw.match(LINKY_URL_REGEXP))) {
  581. // We can not end in these as they are sometimes found at the end of the sentence
  582. url = match[0];
  583. // if we did not match ftp/http/www/mailto then assume mailto
  584. if (!match[2] && !match[4]) {
  585. url = (match[3] ? 'http://' : 'mailto:') + url;
  586. }
  587. i = match.index;
  588. addText(raw.substr(0, i));
  589. addLink(url, match[0].replace(MAILTO_REGEXP, ''));
  590. raw = raw.substring(i + match[0].length);
  591. }
  592. addText(raw);
  593. return $sanitize(html.join(''));
  594. function addText(text) {
  595. if (!text) {
  596. return;
  597. }
  598. html.push(sanitizeText(text));
  599. }
  600. function addLink(url, text) {
  601. html.push('<a ');
  602. if (angular.isDefined(target)) {
  603. html.push('target="',
  604. target,
  605. '" ');
  606. }
  607. html.push('href="',
  608. url.replace(/"/g, '&quot;'),
  609. '">');
  610. addText(text);
  611. html.push('</a>');
  612. }
  613. };
  614. }]);
  615. })(window, window.angular);