GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Issues (3063)

angularjs/angular-1.5.0/angular-sanitize.js (2 issues)

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 {
0 ignored issues
show
Comprehensibility introduced by
else is not necessary here since all if branches return, consider removing it to reduce nesting and make code more readable.
Loading history...
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 View Code Duplication
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--;
0 ignored issues
show
Complexity Coding Style introduced by
You seem to be assigning a new value to the loop variable i here. Please check if this was indeed your intention. Even if it was, consider using another kind of loop instead.
Loading history...
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:[email protected],\n'+
604
             '[email protected],\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/, [email protected], ' +
613
                  '[email protected], 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:[email protected], ' +
620
                  '[email protected], 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);
718