From f73bc73613c014afd6e46185ddd922df22cb3987 Mon Sep 17 00:00:00 2001 From: Caitlin Potter Date: Wed, 14 May 2014 09:13:43 -0400 Subject: [PATCH 1/2] feat($interpolate): escape interpolated expressions By default, the escaped interpolation start and end symbols are `{{{{` and `}}}}`, respectively. These symbols will be replaced with the proper interpolation start and end symbols during interpolation, without parsing the expression. Fixes #5601 --- src/ng/interpolate.js | 71 +++++++++++++++++++++++++++++++++----- test/ng/interpolateSpec.js | 28 +++++++++++++++ 2 files changed, 91 insertions(+), 8 deletions(-) diff --git a/src/ng/interpolate.js b/src/ng/interpolate.js index e529bc1bf420..77c7ab39d45b 100644 --- a/src/ng/interpolate.js +++ b/src/ng/interpolate.js @@ -41,6 +41,8 @@ var $interpolateMinErr = minErr('$interpolate'); function $InterpolateProvider() { var startSymbol = '{{'; var endSymbol = '}}'; + var escapedStartSymbol = '{{{{'; + var escapedEndSymbol = '}}}}'; /** * @ngdoc method @@ -49,11 +51,15 @@ function $InterpolateProvider() { * Symbol to denote start of expression in the interpolated string. Defaults to `{{`. * * @param {string=} value new value to set the starting symbol to. + * @param {string=} escaped the new value to set the escaped starting symbol to. * @returns {string|self} Returns the symbol when used as getter and self if used as setter. */ - this.startSymbol = function(value){ + this.startSymbol = function(value, escaped){ if (value) { startSymbol = value; + if (escaped && escaped !== value) { + escapedStartSymbol = '' + escaped; + } return this; } else { return startSymbol; @@ -67,11 +73,15 @@ function $InterpolateProvider() { * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`. * * @param {string=} value new value to set the ending symbol to. + * @param {string=} escaped the new value to set the escaped ending symbol to. * @returns {string|self} Returns the symbol when used as getter and self if used as setter. */ - this.endSymbol = function(value){ + this.endSymbol = function(value, escaped){ if (value) { endSymbol = value; + if (escaped && escaped !== value) { + escapedEndSymbol = '' + escaped; + } return this; } else { return endSymbol; @@ -81,7 +91,9 @@ function $InterpolateProvider() { this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) { var startSymbolLength = startSymbol.length, - endSymbolLength = endSymbol.length; + endSymbolLength = endSymbol.length, + escapedStartLength = escapedStartSymbol.length, + escapedEndLength = escapedEndSymbol.length; /** * @ngdoc service @@ -154,23 +166,48 @@ function $InterpolateProvider() { hasText = false, exp, concat = [], - lastValuesCache = { values: {}, results: {}}; + lastValuesCache = { values: {}, results: {}}, + escapedStart = -1, + escapedEnd = -1, + escaped = true, + i = -1; while(index < textLength) { - if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) && - ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) { + if (i < 0) { + i = index; + } + + if (escaped) { + if ((escapedStart = text.indexOf(escapedStartSymbol, i)) != -1) { + if ((escapedEnd = text.indexOf(escapedEndSymbol, escapedStart + escapedStartLength)) + != -1) { + escapedEnd += escapedEndLength; + } else { + escaped = false; + } + } else { + escaped = false; + } + } + + if (((startIndex = text.indexOf(startSymbol, i)) != -1) && + ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) && + (!escaped || escapedEnd < startIndex || escapedStart > endIndex + endSymbolLength) ) { if (index !== startIndex) hasText = true; - separators.push(text.substring(index, startIndex)); + separators.push(unescape(text.substring(index, startIndex))); exp = text.substring(startIndex + startSymbolLength, endIndex); expressions.push(exp); parseFns.push($parse(exp)); index = endIndex + endSymbolLength; hasInterpolation = true; + i = -1; + } else if (escaped && startIndex >= 0 && escapedStart < (endIndex + endSymbolLength)) { + i = escapedEnd; } else { // we did not find an interpolation, so we have to add the remainder to the separators array if (index !== textLength) { hasText = true; - separators.push(text.substring(index)); + separators.push(unescape(text.substring(index))); } break; } @@ -316,6 +353,24 @@ function $InterpolateProvider() { return endSymbol; }; + function unescape(text) { + var i = 0, start, end, unescaped = ""; + while (i < text.length) { + if (((start = text.indexOf(escapedStartSymbol, i)) != -1 && + ((end = text.indexOf(escapedEndSymbol, start + escapedStartLength)) != -1))) { + end = end + escapedEndLength; + unescaped += text.substring(i, end). + replace(escapedStartSymbol, startSymbol). + replace(escapedEndSymbol, endSymbol); + i = end; + } else { + unescaped += text.substring(i); + break; + } + } + return unescaped; + } + return $interpolate; }]; } diff --git a/test/ng/interpolateSpec.js b/test/ng/interpolateSpec.js index 6dd49d6bdaae..f496630938ff 100644 --- a/test/ng/interpolateSpec.js +++ b/test/ng/interpolateSpec.js @@ -61,6 +61,34 @@ describe('$interpolate', function() { })); + it('should not parse escaped interpolation expressions', inject(function($interpolate, $rootScope) { + $rootScope.foo = 'World'; + expect($interpolate('Hello, {{{{foo}}}}!')($rootScope)).toBe('Hello, {{foo}}!'); + })); + + + it('should parse expressions before escaped expressions', inject(function($interpolate, $rootScope) { + $rootScope.foo = 'World'; + expect($interpolate('Hello, {{foo}}! ({{{{foo}}}})')($rootScope)).toBe('Hello, World! ({{foo}})'); + })); + + + it('should parse expressions after escaped expressions', inject(function($interpolate, $rootScope) { + $rootScope.foo = 'World'; + expect($interpolate('Hello, {{{{foo}}}} ({{foo}}!)')($rootScope)).toBe('Hello, {{foo}} (World!)'); + })); + + + it('should not parse naively escaped expressions', inject(function($interpolate, $rootScope) { + $rootScope.abc = 'Hello'; + $rootScope.def = 'world'; + expect($interpolate('{{{{abc}}}}}}{{{{def}}}}')($rootScope)).toBe('{{abc}}}}{{def}}'); + expect($interpolate('{{{{{{abc}}}}{{{{def}}}}')($rootScope)).toBe('{{{{abc}}{{def}}'); + expect($interpolate('{{{{abc}}}}{{{{def}}}}}}')($rootScope)).toBe('{{abc}}{{def}}}}'); + expect($interpolate('{{{{abc}}}}{{{{{{def}}}}')($rootScope)).toBe('{{abc}}{{{{def}}'); + })); + + describe('interpolating in a trusted context', function() { var sce; beforeEach(function() { From 502d4117798a3e46063344d4af7261c01579ce3e Mon Sep 17 00:00:00 2001 From: Caitlin Potter Date: Wed, 14 May 2014 12:08:57 -0400 Subject: [PATCH 2/2] WIP: improve robustness of escaped interpolation --- src/ng/interpolate.js | 10 +++++++++- test/ng/interpolateSpec.js | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/ng/interpolate.js b/src/ng/interpolate.js index 77c7ab39d45b..53513a3a2cf7 100644 --- a/src/ng/interpolate.js +++ b/src/ng/interpolate.js @@ -93,7 +93,8 @@ function $InterpolateProvider() { var startSymbolLength = startSymbol.length, endSymbolLength = endSymbol.length, escapedStartLength = escapedStartSymbol.length, - escapedEndLength = escapedEndSymbol.length; + escapedEndLength = escapedEndSymbol.length, + similarStartSymbols = escapedStartSymbol.indexOf(startSymbol) === 0; /** * @ngdoc service @@ -193,6 +194,13 @@ function $InterpolateProvider() { if (((startIndex = text.indexOf(startSymbol, i)) != -1) && ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) && (!escaped || escapedEnd < startIndex || escapedStart > endIndex + endSymbolLength) ) { + if (escapedEnd < 0 && similarStartSymbols) { + while (escapedStart >= 0 && startIndex === escapedStart && + text.indexOf(startSymbol, startIndex) === startIndex) { + startIndex = escapedStart + startSymbolLength; + escapedStart = text.indexOf(escapedStartSymbol, startIndex + startSymbolLength); + } + } if (index !== startIndex) hasText = true; separators.push(unescape(text.substring(index, startIndex))); exp = text.substring(startIndex + startSymbolLength, endIndex); diff --git a/test/ng/interpolateSpec.js b/test/ng/interpolateSpec.js index f496630938ff..e9da1415ea21 100644 --- a/test/ng/interpolateSpec.js +++ b/test/ng/interpolateSpec.js @@ -86,6 +86,8 @@ describe('$interpolate', function() { expect($interpolate('{{{{{{abc}}}}{{{{def}}}}')($rootScope)).toBe('{{{{abc}}{{def}}'); expect($interpolate('{{{{abc}}}}{{{{def}}}}}}')($rootScope)).toBe('{{abc}}{{def}}}}'); expect($interpolate('{{{{abc}}}}{{{{{{def}}}}')($rootScope)).toBe('{{abc}}{{{{def}}'); + expect($interpolate('}}}}abc{{abc}}}}{{{{def}}}')($rootScope)).toBe('}}}}abcHello}}{{world}'); + expect($interpolate('{{{{{{abc{{def}}}}}}')($rootScope)).toBe('{{{{abc{{def}}}}'); }));