diff --git a/Gruntfile.js b/Gruntfile.js index 5199a4766..339127420 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -51,6 +51,7 @@ module.exports = function (grunt) { 'src/service/translate.js', 'src/service/default-interpolation.js', 'src/service/storage-key.js', + 'src/service/sanitization.js', 'src/directive/translate.js', 'src/directive/translate-cloak.js', 'src/filter/translate.js' diff --git a/src/service/default-interpolation.js b/src/service/default-interpolation.js index 1da06116f..51b9ac8a2 100644 --- a/src/service/default-interpolation.js +++ b/src/service/default-interpolation.js @@ -44,6 +44,14 @@ function $translateDefaultInterpolation ($interpolate, $translateSanitization) { return $identifier; }; + /** + * @deprecated ToDo: remove in 3.0 + */ + $translateInterpolator.useSanitizeValueStrategy = function (value) { + $translateSanitization.useStrategy(value); + return this; + }; + /** * @ngdoc function * @name pascalprecht.translate.$translateDefaultInterpolation#interpolate @@ -57,10 +65,10 @@ function $translateDefaultInterpolation ($interpolate, $translateSanitization) { */ $translateInterpolator.interpolate = function (string, interpolationParams) { interpolationParams = interpolationParams || {}; - interpolationParams = $translateSanitization.sanitize(interpolationParams); + interpolationParams = $translateSanitization.sanitize(interpolationParams, 'params'); var interpolatedText = $interpolate(string)(interpolationParams); - interpolatedText = $translateSanitization.sanitize(interpolatedText); + interpolatedText = $translateSanitization.sanitize(interpolatedText, 'text'); return interpolatedText; }; diff --git a/src/service/messageformat-interpolation.js b/src/service/messageformat-interpolation.js index 7cb09d204..d9e968c20 100644 --- a/src/service/messageformat-interpolation.js +++ b/src/service/messageformat-interpolation.js @@ -61,6 +61,14 @@ function $translateMessageFormatInterpolation($translateSanitization, $cacheFact return $identifier; }; + /** + * @deprecated ToDo: remove in 3.0 + */ + $translateInterpolator.useSanitizeValueStrategy = function (value) { + $translateSanitization.useStrategy(value); + return this; + }; + /** * @ngdoc function * @name pascalprecht.translate.$translateMessageFormatInterpolation#interpolate @@ -73,14 +81,14 @@ function $translateMessageFormatInterpolation($translateSanitization, $cacheFact */ $translateInterpolator.interpolate = function (string, interpolationParams) { interpolationParams = interpolationParams || {}; - interpolationParams = $translateSanitization.sanitize(interpolationParams); + interpolationParams = $translateSanitization.sanitize(interpolationParams, 'params'); var interpolatedText = $cache.get(string + angular.toJson(interpolationParams)); // if given string wasn't interpolated yet, we do so now and never have to do it again if (!interpolatedText) { interpolatedText = $mf.compile(string)(interpolationParams); - interpolatedText = $translateSanitization.sanitize(interpolatedText); + interpolatedText = $translateSanitization.sanitize(interpolatedText, 'text'); $cache.put(string + angular.toJson(interpolationParams), interpolatedText); } diff --git a/src/service/sanitization.js b/src/service/sanitization.js index ee0948248..879575e3d 100644 --- a/src/service/sanitization.js +++ b/src/service/sanitization.js @@ -3,7 +3,7 @@ * @name pascalprecht.translate.$translateSanitization * * @description - * Sanitizes interpolation parameters and interpolated strings + * Sanitizes interpolation parameters and translated texts. * * @return {object} $translateInterpolator Interpolator service */ @@ -13,72 +13,132 @@ function $translateSanitizationProvider(){ var provider = this, $sanitize, - currentStrategy = null; // ToDo: change to either 'sanitize' or 'escape' in 3.0. + currentStrategy = null,// ToDo: change to either 'sanitize', 'escape' or ['sanitize', 'escapeParameters'] in 3.0. + hasConfiguredStrategy = false, + hasShownNoStrategyConfiguredWarning = false; provider.strategies = { - sanitize: function(value){ - if(angular.isString(value)){ - htmlSanitizeValue(value); - } - else{ - return value; + /** + * Sanitizes HTML in the translation text using $sanitize. + */ + sanitize: function(value, mode){ + if(mode === 'text'){ + value = htmlSanitizeValue(value); } + return value; }, - escape: function(value){ - if(angular.isString(value)){ - return htmlEscapeValue(value); - } - else{ - return value; + /** + * Escapes HTML in the translation. + */ + escape: function(value, mode){ + if(mode === 'text'){ + value = htmlEscapeValue(value); } + return value; }, - escapeParameters: function(value){ - if(angular.isString(value)){ - return value; - } - else{ - return mapInterpolationParameters(value, htmlEscapeValue); + /** + * Sanitizes HTML in the values of the interpolation parameters using $sanitize. + */ + sanitizeParameters: function(value, mode){ + if(mode === 'params'){ + value = mapInterpolationParameters(value, htmlSanitizeValue); } + return value; }, - sanitizeParameters: function(value){ - if(angular.isString(value)){ - return value; - } - else{ - return mapInterpolationParameters(value, htmlSanitizeValue); + /** + * Escapes HTML in the values of the interpolation parameters. + */ + escapeParameters: function(value, mode){ + if(mode === 'params'){ + value = mapInterpolationParameters(value, htmlEscapeValue); } + return value; } }; - // Support legacy strategy name 'escaped' for backwards compatability. ToDo: should be removed in 3.0 + // Support legacy strategy name 'escaped' for backwards compatability. ToDo: should be removed in 3.0. provider.strategies.escaped = provider.strategies.escapeParameters; + /** + * Adds a sanitization strategy to the list of known strategies. + * @param {string} strategyName + * @param {Function} strategyFunction + */ + provider.addStrategy = function(strategyName, strategyFunction){ + provider.strategies[strategyName] = strategyFunction; + }; + + /** + * Removes a sanitization strategy from the list of known strategies. + * @param {string} strategyName + */ + provider.removeStrategy = function(strategyName){ + delete provider.strategies[strategyName]; + }; + + /** + * Selects a sanitization strategy. When an array is provided the strategies will be executed in order. + * @param {string|Function|Array} strategy The sanitization strategy / strategies which should be used. Either a name of an existing strategy, a custom strategy function, or an array consisting of multiple names and / or custom functions. + * @returns {$translateSanitizationProvider} + */ provider.useStrategy = function(strategy){ + hasConfiguredStrategy = true; currentStrategy = strategy; return this; }; - provider.$get = function($injector){ + provider.$get = function($injector, $log){ + + var applyStrategies = function(value, mode, strategies){ + angular.forEach(strategies, function(strategy){ + if(angular.isFunction(strategy)){ + value = strategy(value, mode); + } + else if(angular.isFunction(provider.strategies[strategy])){ + value = provider.strategies[strategy](value, mode); + } + else{ + throw new Error('pascalprecht.translate.$translateSanitization: Unknown sanitization strategy: \'' + strategy + '\''); + } + }); + return value; + }; + + // ToDo: should be removed in 3.0 + var showNoStrategyConfiguredWarning = function(){ + if(!hasConfiguredStrategy && !hasShownNoStrategyConfiguredWarning){ + $log.warn('pascalprecht.translate.$translateSanitization: No sanitization strategy has been configured. This can have serious security implications. See http://angular-translate.github.io/docs/#/guide/19_security for details.'); + hasShownNoStrategyConfiguredWarning = true; + } + }; + if($injector.has('$sanitize')){ $sanitize = $injector.get('$sanitize'); } return { + /** + * Selects a sanitization strategy. When an array is provided the strategies will be executed in order. + * @param {string|Function|Array} strategy The sanitization strategy / strategies which should be used. Either a name of an existing strategy, a custom strategy function, or an array consisting of multiple names and / or custom functions. + * @returns {$translateSanitizationProvider} + */ useStrategy: provider.useStrategy, - sanitize: function(value, strategy){ + /** + * Sanitizes a value. + * @param {*} value The value which should be sanitized. + * @param {string} mode The current sanitization mode, either 'params' or 'text'. + * @param {string|Function|Array} [strategy] Optional custom strategy which should be used instead of the currently selected strategy. + * @returns {*} + */ + sanitize: function(value, mode, strategy){ + if(!strategy && !currentStrategy){ + showNoStrategyConfiguredWarning(); + return value; + } + strategy = strategy || currentStrategy; var strategies = angular.isArray(strategy) ? strategy : [strategy]; - angular.forEach(strategies, function(strategy){ - if(angular.isFunction(provider.strategies[strategy])){ - value = provider.strategies[strategy](value); - } - else if(angular.isFunction(strategy)){ - value = strategy(value); - } - else{ - throw new Error('pascalprecht.translate.$translateSanitization: Unknown sanitization strategy: \'' + strategy + '\''); - } - }); - return value; + + return applyStrategies(value, mode, strategies); } }; }; @@ -89,27 +149,24 @@ function $translateSanitizationProvider(){ var htmlSanitizeValue = function(value){ if(!$sanitize){ - throw new Error('pascalprecht.translate.$translateSanitization: Error cannot find $sanitize service. Either include the ngSanitize module (https://docs.angularjs.org/api/ngSanitize) or use a sanitization strategy which does not depend on $sanitize, such as \'escape\'.') + throw new Error('pascalprecht.translate.$translateSanitization: Error cannot find $sanitize service. Either include the ngSanitize module (https://docs.angularjs.org/api/ngSanitize) or use a sanitization strategy which does not depend on $sanitize, such as \'escape\'.'); } return $sanitize(value); }; - var mapInterpolationParameters = function(parameters, iteratee){ - var result = {}; - for(var key in parameters){ - if(Object.prototype.hasOwnProperty.call(parameters, key)){ - var value = parameters[key]; - if(angular.isNumber(value)){ - result[key] = value; - } - else if(angular.isObject(value)){ - result[key] = mapInterpolationParameters(value, iteratee); - } - else{ - result[key] = iteratee(value); - } - } + var mapInterpolationParameters = function(value, iteratee){ + if(angular.isObject(value)){ + var result = angular.isArray(value) ? [] : {}; + angular.forEach(value, function(propertyValue, propertyKey){ + result[propertyKey] = mapInterpolationParameters(propertyValue, iteratee); + }); + return result; + } + else if(angular.isNumber(value)){ + return value; + } + else{ + return iteratee(value); } - return result; }; }