From 5bcd7198664dca2bf85ddf8b3a89f417cd4e4796 Mon Sep 17 00:00:00 2001 From: Vojta Jina Date: Tue, 10 Apr 2012 16:50:31 -0700 Subject: chore(ngSanitize): extract $sanitize, ngBindHtml, linkyFilter into a module Create build for other modules as well (ngResource, ngCookies): - wrap into a function - add license - add version Breaks `$sanitize` service, `ngBindHtml` directive and `linky` filter were moved to the `ngSanitize` module. Apps that depend on any of these will need to load `angular-sanitize.js` and include `ngSanitize` in their dependency list: `var myApp = angular.module('myApp', ['ngSanitize']);` --- Rakefile | 24 +- angularFiles.js | 15 +- docs/content/cookbook/deeplinking.ngdoc | 2 +- docs/src/templates/docs.js | 3 +- docs/src/templates/index.html | 1 + src/AngularPublic.js | 2 - src/module.prefix | 6 + src/module.suffix | 2 + src/ng/directive/ngBind.js | 73 ++--- src/ng/filter.js | 1 - src/ng/filter/filters.js | 107 -------- src/ng/sanitize.js | 381 --------------------------- src/ngSanitize/directive/ngBindHtml.js | 26 ++ src/ngSanitize/filter/linky.js | 106 ++++++++ src/ngSanitize/sanitize.js | 395 ++++++++++++++++++++++++++++ test/ng/directive/ngBindSpec.js | 28 +- test/ng/filter/filtersSpec.js | 26 -- test/ng/sanitizeSpec.js | 281 -------------------- test/ngSanitize/directive/ngBindHtmlSpec.js | 25 ++ test/ngSanitize/filter/linkySpec.js | 27 ++ test/ngSanitize/sanitizeSpec.js | 286 ++++++++++++++++++++ test/ngScenario/dslSpec.js | 2 + 22 files changed, 938 insertions(+), 881 deletions(-) create mode 100644 src/module.prefix create mode 100644 src/module.suffix delete mode 100644 src/ng/sanitize.js create mode 100644 src/ngSanitize/directive/ngBindHtml.js create mode 100644 src/ngSanitize/filter/linky.js create mode 100644 src/ngSanitize/sanitize.js delete mode 100644 test/ng/sanitizeSpec.js create mode 100644 test/ngSanitize/directive/ngBindHtmlSpec.js create mode 100644 test/ngSanitize/filter/linkySpec.js create mode 100644 test/ngSanitize/sanitizeSpec.js diff --git a/Rakefile b/Rakefile index 3033d62d..7b5872d3 100644 --- a/Rakefile +++ b/Rakefile @@ -82,15 +82,23 @@ task :compile => [:init, :compile_scenario, :compile_jstd_scenario_adapter] do 'src/loader.js', 'src/loader.suffix']) - FileUtils.cp 'src/ngMock/angular-mocks.js', path_to('angular-mocks.js') - FileUtils.cp 'src/ngResource/resource.js', path_to('angular-resource.js') - FileUtils.cp 'src/ngCookies/cookies.js', path_to('angular-cookies.js') + concat_module('sanitize', [ + 'src/ngSanitize/sanitize.js', + 'src/ngSanitize/directive/ngBindHtml.js', + 'src/ngSanitize/filter/linky.js']) + + concat_module('resource', ['src/ngResource/resource.js']) + concat_module('cookies', ['src/ngCookies/cookies.js']) + + + FileUtils.cp 'src/ngMock/angular-mocks.js', path_to('angular-mocks.js') closure_compile('angular.js') closure_compile('angular-cookies.js') closure_compile('angular-loader.js') closure_compile('angular-resource.js') + closure_compile('angular-sanitize.js') end @@ -121,6 +129,8 @@ task :package => [:clean, :compile, :docs] do path_to('angular-cookies.min.js'), path_to('angular-resource.js'), path_to('angular-resource.min.js'), + path_to('angular-sanitize.js'), + path_to('angular-sanitize.min.js'), path_to('angular-scenario.js'), path_to('jstd-scenario-adapter.js'), path_to('jstd-scenario-adapter-config.js'), @@ -147,7 +157,8 @@ task :package => [:clean, :compile, :docs] do rewrite_file(src) do |content| content.sub!('angular.js', "angular-#{NG_VERSION.full}.js"). sub!('angular-resource.js', "angular-resource-#{NG_VERSION.full}.js"). - sub!('angular-cookies.js', "angular-cookies-#{NG_VERSION.full}.js") + sub!('angular-cookies.js', "angular-cookies-#{NG_VERSION.full}.js"). + sub!('angular-sanitize.js', "angular-sanitize-#{NG_VERSION.full}.js") end end @@ -290,6 +301,11 @@ def concat_file(filename, deps, footer='') end +def concat_module(name, files) + concat_file('angular-' + name + '.js', ['src/module.prefix'] + files + ['src/module.suffix']) +end + + def rewrite_file(filename) File.open(filename, File::RDWR) do |f| content = f.read diff --git a/angularFiles.js b/angularFiles.js index c819b629..d8be657a 100644 --- a/angularFiles.js +++ b/angularFiles.js @@ -24,7 +24,6 @@ angularFiles = { 'src/ng/route.js', 'src/ng/routeParams.js', 'src/ng/rootScope.js', - 'src/ng/sanitize.js', 'src/ng/sniffer.js', 'src/ng/window.js', 'src/ng/http.js', @@ -65,6 +64,9 @@ angularFiles = { 'angularSrcModules': [ 'src/ngCookies/cookies.js', 'src/ngResource/resource.js', + 'src/ngSanitize/sanitize.js', + 'src/ngSanitize/directive/ngBindHtml.js', + 'src/ngSanitize/filter/linky.js', 'src/ngMock/angular-mocks.js' ], @@ -98,6 +100,9 @@ angularFiles = { 'test/ng/filter/*.js', 'test/ngCookies/*.js', 'test/ngResource/*.js', + 'test/ngSanitize/*.js', + 'test/ngSanitize/directive/*.js', + 'test/ngSanitize/filter/*.js', 'test/ngMock/*.js' ], @@ -136,10 +141,16 @@ angularFiles = { 'src/ngMock/angular-mocks.js', 'src/ngCookies/cookies.js', 'src/ngResource/resource.js', + 'src/ngSanitize/sanitize.js', + 'src/ngSanitize/directive/ngBindHtml.js', + 'src/ngSanitize/filter/linky.js', 'test/matchers.js', 'test/ngMock/*.js', 'test/ngCookies/*.js', - 'test/ngResource/*.js' + 'test/ngResource/*.js', + 'test/ngSanitize/*.js', + 'test/ngSanitize/directive/*.js', + 'test/ngSanitize/filter/*.js' ], 'jstdPerf': [ diff --git a/docs/content/cookbook/deeplinking.ngdoc b/docs/content/cookbook/deeplinking.ngdoc index 10af8a2d..4343ded2 100644 --- a/docs/content/cookbook/deeplinking.ngdoc +++ b/docs/content/cookbook/deeplinking.ngdoc @@ -39,7 +39,7 @@ The two partials are defined in the following URLs: -
- Snippet: - - - - - - - - - - - - - - - - -
FilterSourceRendered
linky filter -
<div ng-bind-html="snippet | linky">
</div>
-
-
-
no filter
<div ng-bind="snippet">
</div>
- - - it('should linkify the snippet with urls', function() { - expect(using('#linky-filter').binding('snippet | linky')). - toBe('Pretty text with some links: ' + - 'http://angularjs.org/, ' + - 'us@somewhere.org, ' + - 'another@somewhere.org, ' + - 'and one more: ftp://127.0.0.1/.'); - }); - - it ('should not linkify snippet without the linky filter', function() { - expect(using('#escaped-html').binding('snippet')). - toBe("Pretty text with some links:\n" + - "http://angularjs.org/,\n" + - "mailto:us@somewhere.org,\n" + - "another@somewhere.org,\n" + - "and one more: ftp://127.0.0.1/."); - }); - - it('should update', function() { - input('snippet').enter('new http://link.'); - expect(using('#linky-filter').binding('snippet | linky')). - toBe('new http://link.'); - expect(using('#escaped-html').binding('snippet')).toBe('new http://link.'); - }); - - - */ -function linkyFilter() { - var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/, - MAILTO_REGEXP = /^mailto:/; - - return function(text) { - if (!text) return text; - var match; - var raw = text; - var html = []; - var writer = htmlSanitizeWriter(html); - var url; - var i; - while ((match = raw.match(LINKY_URL_REGEXP))) { - // We can not end in these as they are sometimes found at the end of the sentence - url = match[0]; - // if we did not match ftp/http/mailto then assume mailto - if (match[2] == match[3]) url = 'mailto:' + url; - i = match.index; - writer.chars(raw.substr(0, i)); - writer.start('a', {href:url}); - writer.chars(match[0].replace(MAILTO_REGEXP, '')); - writer.end('a'); - raw = raw.substring(i + match[0].length); - } - writer.chars(raw); - return html.join(''); - }; -} diff --git a/src/ng/sanitize.js b/src/ng/sanitize.js deleted file mode 100644 index 6a7a2be4..00000000 --- a/src/ng/sanitize.js +++ /dev/null @@ -1,381 +0,0 @@ -'use strict'; - -/* - * HTML Parser By Misko Hevery (misko@hevery.com) - * based on: HTML Parser By John Resig (ejohn.org) - * Original code by Erik Arvidsson, Mozilla Public License - * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js - * - * // Use like so: - * htmlParser(htmlString, { - * start: function(tag, attrs, unary) {}, - * end: function(tag) {}, - * chars: function(text) {}, - * comment: function(text) {} - * }); - * - */ - - - -/** - * @ngdoc service - * @name angular.module.ng.$sanitize - * @function - * - * @description - * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are - * then serialized back to properly escaped html string. This means that no unsafe input can make - * it into the returned string, however, since our parser is more strict than a typical browser - * parser, it's possible that some obscure input, which would be recognized as valid HTML by a - * browser, won't make it through the sanitizer. - * - * @param {string} html Html input. - * @returns {string} Sanitized html. - * - * @example - - - -
- Snippet: - - - - - - - - - - - - - - - - - - - - - -
FilterSourceRendered
html filter -
<div ng-bind-html="snippet">
</div>
-
-
-
no filter
<div ng-bind="snippet">
</div>
unsafe html filter
<div ng-bind-html-unsafe="snippet">
</div>
-
-
- - it('should sanitize the html snippet ', function() { - expect(using('#html-filter').element('div').html()). - toBe('

an html\nclick here\nsnippet

'); - }); - - it('should escape snippet without any filter', function() { - expect(using('#escaped-html').element('div').html()). - toBe("<p style=\"color:blue\">an html\n" + - "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + - "snippet</p>"); - }); - - it('should inline raw snippet if filtered as unsafe', function() { - expect(using('#html-unsafe-filter').element("div").html()). - toBe("

an html\n" + - "click here\n" + - "snippet

"); - }); - - it('should update', function() { - input('snippet').enter('new text'); - expect(using('#html-filter').binding('snippet')).toBe('new text'); - expect(using('#escaped-html').element('div').html()).toBe("new <b>text</b>"); - expect(using('#html-unsafe-filter').binding("snippet")).toBe('new text'); - }); -
-
- */ - -function $SanitizeProvider() { - this.$get = valueFn(function(html) { - var buf = []; - htmlParser(html, htmlSanitizeWriter(buf)); - return buf.join(''); - }); -} - -// Regular Expressions for parsing tags and attributes -var START_TAG_REGEXP = /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/, - END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/, - ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, - BEGIN_TAG_REGEXP = /^/g, - CDATA_REGEXP = //g, - URI_REGEXP = /^((ftp|https?):\/\/|mailto:|#)/, - NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; // Match everything outside of normal chars and " (quote character) - - -// Good source of info about elements and attributes -// http://dev.w3.org/html5/spec/Overview.html#semantics -// http://simon.html5.org/html-elements - -// Safe Void Elements - HTML5 -// http://dev.w3.org/html5/spec/Overview.html#void-elements -var voidElements = makeMap("area,br,col,hr,img,wbr"); - -// Elements that you can, intentionally, leave open (and which close themselves) -// http://dev.w3.org/html5/spec/Overview.html#optional-tags -var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), - optionalEndTagInlineElements = makeMap("rp,rt"), - optionalEndTagElements = extend({}, optionalEndTagInlineElements, optionalEndTagBlockElements); - -// Safe Block Elements - HTML5 -var blockElements = extend({}, optionalEndTagBlockElements, makeMap("address,article,aside," + - "blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6," + - "header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); - -// Inline Elements - HTML5 -var inlineElements = extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b,bdi,bdo," + - "big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small," + - "span,strike,strong,sub,sup,time,tt,u,var")); - - -// Special Elements (can contain anything) -var specialElements = makeMap("script,style"); - -var validElements = extend({}, voidElements, blockElements, inlineElements, optionalEndTagElements); - -//Attributes that have href and hence need to be sanitized -var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap"); -var validAttrs = extend({}, uriAttrs, makeMap( - 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+ - 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+ - 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+ - 'scope,scrolling,shape,span,start,summary,target,title,type,'+ - 'valign,value,vspace,width')); - -/** - * @example - * htmlParser(htmlString, { - * start: function(tag, attrs, unary) {}, - * end: function(tag) {}, - * chars: function(text) {}, - * comment: function(text) {} - * }); - * - * @param {string} html string - * @param {object} handler - */ -function htmlParser( html, handler ) { - var index, chars, match, stack = [], last = html; - stack.last = function() { return stack[ stack.length - 1 ]; }; - - while ( html ) { - chars = true; - - // Make sure we're not in a script or style element - if ( !stack.last() || !specialElements[ stack.last() ] ) { - - // Comment - if ( html.indexOf(""); - - if ( index >= 0 ) { - if (handler.comment) handler.comment( html.substring( 4, index ) ); - html = html.substring( index + 3 ); - chars = false; - } - - // end tag - } else if ( BEGING_END_TAGE_REGEXP.test(html) ) { - match = html.match( END_TAG_REGEXP ); - - if ( match ) { - html = html.substring( match[0].length ); - match[0].replace( END_TAG_REGEXP, parseEndTag ); - chars = false; - } - - // start tag - } else if ( BEGIN_TAG_REGEXP.test(html) ) { - match = html.match( START_TAG_REGEXP ); - - if ( match ) { - html = html.substring( match[0].length ); - match[0].replace( START_TAG_REGEXP, parseStartTag ); - chars = false; - } - } - - if ( chars ) { - index = html.indexOf("<"); - - var text = index < 0 ? html : html.substring( 0, index ); - html = index < 0 ? "" : html.substring( index ); - - if (handler.chars) handler.chars( decodeEntities(text) ); - } - - } else { - html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), function(all, text){ - text = text. - replace(COMMENT_REGEXP, "$1"). - replace(CDATA_REGEXP, "$1"); - - if (handler.chars) handler.chars( decodeEntities(text) ); - - return ""; - }); - - parseEndTag( "", stack.last() ); - } - - if ( html == last ) { - throw "Parse Error: " + html; - } - last = html; - } - - // Clean up any remaining tags - parseEndTag(); - - function parseStartTag( tag, tagName, rest, unary ) { - tagName = lowercase(tagName); - if ( blockElements[ tagName ] ) { - while ( stack.last() && inlineElements[ stack.last() ] ) { - parseEndTag( "", stack.last() ); - } - } - - if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) { - parseEndTag( "", tagName ); - } - - unary = voidElements[ tagName ] || !!unary; - - if ( !unary ) - stack.push( tagName ); - - var attrs = {}; - - rest.replace(ATTR_REGEXP, function(match, name, doubleQuotedValue, singleQoutedValue, unqoutedValue) { - var value = doubleQuotedValue - || singleQoutedValue - || unqoutedValue - || ''; - - attrs[name] = decodeEntities(value); - }); - if (handler.start) handler.start( tagName, attrs, unary ); - } - - function parseEndTag( tag, tagName ) { - var pos = 0, i; - tagName = lowercase(tagName); - if ( tagName ) - // Find the closest opened tag of the same type - for ( pos = stack.length - 1; pos >= 0; pos-- ) - if ( stack[ pos ] == tagName ) - break; - - if ( pos >= 0 ) { - // Close all the open elements, up the stack - for ( i = stack.length - 1; i >= pos; i-- ) - if (handler.end) handler.end( stack[ i ] ); - - // Remove the open elements from the stack - stack.length = pos; - } - } -} - -/** - * decodes all entities into regular string - * @param value - * @returns {string} A string with decoded entities. - */ -var hiddenPre=document.createElement("pre"); -function decodeEntities(value) { - hiddenPre.innerHTML=value.replace(//g, '>'); -} - -/** - * create an HTML/XML writer which writes to buffer - * @param {Array} buf use buf.jain('') to get out sanitized html string - * @returns {object} in the form of { - * start: function(tag, attrs, unary) {}, - * end: function(tag) {}, - * chars: function(text) {}, - * comment: function(text) {} - * } - */ -function htmlSanitizeWriter(buf){ - var ignore = false; - var out = bind(buf, buf.push); - return { - start: function(tag, attrs, unary){ - tag = lowercase(tag); - if (!ignore && specialElements[tag]) { - ignore = tag; - } - if (!ignore && validElements[tag] == true) { - out('<'); - out(tag); - forEach(attrs, function(value, key){ - var lkey=lowercase(key); - if (validAttrs[lkey]==true && (uriAttrs[lkey]!==true || value.match(URI_REGEXP))) { - out(' '); - out(key); - out('="'); - out(encodeEntities(value)); - out('"'); - } - }); - out(unary ? '/>' : '>'); - } - }, - end: function(tag){ - tag = lowercase(tag); - if (!ignore && validElements[tag] == true) { - out(''); - } - if (tag == ignore) { - ignore = false; - } - }, - chars: function(chars){ - if (!ignore) { - out(encodeEntities(chars)); - } - } - }; -} diff --git a/src/ngSanitize/directive/ngBindHtml.js b/src/ngSanitize/directive/ngBindHtml.js new file mode 100644 index 00000000..f8ccef18 --- /dev/null +++ b/src/ngSanitize/directive/ngBindHtml.js @@ -0,0 +1,26 @@ + + + +/** + * @ngdoc directive + * @name angular.module.ngSanitize.directive.ngBindHtml + * + * @description + * Creates a binding that will sanitize the result of evaluating the `expression` with the + * {@link angular.module.ng.$sanitize $sanitize} service and innerHTML the result into the current + * element. + * + * See {@link angular.module.ng.$sanitize $sanitize} docs for examples. + * + * @element ANY + * @param {expression} ngBindHtml {@link guide/dev_guide.expressions Expression} to evaluate. + */ +angular.module('ngSanitize').directive('ngBindHtml', ['$sanitize', function($sanitize) { + return function(scope, element, attr) { + element.addClass('ng-binding').data('$binding', attr.ngBindHtml); + scope.$watch(attr.ngBindHtml, function(value) { + value = $sanitize(value); + element.html(value || ''); + }); + }; +}]); diff --git a/src/ngSanitize/filter/linky.js b/src/ngSanitize/filter/linky.js new file mode 100644 index 00000000..c30665a2 --- /dev/null +++ b/src/ngSanitize/filter/linky.js @@ -0,0 +1,106 @@ +/** + * @ngdoc filter + * @name angular.module.ngSanitize.filter.linky + * @function + * + * @description + * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and + * plain email address links. + * + * @param {string} text Input text. + * @returns {string} Html-linkified text. + * + * @example + + + +
+ Snippet: + + + + + + + + + + + + + + + + +
FilterSourceRendered
linky filter +
<div ng-bind-html="snippet | linky">
</div>
+
+
+
no filter
<div ng-bind="snippet">
</div>
+ + + it('should linkify the snippet with urls', function() { + expect(using('#linky-filter').binding('snippet | linky')). + toBe('Pretty text with some links: ' + + 'http://angularjs.org/, ' + + 'us@somewhere.org, ' + + 'another@somewhere.org, ' + + 'and one more: ftp://127.0.0.1/.'); + }); + + it ('should not linkify snippet without the linky filter', function() { + expect(using('#escaped-html').binding('snippet')). + toBe("Pretty text with some links:\n" + + "http://angularjs.org/,\n" + + "mailto:us@somewhere.org,\n" + + "another@somewhere.org,\n" + + "and one more: ftp://127.0.0.1/."); + }); + + it('should update', function() { + input('snippet').enter('new http://link.'); + expect(using('#linky-filter').binding('snippet | linky')). + toBe('new http://link.'); + expect(using('#escaped-html').binding('snippet')).toBe('new http://link.'); + }); + + + */ +angular.module('ngSanitize').filter('linky', function() { + var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/, + MAILTO_REGEXP = /^mailto:/; + + return function(text) { + if (!text) return text; + var match; + var raw = text; + var html = []; + // TODO(vojta): use $sanitize instead + var writer = htmlSanitizeWriter(html); + var url; + var i; + while ((match = raw.match(LINKY_URL_REGEXP))) { + // We can not end in these as they are sometimes found at the end of the sentence + url = match[0]; + // if we did not match ftp/http/mailto then assume mailto + if (match[2] == match[3]) url = 'mailto:' + url; + i = match.index; + writer.chars(raw.substr(0, i)); + writer.start('a', {href:url}); + writer.chars(match[0].replace(MAILTO_REGEXP, '')); + writer.end('a'); + raw = raw.substring(i + match[0].length); + } + writer.chars(raw); + return html.join(''); + }; +}); diff --git a/src/ngSanitize/sanitize.js b/src/ngSanitize/sanitize.js new file mode 100644 index 00000000..c8d28315 --- /dev/null +++ b/src/ngSanitize/sanitize.js @@ -0,0 +1,395 @@ +'use strict'; + +/** + * @ngdoc overview + * @name angular.module.ngSanitize + * @description + */ + +/* + * HTML Parser By Misko Hevery (misko@hevery.com) + * based on: HTML Parser By John Resig (ejohn.org) + * Original code by Erik Arvidsson, Mozilla Public License + * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js + * + * // Use like so: + * htmlParser(htmlString, { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * }); + * + */ + + +/** + * @ngdoc service + * @name angular.module.ngSanitize.$sanitize + * @function + * + * @description + * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are + * then serialized back to properly escaped html string. This means that no unsafe input can make + * it into the returned string, however, since our parser is more strict than a typical browser + * parser, it's possible that some obscure input, which would be recognized as valid HTML by a + * browser, won't make it through the sanitizer. + * + * @param {string} html Html input. + * @returns {string} Sanitized html. + * + * @example + + + +
+ Snippet: + + + + + + + + + + + + + + + + + + + + + +
FilterSourceRendered
html filter +
<div ng-bind-html="snippet">
</div>
+
+
+
no filter
<div ng-bind="snippet">
</div>
unsafe html filter
<div ng-bind-html-unsafe="snippet">
</div>
+
+
+ + it('should sanitize the html snippet ', function() { + expect(using('#html-filter').element('div').html()). + toBe('

an html\nclick here\nsnippet

'); + }); + + it('should escape snippet without any filter', function() { + expect(using('#escaped-html').element('div').html()). + toBe("<p style=\"color:blue\">an html\n" + + "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + + "snippet</p>"); + }); + + it('should inline raw snippet if filtered as unsafe', function() { + expect(using('#html-unsafe-filter').element("div").html()). + toBe("

an html\n" + + "click here\n" + + "snippet

"); + }); + + it('should update', function() { + input('snippet').enter('new text'); + expect(using('#html-filter').binding('snippet')).toBe('new text'); + expect(using('#escaped-html').element('div').html()).toBe("new <b>text</b>"); + expect(using('#html-unsafe-filter').binding("snippet")).toBe('new text'); + }); +
+
+ */ +var $sanitize = function(html) { + var buf = []; + htmlParser(html, htmlSanitizeWriter(buf)); + return buf.join(''); +}; + + +// Regular Expressions for parsing tags and attributes +var START_TAG_REGEXP = /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/, + END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/, + ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, + BEGIN_TAG_REGEXP = /^/g, + CDATA_REGEXP = //g, + URI_REGEXP = /^((ftp|https?):\/\/|mailto:|#)/, + NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; // Match everything outside of normal chars and " (quote character) + + +// Good source of info about elements and attributes +// http://dev.w3.org/html5/spec/Overview.html#semantics +// http://simon.html5.org/html-elements + +// Safe Void Elements - HTML5 +// http://dev.w3.org/html5/spec/Overview.html#void-elements +var voidElements = makeMap("area,br,col,hr,img,wbr"); + +// Elements that you can, intentionally, leave open (and which close themselves) +// http://dev.w3.org/html5/spec/Overview.html#optional-tags +var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), + optionalEndTagInlineElements = makeMap("rp,rt"), + optionalEndTagElements = angular.extend({}, optionalEndTagInlineElements, optionalEndTagBlockElements); + +// Safe Block Elements - HTML5 +var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article,aside," + + "blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6," + + "header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); + +// Inline Elements - HTML5 +var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b,bdi,bdo," + + "big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small," + + "span,strike,strong,sub,sup,time,tt,u,var")); + + +// Special Elements (can contain anything) +var specialElements = makeMap("script,style"); + +var validElements = angular.extend({}, voidElements, blockElements, inlineElements, optionalEndTagElements); + +//Attributes that have href and hence need to be sanitized +var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap"); +var validAttrs = angular.extend({}, uriAttrs, makeMap( + 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+ + 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+ + 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+ + 'scope,scrolling,shape,span,start,summary,target,title,type,'+ + 'valign,value,vspace,width')); + +function makeMap(str) { + var obj = {}, items = str.split(','), i; + for (i = 0; i < items.length; i++) obj[items[i]] = true; + return obj; +} + + +/** + * @example + * htmlParser(htmlString, { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * }); + * + * @param {string} html string + * @param {object} handler + */ +function htmlParser( html, handler ) { + var index, chars, match, stack = [], last = html; + stack.last = function() { return stack[ stack.length - 1 ]; }; + + while ( html ) { + chars = true; + + // Make sure we're not in a script or style element + if ( !stack.last() || !specialElements[ stack.last() ] ) { + + // Comment + if ( html.indexOf(""); + + if ( index >= 0 ) { + if (handler.comment) handler.comment( html.substring( 4, index ) ); + html = html.substring( index + 3 ); + chars = false; + } + + // end tag + } else if ( BEGING_END_TAGE_REGEXP.test(html) ) { + match = html.match( END_TAG_REGEXP ); + + if ( match ) { + html = html.substring( match[0].length ); + match[0].replace( END_TAG_REGEXP, parseEndTag ); + chars = false; + } + + // start tag + } else if ( BEGIN_TAG_REGEXP.test(html) ) { + match = html.match( START_TAG_REGEXP ); + + if ( match ) { + html = html.substring( match[0].length ); + match[0].replace( START_TAG_REGEXP, parseStartTag ); + chars = false; + } + } + + if ( chars ) { + index = html.indexOf("<"); + + var text = index < 0 ? html : html.substring( 0, index ); + html = index < 0 ? "" : html.substring( index ); + + if (handler.chars) handler.chars( decodeEntities(text) ); + } + + } else { + html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), function(all, text){ + text = text. + replace(COMMENT_REGEXP, "$1"). + replace(CDATA_REGEXP, "$1"); + + if (handler.chars) handler.chars( decodeEntities(text) ); + + return ""; + }); + + parseEndTag( "", stack.last() ); + } + + if ( html == last ) { + throw "Parse Error: " + html; + } + last = html; + } + + // Clean up any remaining tags + parseEndTag(); + + function parseStartTag( tag, tagName, rest, unary ) { + tagName = angular.lowercase(tagName); + if ( blockElements[ tagName ] ) { + while ( stack.last() && inlineElements[ stack.last() ] ) { + parseEndTag( "", stack.last() ); + } + } + + if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) { + parseEndTag( "", tagName ); + } + + unary = voidElements[ tagName ] || !!unary; + + if ( !unary ) + stack.push( tagName ); + + var attrs = {}; + + rest.replace(ATTR_REGEXP, function(match, name, doubleQuotedValue, singleQoutedValue, unqoutedValue) { + var value = doubleQuotedValue + || singleQoutedValue + || unqoutedValue + || ''; + + attrs[name] = decodeEntities(value); + }); + if (handler.start) handler.start( tagName, attrs, unary ); + } + + function parseEndTag( tag, tagName ) { + var pos = 0, i; + tagName = angular.lowercase(tagName); + if ( tagName ) + // Find the closest opened tag of the same type + for ( pos = stack.length - 1; pos >= 0; pos-- ) + if ( stack[ pos ] == tagName ) + break; + + if ( pos >= 0 ) { + // Close all the open elements, up the stack + for ( i = stack.length - 1; i >= pos; i-- ) + if (handler.end) handler.end( stack[ i ] ); + + // Remove the open elements from the stack + stack.length = pos; + } + } +} + +/** + * decodes all entities into regular string + * @param value + * @returns {string} A string with decoded entities. + */ +var hiddenPre=document.createElement("pre"); +function decodeEntities(value) { + hiddenPre.innerHTML=value.replace(//g, '>'); +} + +/** + * create an HTML/XML writer which writes to buffer + * @param {Array} buf use buf.jain('') to get out sanitized html string + * @returns {object} in the form of { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * } + */ +function htmlSanitizeWriter(buf){ + var ignore = false; + var out = angular.bind(buf, buf.push); + return { + start: function(tag, attrs, unary){ + tag = angular.lowercase(tag); + if (!ignore && specialElements[tag]) { + ignore = tag; + } + if (!ignore && validElements[tag] == true) { + out('<'); + out(tag); + angular.forEach(attrs, function(value, key){ + var lkey=angular.lowercase(key); + if (validAttrs[lkey]==true && (uriAttrs[lkey]!==true || value.match(URI_REGEXP))) { + out(' '); + out(key); + out('="'); + out(encodeEntities(value)); + out('"'); + } + }); + out(unary ? '/>' : '>'); + } + }, + end: function(tag){ + tag = angular.lowercase(tag); + if (!ignore && validElements[tag] == true) { + out(''); + } + if (tag == ignore) { + ignore = false; + } + }, + chars: function(chars){ + if (!ignore) { + out(encodeEntities(chars)); + } + } + }; +} + + +// define ngSanitize module and register $sanitize service +angular.module('ngSanitize', []).value('$sanitize', $sanitize); diff --git a/test/ng/directive/ngBindSpec.js b/test/ng/directive/ngBindSpec.js index b3c63b34..da291fa4 100644 --- a/test/ng/directive/ngBindSpec.js +++ b/test/ng/directive/ngBindSpec.js @@ -67,39 +67,13 @@ describe('ngBind*', function() { }); - describe('ngBindHtml', function() { - - it('should set html', inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.html = '
hello
'; - $rootScope.$digest(); - expect(lowercase(element.html())).toEqual('
hello
'); - })); - - - it('should reset html when value is null or undefined', inject(function($compile, $rootScope) { - element = $compile('
')($rootScope); - - forEach([null, undefined, ''], function(val) { - $rootScope.html = 'some val'; - $rootScope.$digest(); - expect(lowercase(element.html())).toEqual('some val'); - - $rootScope.html = val; - $rootScope.$digest(); - expect(lowercase(element.html())).toEqual(''); - }); - })); - }); - - describe('ngBindHtmlUnsafe', function() { it('should set unsafe html', inject(function($rootScope, $compile) { element = $compile('
')($rootScope); $rootScope.html = '
hello
'; $rootScope.$digest(); - expect(lowercase(element.html())).toEqual('
hello
'); + expect(angular.lowercase(element.html())).toEqual('
hello
'); })); }); }); diff --git a/test/ng/filter/filtersSpec.js b/test/ng/filter/filtersSpec.js index 9ea200a3..55476882 100644 --- a/test/ng/filter/filtersSpec.js +++ b/test/ng/filter/filtersSpec.js @@ -153,32 +153,6 @@ describe('filters', function() { }); }); - describe('linky', function() { - var linky; - - beforeEach(inject(function($filter){ - linky = $filter('linky') - })); - - it('should do basic filter', function() { - expect(linky("http://ab/ (http://a/) http://1.2/v:~-123. c")). - toEqual('http://ab/ ' + - '(http://a/) ' + - '<http://a/> ' + - 'http://1.2/v:~-123. c'); - expect(linky(undefined)).not.toBeDefined(); - }); - - it('should handle mailto:', function() { - expect(linky("mailto:me@example.com")). - toEqual('me@example.com'); - expect(linky("me@example.com")). - toEqual('me@example.com'); - expect(linky("send email to me@example.com, but")). - toEqual('send email to me@example.com, but'); - }); - }); - describe('date', function() { var morning = new angular.mock.TzDate(+5, '2010-09-03T12:05:08.000Z'); //7am diff --git a/test/ng/sanitizeSpec.js b/test/ng/sanitizeSpec.js deleted file mode 100644 index a33d8992..00000000 --- a/test/ng/sanitizeSpec.js +++ /dev/null @@ -1,281 +0,0 @@ -'use strict'; - -describe('HTML', function() { - - var expectHTML; - - beforeEach(inject(function($sanitize) { - expectHTML = function(html){ - return expect($sanitize(html)); - }; - })); - - describe('htmlParser', function() { - var handler, start, text; - beforeEach(function() { - handler = { - start: function(tag, attrs, unary){ - start = { - tag: tag, - attrs: attrs, - unary: unary - }; - // Since different browsers handle newlines differenttly we trim - // so that it is easier to write tests. - forEach(attrs, function(value, key){ - attrs[key] = trim(value); - }); - }, - chars: function(text_){ - text = text_; - }, - end:function(tag) { - expect(tag).toEqual(start.tag); - } - }; - }); - - it('should parse basic format', function() { - htmlParser('text', handler); - expect(start).toEqual({tag:'tag', attrs:{attr:'value'}, unary:false}); - expect(text).toEqual('text'); - }); - - it('should parse newlines in tags', function() { - htmlParser('<\ntag\n attr="value"\n>text<\n/\ntag\n>', handler); - expect(start).toEqual({tag:'tag', attrs:{attr:'value'}, unary:false}); - expect(text).toEqual('text'); - }); - - it('should parse newlines in attributes', function() { - htmlParser('text', handler); - expect(start).toEqual({tag:'tag', attrs:{attr:'value'}, unary:false}); - expect(text).toEqual('text'); - }); - - it('should parse namespace', function() { - htmlParser('text', handler); - expect(start).toEqual({tag:'ns:t-a-g', attrs:{'ns:a-t-t-r':'value'}, unary:false}); - expect(text).toEqual('text'); - }); - - it('should parse empty value attribute of node', function() { - htmlParser('', handler); - expect(start).toEqual({tag:'option', attrs:{selected:'', value:''}, unary:false}); - expect(text).toEqual('abc'); - }); - - }); - - it('should echo html', function() { - expectHTML('helloworld.'). - toEqual('helloworld.'); - }); - - it('should remove script', function() { - expectHTML('ac.').toEqual('ac.'); - }); - - it('should remove double nested script', function() { - expectHTML('ailc.').toEqual('ac.'); - }); - - it('should remove unknown names', function() { - expectHTML('abc').toEqual('abc'); - }); - - it('should remove unsafe value', function() { - expectHTML('').toEqual(''); - }); - - it('should handle self closed elements', function() { - expectHTML('a
c').toEqual('a
c'); - }); - - it('should handle namespace', function() { - expectHTML('abc').toEqual('abc'); - }); - - it('should handle entities', function() { - var everything = '
' + - '!@#$%^&*()_+-={}[]:";\'<>?,./`~ ħ
'; - expectHTML(everything).toEqual(everything); - }); - - it('should handle improper html', function() { - expectHTML('< div rel="
" alt=abc dir=\'"\' >text< /div>'). - toEqual('
text
'); - }); - - it('should handle improper html2', function() { - expectHTML('< div rel="
" / >'). - toEqual('
'); - }); - - it('should ignore back slash as escape', function() { - expectHTML('xxx\\'). - toEqual('xxx\\'); - }); - - it('should ignore object attributes', function() { - expectHTML(':)'). - toEqual(':)'); - expectHTML(':)'). - toEqual(''); - }); - - describe('htmlSanitizerWriter', function() { - var writer, html; - beforeEach(function() { - html = ''; - writer = htmlSanitizeWriter({push:function(text){html+=text;}}); - }); - - it('should write basic HTML', function() { - writer.chars('before'); - writer.start('div', {rel:'123'}, false); - writer.chars('in'); - writer.end('div'); - writer.chars('after'); - - expect(html).toEqual('before
in
after'); - }); - - it('should escape text nodes', function() { - writer.chars('a
&
c'); - expect(html).toEqual('a<div>&</div>c'); - }); - - it('should escape IE script', function() { - writer.chars('&<>{}'); - expect(html).toEqual('&<>{}'); - }); - - it('should escape attributes', function() { - writer.start('div', {rel:'!@#$%^&*()_+-={}[]:";\'<>?,./`~ \n\0\r\u0127'}); - expect(html).toEqual('
'); - }); - - it('should ignore missformed elements', function() { - writer.start('d>i&v', {}); - expect(html).toEqual(''); - }); - - it('should ignore unknown attributes', function() { - writer.start('div', {unknown:""}); - expect(html).toEqual('
'); - }); - - describe('explicitly dissallow', function() { - it('should not allow attributes', function() { - writer.start('div', {id:'a', name:'a', style:'a'}); - expect(html).toEqual('
'); - }); - - it('should not allow tags', function() { - function tag(name) { - writer.start(name, {}); - writer.end(name); - } - tag('frameset'); - tag('frame'); - tag('form'); - tag('param'); - tag('object'); - tag('embed'); - tag('textarea'); - tag('input'); - tag('button'); - tag('option'); - tag('select'); - tag('script'); - tag('style'); - tag('link'); - tag('base'); - tag('basefont'); - expect(html).toEqual(''); - }); - }); - - describe('isUri', function() { - - function isUri(value) { - return value.match(URI_REGEXP); - } - - it('should be URI', function() { - expect(isUri('http://abc')).toBeTruthy(); - expect(isUri('https://abc')).toBeTruthy(); - expect(isUri('ftp://abc')).toBeTruthy(); - expect(isUri('mailto:me@example.com')).toBeTruthy(); - expect(isUri('#anchor')).toBeTruthy(); - }); - - it('should not be UIR', function() { - expect(isUri('')).toBeFalsy(); - expect(isUri('javascript:alert')).toBeFalsy(); - }); - }); - - describe('javascript URL attribute', function() { - beforeEach(function() { - this.addMatchers({ - toBeValidUrl: function() { - return URI_REGEXP.exec(this.actual); - } - }); - }); - - it('should ignore javascript:', function() { - expect('JavaScript:abc').not.toBeValidUrl(); - expect(' \n Java\n Script:abc').not.toBeValidUrl(); - expect('http://JavaScript/my.js').toBeValidUrl(); - }); - - it('should ignore dec encoded javascript:', function() { - expect('javascript:').not.toBeValidUrl(); - expect('javascript:').not.toBeValidUrl(); - expect('j avascript:').not.toBeValidUrl(); - }); - - it('should ignore decimal with leading 0 encodede javascript:', function() { - expect('javascript:').not.toBeValidUrl(); - expect('j avascript:').not.toBeValidUrl(); - expect('j avascript:').not.toBeValidUrl(); - }); - - it('should ignore hex encoded javascript:', function() { - expect('javascript:').not.toBeValidUrl(); - expect('javascript:').not.toBeValidUrl(); - expect('j avascript:').not.toBeValidUrl(); - }); - - it('should ignore hex encoded whitespace javascript:', function() { - expect('jav ascript:alert("A");').not.toBeValidUrl(); - expect('jav ascript:alert("B");').not.toBeValidUrl(); - expect('jav ascript:alert("C");').not.toBeValidUrl(); - expect('jav\u0000ascript:alert("D");').not.toBeValidUrl(); - expect('java\u0000\u0000script:alert("D");').not.toBeValidUrl(); - expect('  java\u0000\u0000script:alert("D");').not.toBeValidUrl(); - }); - }); - - }); - -}); diff --git a/test/ngSanitize/directive/ngBindHtmlSpec.js b/test/ngSanitize/directive/ngBindHtmlSpec.js new file mode 100644 index 00000000..be23e5a5 --- /dev/null +++ b/test/ngSanitize/directive/ngBindHtmlSpec.js @@ -0,0 +1,25 @@ +describe('ngBindHtml', function() { + beforeEach(module('ngSanitize')); + + it('should set html', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.html = '
hello
'; + $rootScope.$digest(); + expect(angular.lowercase(element.html())).toEqual('
hello
'); + })); + + + it('should reset html when value is null or undefined', inject(function($compile, $rootScope) { + element = $compile('
')($rootScope); + + angular.forEach([null, undefined, ''], function(val) { + $rootScope.html = 'some val'; + $rootScope.$digest(); + expect(angular.lowercase(element.html())).toEqual('some val'); + + $rootScope.html = val; + $rootScope.$digest(); + expect(angular.lowercase(element.html())).toEqual(''); + }); + })); +}); diff --git a/test/ngSanitize/filter/linkySpec.js b/test/ngSanitize/filter/linkySpec.js new file mode 100644 index 00000000..0448159a --- /dev/null +++ b/test/ngSanitize/filter/linkySpec.js @@ -0,0 +1,27 @@ +describe('linky', function() { + var linky; + + beforeEach(module('ngSanitize')); + + beforeEach(inject(function($filter){ + linky = $filter('linky'); + })); + + it('should do basic filter', function() { + expect(linky("http://ab/ (http://a/) http://1.2/v:~-123. c")). + toEqual('http://ab/ ' + + '(http://a/) ' + + '<http://a/> ' + + 'http://1.2/v:~-123. c'); + expect(linky(undefined)).not.toBeDefined(); + }); + + it('should handle mailto:', function() { + expect(linky("mailto:me@example.com")). + toEqual('me@example.com'); + expect(linky("me@example.com")). + toEqual('me@example.com'); + expect(linky("send email to me@example.com, but")). + toEqual('send email to me@example.com, but'); + }); +}); diff --git a/test/ngSanitize/sanitizeSpec.js b/test/ngSanitize/sanitizeSpec.js new file mode 100644 index 00000000..b4fd8a2a --- /dev/null +++ b/test/ngSanitize/sanitizeSpec.js @@ -0,0 +1,286 @@ +'use strict'; + +describe('HTML', function() { + + var expectHTML; + + beforeEach(module('ngSanitize')); + + beforeEach(inject(function($sanitize) { + expectHTML = function(html){ + return expect($sanitize(html)); + }; + })); + + describe('htmlParser', function() { + if (angular.isUndefined(window.htmlParser)) return; + + var handler, start, text; + beforeEach(function() { + handler = { + start: function(tag, attrs, unary){ + start = { + tag: tag, + attrs: attrs, + unary: unary + }; + // Since different browsers handle newlines differenttly we trim + // so that it is easier to write tests. + angular.forEach(attrs, function(value, key) { + attrs[key] = value.replace(/^\s*/, '').replace(/\s*$/, '') + }); + }, + chars: function(text_){ + text = text_; + }, + end:function(tag) { + expect(tag).toEqual(start.tag); + } + }; + }); + + it('should parse basic format', function() { + htmlParser('text', handler); + expect(start).toEqual({tag:'tag', attrs:{attr:'value'}, unary:false}); + expect(text).toEqual('text'); + }); + + it('should parse newlines in tags', function() { + htmlParser('<\ntag\n attr="value"\n>text<\n/\ntag\n>', handler); + expect(start).toEqual({tag:'tag', attrs:{attr:'value'}, unary:false}); + expect(text).toEqual('text'); + }); + + it('should parse newlines in attributes', function() { + htmlParser('text', handler); + expect(start).toEqual({tag:'tag', attrs:{attr:'value'}, unary:false}); + expect(text).toEqual('text'); + }); + + it('should parse namespace', function() { + htmlParser('text', handler); + expect(start).toEqual({tag:'ns:t-a-g', attrs:{'ns:a-t-t-r':'value'}, unary:false}); + expect(text).toEqual('text'); + }); + + it('should parse empty value attribute of node', function() { + htmlParser('', handler); + expect(start).toEqual({tag:'option', attrs:{selected:'', value:''}, unary:false}); + expect(text).toEqual('abc'); + }); + }); + + // THESE TESTS ARE EXECUTED WITH COMPILED ANGULAR + it('should echo html', function() { + expectHTML('helloworld.'). + toEqual('helloworld.'); + }); + + it('should remove script', function() { + expectHTML('ac.').toEqual('ac.'); + }); + + it('should remove double nested script', function() { + expectHTML('ailc.').toEqual('ac.'); + }); + + it('should remove unknown names', function() { + expectHTML('abc').toEqual('abc'); + }); + + it('should remove unsafe value', function() { + expectHTML('').toEqual(''); + }); + + it('should handle self closed elements', function() { + expectHTML('a
c').toEqual('a
c'); + }); + + it('should handle namespace', function() { + expectHTML('abc').toEqual('abc'); + }); + + it('should handle entities', function() { + var everything = '
' + + '!@#$%^&*()_+-={}[]:";\'<>?,./`~ ħ
'; + expectHTML(everything).toEqual(everything); + }); + + it('should handle improper html', function() { + expectHTML('< div rel="
" alt=abc dir=\'"\' >text< /div>'). + toEqual('
text
'); + }); + + it('should handle improper html2', function() { + expectHTML('< div rel="
" / >'). + toEqual('
'); + }); + + it('should ignore back slash as escape', function() { + expectHTML('xxx\\'). + toEqual('xxx\\'); + }); + + it('should ignore object attributes', function() { + expectHTML(':)'). + toEqual(':)'); + expectHTML(':)'). + toEqual(''); + }); + + describe('htmlSanitizerWriter', function() { + if (angular.isUndefined(window.htmlSanitizeWriter)) return; + + var writer, html; + beforeEach(function() { + html = ''; + writer = htmlSanitizeWriter({push:function(text){html+=text;}}); + }); + + it('should write basic HTML', function() { + writer.chars('before'); + writer.start('div', {rel:'123'}, false); + writer.chars('in'); + writer.end('div'); + writer.chars('after'); + + expect(html).toEqual('before
in
after'); + }); + + it('should escape text nodes', function() { + writer.chars('a
&
c'); + expect(html).toEqual('a<div>&</div>c'); + }); + + it('should escape IE script', function() { + writer.chars('&<>{}'); + expect(html).toEqual('&<>{}'); + }); + + it('should escape attributes', function() { + writer.start('div', {rel:'!@#$%^&*()_+-={}[]:";\'<>?,./`~ \n\0\r\u0127'}); + expect(html).toEqual('
'); + }); + + it('should ignore missformed elements', function() { + writer.start('d>i&v', {}); + expect(html).toEqual(''); + }); + + it('should ignore unknown attributes', function() { + writer.start('div', {unknown:""}); + expect(html).toEqual('
'); + }); + + describe('explicitly dissallow', function() { + it('should not allow attributes', function() { + writer.start('div', {id:'a', name:'a', style:'a'}); + expect(html).toEqual('
'); + }); + + it('should not allow tags', function() { + function tag(name) { + writer.start(name, {}); + writer.end(name); + } + tag('frameset'); + tag('frame'); + tag('form'); + tag('param'); + tag('object'); + tag('embed'); + tag('textarea'); + tag('input'); + tag('button'); + tag('option'); + tag('select'); + tag('script'); + tag('style'); + tag('link'); + tag('base'); + tag('basefont'); + expect(html).toEqual(''); + }); + }); + + describe('isUri', function() { + + function isUri(value) { + return value.match(URI_REGEXP); + } + + it('should be URI', function() { + expect(isUri('http://abc')).toBeTruthy(); + expect(isUri('https://abc')).toBeTruthy(); + expect(isUri('ftp://abc')).toBeTruthy(); + expect(isUri('mailto:me@example.com')).toBeTruthy(); + expect(isUri('#anchor')).toBeTruthy(); + }); + + it('should not be UIR', function() { + expect(isUri('')).toBeFalsy(); + expect(isUri('javascript:alert')).toBeFalsy(); + }); + }); + + describe('javascript URL attribute', function() { + beforeEach(function() { + this.addMatchers({ + toBeValidUrl: function() { + return URI_REGEXP.exec(this.actual); + } + }); + }); + + it('should ignore javascript:', function() { + expect('JavaScript:abc').not.toBeValidUrl(); + expect(' \n Java\n Script:abc').not.toBeValidUrl(); + expect('http://JavaScript/my.js').toBeValidUrl(); + }); + + it('should ignore dec encoded javascript:', function() { + expect('javascript:').not.toBeValidUrl(); + expect('javascript:').not.toBeValidUrl(); + expect('j avascript:').not.toBeValidUrl(); + }); + + it('should ignore decimal with leading 0 encodede javascript:', function() { + expect('javascript:').not.toBeValidUrl(); + expect('j avascript:').not.toBeValidUrl(); + expect('j avascript:').not.toBeValidUrl(); + }); + + it('should ignore hex encoded javascript:', function() { + expect('javascript:').not.toBeValidUrl(); + expect('javascript:').not.toBeValidUrl(); + expect('j avascript:').not.toBeValidUrl(); + }); + + it('should ignore hex encoded whitespace javascript:', function() { + expect('jav ascript:alert("A");').not.toBeValidUrl(); + expect('jav ascript:alert("B");').not.toBeValidUrl(); + expect('jav ascript:alert("C");').not.toBeValidUrl(); + expect('jav\u0000ascript:alert("D");').not.toBeValidUrl(); + expect('java\u0000\u0000script:alert("D");').not.toBeValidUrl(); + expect('  java\u0000\u0000script:alert("D");').not.toBeValidUrl(); + }); + }); + + }); +}); diff --git a/test/ngScenario/dslSpec.js b/test/ngScenario/dslSpec.js index 0a8ab762..fee5c3b5 100644 --- a/test/ngScenario/dslSpec.js +++ b/test/ngScenario/dslSpec.js @@ -9,6 +9,8 @@ describe("angular.scenario.dsl", function() { dealoc(element); }); + beforeEach(module('ngSanitize')); + beforeEach(inject(function($injector) { eventLog = []; $window = { -- cgit v1.2.3