1/** 2 * @fileoverview CSP-Safe Interpolation for angular-translate 3 * @description 4 * A Content Security Policy (CSP) compliant interpolation service for angular-translate 5 * that implements a subset of ICU MessageFormat syntax without using eval() or new Function(). 6 * 7 * This implementation provides a secure alternative to the standard messageformat.js library, 8 * which is blocked by CSP policies that prohibit 'unsafe-eval'. 9 * 10 * @usage 11 * This module is automatically loaded in place of messageformat.js and 12 * angular-translate-interpolation-messageformat.min.js when CSP mode is enabled. 13 */ 14/* global angular */ 15(function () { 16 'use strict'; 17 18 // Cache for parsed template structures (max 100 entries to prevent memory leaks) 19 var templateCache = new Map(); 20 var cacheKeys = []; 21 var MAX_CACHE_SIZE = 100; 22 23 /** 24 * Find the index of the closing brace that matches the opening brace at pos. 25 * str[pos] must be '{'. 26 * Returns the index of the matching '}', or -1 if not found. 27 */ 28 function findMatchingClose(str, pos, len) { 29 var depth = 1; 30 var i = pos + 1; 31 while (i < len) { 32 var c = str.charCodeAt(i); 33 if (c === 123) { // '{' 34 depth++; 35 } else if (c === 125) { // '}' 36 depth--; 37 if (depth === 0) { 38 return i; 39 } 40 } 41 i++; 42 } 43 return -1; // unmatched 44 } 45 46 /** 47 * Find the index of the first top-level comma (not inside nested braces) in str, 48 * starting from startPos. 49 * Returns -1 if not found. 50 */ 51 function findTopLevelComma(str, startPos, len) { 52 var depth = 0; 53 for (var i = startPos; i < len; i++) { 54 var c = str.charCodeAt(i); 55 if (c === 123) { // '{' 56 depth++; 57 } else if (c === 125) { // '}' 58 depth--; 59 } else if (c === 44 && depth === 0) { // ',' 60 return i; 61 } 62 } 63 return -1; 64 } 65 66 /** 67 * Trim whitespace from start and end of string (inline for performance) 68 */ 69 function fastTrim(str, start, end) { 70 while (start < end && str.charCodeAt(start) <= 32) { 71 start++; 72 } 73 while (end > start && str.charCodeAt(end - 1) <= 32) { 74 end--; 75 } 76 return str.substring(start, end); 77 } 78 79 /** 80 * Safe own-property check that doesn't rely on the target having 81 * Object.prototype in its chain (avoids no-prototype-builtins lint warnings). 82 */ 83 function hasOwn(obj, key) { 84 return Object.prototype.hasOwnProperty.call(obj, key); 85 } 86 87 /** 88 * Safe dynamic-key read of a plain object. 89 * Only used for the `params` object passed in by angular-translate's 90 * interpolate(str, params) call â its shape is dictated by that external 91 * caller, so unlike the internal `cases`/`templateCache` maps above it 92 * can't be swapped for a Map. Existence is verified via hasOwn() 93 * immediately before the read, justifying the lint suppression below. 94 */ 95 function getOwn(obj, key) { 96 if (!hasOwn(obj, key)) { 97 return undefined; 98 } 99 // eslint-disable-next-line security/detect-object-injection -- key presence verified by hasOwn() above 100 return obj[key]; 101 } 102 103 /** 104 * Parse ICU-style cases from a string like: 1{text} other{text} 105 * Returns a Map of each case key to its raw (unprocessed) content string. 106 */ 107 function parseCases(casesStr) { 108 var cases = new Map(); 109 var i = 0; 110 var len = casesStr.length; 111 112 while (i < len) { 113 // Skip whitespace between cases 114 while (i < len && casesStr.charCodeAt(i) <= 32) { 115 i++; 116 } 117 if (i >= len) { 118 break; 119 } 120 121 // Read the case key (everything up to the next '{') 122 var keyStart = i; 123 while (i < len && casesStr.charCodeAt(i) !== 123) { // '{' 124 i++; 125 } 126 if (i >= len) { 127 break; // malformed 128 } 129 var key = fastTrim(casesStr, keyStart, i); 130 131 // i now points at '{' â find its matching '}' 132 var closePos = findMatchingClose(casesStr, i, len); 133 if (closePos === -1) { 134 break; // malformed 135 } 136 137 cases.set(key, casesStr.substring(i + 1, closePos)); 138 i = closePos + 1; 139 } 140 141 return cases; 142 } 143 144 /** 145 * Recursively process a translation string, resolving all {â¦} tokens. 146 * Uses array-based string building for better performance. 147 * @param {string} str The string (or sub-string) to process. 148 * @param {object} params Key/value interpolation parameters. 149 * @return {string} The resolved string. 150 */ 151 function process(str, params) { 152 if (!str || typeof str !== 'string') { 153 return str || ''; 154 } 155 156 var len = str.length; 157 var result = []; 158 var i = 0;
159 var lastPos = 0; 160 161 // Early exit: check if there are any placeholders at all 162 if (str.indexOf('{') === -1) { 163 return str; 164 } 165 166 while (i < len) { 167 if (str.charCodeAt(i) !== 123) { // not '{' 168 i++; 169 continue; 170 } 171 172 // Add the literal string before this placeholder 173 if (i > lastPos) { 174 result.push(str.substring(lastPos, i)); 175 } 176 177 // Find the matching closing brace for this opening brace 178 var closePos = findMatchingClose(str, i, len); 179 if (closePos === -1) { 180 // Unmatched '{' â treat as a literal character 181 result.push('{'); 182 i++; 183 lastPos = i; 184 continue; 185 } 186 187 // inner is everything between the outer { and } 188 var inner = str.substring(i + 1, closePos); 189 var innerLen = inner.length; 190 191 // Look for the first top-level comma inside inner 192 var firstComma = findTopLevelComma(inner, 0, innerLen); 193 194 if (firstComma === -1) { 195 // Simple variable substitution: {VARNAME} 196 var varName = fastTrim(inner, 0, innerLen); 197 if (hasOwn(params, varName)) { 198 result.push(String(getOwn(params, varName))); 199 } else { 200 result.push('{', varName, '}'); 201 } 202 } else { 203 // Complex form: {VARNAME, formatType, casesâ¦} 204 var complexVarName = fastTrim(inner, 0, firstComma); 205 206 var afterFirstComma = inner.substring(firstComma + 1); 207 var afterLen = afterFirstComma.length; 208 var secondComma = findTopLevelComma(afterFirstComma, 0, afterLen); 209 210 if (secondComma === -1) { 211 // Only one comma â malformed, emit as literal 212 result.push('{', inner, '}'); 213 } else { 214 var formatType = fastTrim(afterFirstComma, 0, secondComma); 215 var casesStr = afterFirstComma.substring(secondComma + 1); 216 217 if (formatType === 'select' || formatType === 'plural') { 218 var cases = parseCases(casesStr); 219 220 // Pick the matching case, then fall back to 'other' 221 var rawValue = hasOwn(params, complexVarName) ? String(getOwn(params, complexVarName)) : null; 222 var selectedCase; 223 224 if (formatType === 'plural') { 225 var numValue = rawValue !== null ? Number(rawValue) : NaN; 226 var hasNumericValue = !isNaN(numValue); 227 var exactKey; 228 // 1) Exact-number match: =n 229 if (hasNumericValue) { 230 exactKey = '=' + numValue; 231 if (cases.has(exactKey)) { 232 selectedCase = cases.get(exactKey); 233 } 234 } 235 // 2) Basic plural-category selection (one / other) if not matched yet 236 if (!selectedCase && hasNumericValue) { 237 var pluralCategory = (numValue === 1 ? 'one' : 'other'); 238 if (cases.has(pluralCategory)) { 239 selectedCase = cases.get(pluralCategory); 240 } 241 } 242 // 3) Fall back to raw-value key (for non-standard patterns like "1 {..}") 243 if (!selectedCase && rawValue !== null && cases.has(rawValue)) { 244 selectedCase = cases.get(rawValue); 245 } 246 // 4) Final fallback to "other" 247 if (!selectedCase && cases.has('other')) { 248 selectedCase = cases.get('other'); 249 } 250 if (!selectedCase) { 251 selectedCase = ''; 252 } else { 253 // Replace '#' with the numeric value for plural forms 254 var numberString = hasNumericValue ? String(numValue) : (rawValue !== null ? rawValue : ''); 255 if (numberString) { 256 selectedCase = selectedCase.replace(/#/g, numberString); 257 } 258 } 259 } else { 260 // 'select' format: use raw value as key 261 if (rawValue !== null && cases.has(rawValue)) { 262 selectedCase = cases.get(rawValue); 263 } else if (cases.has('other')) { 264 selectedCase = cases.get('other'); 265 } else { 266 selectedCase = ''; 267 } 268 } 269 270 // Recursively process nested variables in the selected case 271 result.push(process(selectedCase, params)); 272 } else { 273 // Unknown format type â emit as literal 274 result.push('{', inner, '}'); 275 } 276 } 277 } 278 279 i = closePos + 1;
280 lastPos = i; 281 } 282 283 // Add remaining literal string 284 if (lastPos < len) { 285 result.push(str.substring(lastPos)); 286 } 287 288 return result.join(''); 289 } 290 291 // ------------------------------------------------------------------------- 292 // Angular service registration 293 // ------------------------------------------------------------------------- 294 295 angular.module('WebModule').factory('cspSafeInterpolation', function () { 296 var _locale = 'en'; 297 298 return { 299 setLocale: function (value) { 300 _locale = value; 301 // Clear cache when locale changes as translations may differ 302 templateCache = new Map(); 303 cacheKeys = []; 304 }, 305 306 getInterpolationIdentifier: function () { 307 return 'cspSafeInterpolation'; 308 }, 309 310 /** 311 * @param {string} str The raw translation string 312 * @param {object} interpolateParams Variable values to substitute 313 * @return {string} 314 */ 315 interpolate: function (str, interpolateParams) { 316 if (!str) { 317 return ''; 318 } 319 320 // For simple strings without placeholders, return immediately 321 if (typeof str === 'string' && str.indexOf('{') === -1) { 322 return str; 323 } 324 325 // Optional: Use caching for frequently accessed translations 326 // Only cache if no parameters to avoid cache bloat 327 var cacheKey; 328 if (!interpolateParams || Object.keys(interpolateParams).length === 0) { 329 if (templateCache.has(str)) { 330 return templateCache.get(str); 331 } 332 cacheKey = str; 333 } 334 335 var result = process(str, interpolateParams || {}); 336 337 // Cache the result if applicable 338 if (cacheKey) { 339 if (cacheKeys.length >= MAX_CACHE_SIZE) { 340 // Remove oldest entry 341 var oldestKey = cacheKeys.shift(); 342 templateCache.delete(oldestKey); 343 } 344 templateCache.set(cacheKey, result); 345 cacheKeys.push(cacheKey); 346 } 347 348 return result; 349 } 350 }; 351 }); 352 353}());
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.