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