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