Issues (1588)

myems-admin/js/angular/angular-sanitize.js (3 issues)

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