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