PageSourceSearch

https://bid.napenauctions.com/js/csp-safe-interpolation.js

js napenauctions.com collected 2026-10-02 16:04:23 UTC 13,265 bytes, 353 lines download raw bytes

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.