From 764dbe6a2f92165ff7334a819bf502f7d5c7df1b Mon Sep 17 00:00:00 2001 From: Caitlin Potter Date: Tue, 20 May 2014 04:11:34 -0400 Subject: [PATCH] feat($interpolate): escaped interpolation expressions This CL enables interpolation expressions to be escaped, by prefixing each character of their start/end markers with a REVERSE SOLIDUS U+005C, and to render the escaped expression as a regular interpolation expression. Example: `{{foo}}, \\{\\{World!\\}\\}` would be rendered as: `Hello, {{World!}}` This will also work with custom interpolation markers, for example: module. config(function($interpolateProvider) { $interpolateProvider.startSymbol('\\\\'); $interpolateProvider.endSymbol('//'); }). run(function($interpolate) { // Will alert with "hello\\bar//": alert($interpolate('\\\\foo//\\\\\\\\bar\\/\\/')({foo: "hello", bar: "world"})); }); This change effectively only changes the rendering of these escaped markers, because they are not context-aware, and are incapable of preventing nested expressions within those escaped markers from being evaluated. Therefore, backends are encouraged to ensure that when escaping expressions for security reasons, every single instance of a start or end marker have each of its characters prefixed with a backslash (REVERSE SOLIDUS, U+005C) Closes #5601 --- src/ng/interpolate.js | 14 +++++++++- test/ng/interpolateSpec.js | 56 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/ng/interpolate.js b/src/ng/interpolate.js index f3209a9800a0..29afefd09aad 100644 --- a/src/ng/interpolate.js +++ b/src/ng/interpolate.js @@ -81,7 +81,13 @@ function $InterpolateProvider() { this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) { var startSymbolLength = startSymbol.length, - endSymbolLength = endSymbol.length; + endSymbolLength = endSymbol.length, + escapedStartRegexp = new RegExp(startSymbol.replace(/./g, escape), 'g'), + escapedEndRegexp = new RegExp(endSymbol.replace(/./g, escape), 'g'); + + function escape(ch) { + return '\\\\\\' + ch; + } /** * @ngdoc service @@ -176,6 +182,12 @@ function $InterpolateProvider() { } } + forEach(separators, function(key, i) { + separators[i] = separators[i]. + replace(escapedStartRegexp, startSymbol). + replace(escapedEndRegexp, endSymbol); + }); + if (separators.length === expressions.length) { separators.push(''); } diff --git a/test/ng/interpolateSpec.js b/test/ng/interpolateSpec.js index 6dd49d6bdaae..7a8f899a9bd0 100644 --- a/test/ng/interpolateSpec.js +++ b/test/ng/interpolateSpec.js @@ -61,6 +61,62 @@ describe('$interpolate', function() { })); + describe('interpolation escaping', function() { + var obj; + + beforeEach(function() { + obj = {foo: 'Hello', bar: 'World'}; + }); + + it('should support escaping interpolation signs', inject(function($interpolate) { + expect($interpolate('{{foo}} \\{\\{bar\\}\\}')(obj)).toBe('Hello {{bar}}'); + expect($interpolate('\\{\\{foo\\}\\} {{bar}}')(obj)).toBe('{{foo}} World'); + })); + + + it('should unescape multiple expressions', inject(function($interpolate) { + expect($interpolate('\\{\\{foo\\}\\}\\{\\{bar\\}\\} {{foo}}')(obj)).toBe('{{foo}}{{bar}} Hello'); + expect($interpolate('{{foo}}\\{\\{foo\\}\\}\\{\\{bar\\}\\}')(obj)).toBe('Hello{{foo}}{{bar}}'); + expect($interpolate('\\{\\{foo\\}\\}{{foo}}\\{\\{bar\\}\\}')(obj)).toBe('{{foo}}Hello{{bar}}'); + expect($interpolate('{{foo}}\\{\\{foo\\}\\}{{bar}}\\{\\{bar\\}\\}{{foo}}')(obj)).toBe('Hello{{foo}}World{{bar}}Hello'); + })); + + + it('should support escaping custom interpolation start/end symbols', function() { + module(function($interpolateProvider) { + $interpolateProvider.startSymbol('[['); + $interpolateProvider.endSymbol(']]'); + }); + inject(function($interpolate) { + expect($interpolate('[[foo]] \\[\\[bar\\]\\]')(obj)).toBe('Hello [[bar]]'); + }); + }); + + + it('should unescape incomplete escaped expressions', inject(function($interpolate) { + expect($interpolate('\\{\\{foo{{foo}}')(obj)).toBe('{{fooHello'); + expect($interpolate('\\}\\}foo{{foo}}')(obj)).toBe('}}fooHello'); + expect($interpolate('foo{{foo}}\\{\\{')(obj)).toBe('fooHello{{'); + expect($interpolate('foo{{foo}}\\}\\}')(obj)).toBe('fooHello}}'); + })); + + + it('should not unescape markers within expressions', inject(function($interpolate) { + expect($interpolate('{{"\\\\{\\\\{Hello, world!\\\\}\\\\}"}}')(obj)).toBe('\\{\\{Hello, world!\\}\\}'); + expect($interpolate('{{"\\{\\{Hello, world!\\}\\}"}}')(obj)).toBe('{{Hello, world!}}'); + expect(function() { + $interpolate('{{\\{\\{foo\\}\\}}}')(obj); + }).toThrowMinErr('$parse', 'lexerr', + 'Lexer Error: Unexpected next character at columns 0-0 [\\] in expression [\\{\\{foo\\}\\]'); + })); + + + it('should evaluate expressions between escaped start/end symbols', inject(function($interpolate) { + expect($interpolate('\\{\\{Hello, {{bar}}!\\}\\}')(obj)).toBe('{{Hello, World!}}'); + })); + }); + + describe('interpolating in a trusted context', function() { var sce; beforeEach(function() {