Commit | Line | Data |
---|---|---|
6a488035 | 1 | <?php |
6a488035 | 2 | /* |
bc77d7c0 TO |
3 | +--------------------------------------------------------------------+ |
4 | | Copyright CiviCRM LLC. All rights reserved. | | |
5 | | | | |
6 | | This work is published under the GNU AGPLv3 license with some | | |
7 | | permitted exceptions and without any warranty. For full license | | |
8 | | and copyright information, see https://civicrm.org/licensing | | |
9 | +--------------------------------------------------------------------+ | |
e70a7fc0 | 10 | */ |
6a488035 TO |
11 | |
12 | /** | |
13 | * Parse Javascript content and extract translatable strings. | |
14 | * | |
15 | * @package CRM | |
ca5cec67 | 16 | * @copyright CiviCRM LLC https://civicrm.org/licensing |
6a488035 TO |
17 | */ |
18 | class CRM_Utils_JS { | |
6714d8d2 | 19 | |
6a488035 | 20 | /** |
fe482240 | 21 | * Parse a javascript file for translatable strings. |
6a488035 | 22 | * |
77855840 TO |
23 | * @param string $jsCode |
24 | * Raw Javascript code. | |
a6c01b45 | 25 | * @return array |
16b10e64 | 26 | * Array of translatable strings |
6a488035 TO |
27 | */ |
28 | public static function parseStrings($jsCode) { | |
be2fb01f | 29 | $strings = []; |
6a488035 TO |
30 | // Match all calls to ts() in an array. |
31 | // Note: \s also matches newlines with the 's' modifier. | |
32 | preg_match_all('~ | |
33 | [^\w]ts\s* # match "ts" with whitespace | |
34 | \(\s* # match "(" argument list start | |
35 | ((?:(?:\'(?:\\\\\'|[^\'])*\'|"(?:\\\\"|[^"])*")(?:\s*\+\s*)?)+)\s* | |
36 | [,\)] # match ")" or "," to finish | |
37 | ~sx', $jsCode, $matches); | |
38 | foreach ($matches[1] as $text) { | |
39 | $quote = $text[0]; | |
40 | // Remove newlines | |
41 | $text = str_replace("\\\n", '', $text); | |
42 | // Unescape escaped quotes | |
43 | $text = str_replace('\\' . $quote, $quote, $text); | |
44 | // Remove end quotes | |
45 | $text = substr(ltrim($text, $quote), 0, -1); | |
46 | $strings[$text] = $text; | |
47 | } | |
48 | return array_values($strings); | |
49 | } | |
96025800 | 50 | |
ad295ca9 TO |
51 | /** |
52 | * Identify duplicate, adjacent, identical closures and consolidate them. | |
53 | * | |
54 | * Note that you can only dedupe closures if they are directly adjacent and | |
55 | * have exactly the same parameters. | |
56 | * | |
57 | * @param array $scripts | |
58 | * Javascript source. | |
59 | * @param array $localVars | |
60 | * Ordered list of JS vars to identify the start of a closure. | |
61 | * @param array $inputVals | |
62 | * Ordered list of input values passed into the closure. | |
63 | * @return string | |
64 | * Javascript source. | |
65 | */ | |
66 | public static function dedupeClosures($scripts, $localVars, $inputVals) { | |
67 | // Example opening: (function (angular, $, _) { | |
68 | $opening = '\s*\(\s*function\s*\(\s*'; | |
69 | $opening .= implode(',\s*', array_map(function ($v) { | |
70 | return preg_quote($v, '/'); | |
71 | }, $localVars)); | |
72 | $opening .= '\)\s*\{'; | |
73 | $opening = '/^' . $opening . '/'; | |
74 | ||
75 | // Example closing: })(angular, CRM.$, CRM._); | |
76 | $closing = '\}\s*\)\s*\(\s*'; | |
77 | $closing .= implode(',\s*', array_map(function ($v) { | |
78 | return preg_quote($v, '/'); | |
79 | }, $inputVals)); | |
80 | $closing .= '\);\s*'; | |
81 | $closing = "/$closing\$/"; | |
82 | ||
83 | $scripts = array_values($scripts); | |
84 | for ($i = count($scripts) - 1; $i > 0; $i--) { | |
85 | if (preg_match($closing, $scripts[$i - 1]) && preg_match($opening, $scripts[$i])) { | |
86 | $scripts[$i - 1] = preg_replace($closing, '', $scripts[$i - 1]); | |
87 | $scripts[$i] = preg_replace($opening, '', $scripts[$i]); | |
88 | } | |
89 | } | |
90 | ||
91 | return $scripts; | |
92 | } | |
93 | ||
b047e061 TO |
94 | /** |
95 | * This is a primitive comment stripper. It doesn't catch all comments | |
96 | * and falls short of minification, but it doesn't munge Angular injections | |
97 | * and is fast enough to run synchronously (without caching). | |
98 | * | |
99 | * At time of writing, running this against the Angular modules, this impl | |
100 | * of stripComments currently adds 10-20ms and cuts ~7%. | |
101 | * | |
102 | * Please be extremely cautious about extending this. If you want better | |
103 | * minification, you should probably remove this implementation, | |
104 | * import a proper JSMin implementation, and cache its output. | |
105 | * | |
106 | * @param string $script | |
107 | * @return string | |
108 | */ | |
109 | public static function stripComments($script) { | |
110 | return preg_replace(":^\\s*//[^\n]+$:m", "", $script); | |
111 | } | |
112 | ||
a49c5ad6 | 113 | /** |
9511ca30 CW |
114 | * Decodes a js variable (not necessarily strict json but valid js) into a php variable. |
115 | * | |
116 | * This is similar to using json_decode($js, TRUE) but more forgiving about syntax. | |
a49c5ad6 CW |
117 | * |
118 | * ex. {a: 'Apple', 'b': "Banana", c: [1, 2, 3]} | |
9511ca30 CW |
119 | * Returns: [ |
120 | * 'a' => 'Apple', | |
121 | * 'b' => 'Banana', | |
122 | * 'c' => [1, 2, 3], | |
123 | * ] | |
a49c5ad6 CW |
124 | * |
125 | * @param string $js | |
126 | * @return mixed | |
127 | */ | |
128 | public static function decode($js) { | |
9511ca30 | 129 | $js = trim($js); |
d9c7a051 CW |
130 | $first = substr($js, 0, 1); |
131 | $last = substr($js, -1); | |
132 | if ($last === $first && ($first === "'" || $first === '"')) { | |
9511ca30 | 133 | // Use a temp placeholder for escaped backslashes |
3807fa18 CW |
134 | $backslash = chr(0) . 'backslash' . chr(0); |
135 | return str_replace(['\\\\', "\\'", '\\"', '\\&', '\\/', $backslash], [$backslash, "'", '"', '&', '/', '\\'], substr($js, 1, -1)); | |
a49c5ad6 | 136 | } |
d9c7a051 | 137 | if (($first === '{' && $last === '}') || ($first === '[' && $last === ']')) { |
9511ca30 CW |
138 | $obj = self::getRawProps($js); |
139 | foreach ($obj as $idx => $item) { | |
140 | $obj[$idx] = self::decode($item); | |
141 | } | |
142 | return $obj; | |
143 | } | |
144 | return json_decode($js); | |
a49c5ad6 CW |
145 | } |
146 | ||
89b24877 | 147 | /** |
10515677 | 148 | * Encodes a variable to js notation (not strict json) suitable for e.g. an angular attribute. |
89b24877 | 149 | * |
10515677 CW |
150 | * Like json_encode() but the output looks more like native javascript, |
151 | * with single quotes around strings and no unnecessary quotes around object keys. | |
89b24877 | 152 | * |
10515677 CW |
153 | * Ex input: [ |
154 | * 'a' => 'Apple', | |
155 | * 'b' => 'Banana', | |
156 | * 'c' => [1, 2, 3], | |
157 | * ] | |
158 | * Ex output: {a: 'Apple', b: 'Banana', c: [1, 2, 3]} | |
159 | * | |
160 | * @param mixed $value | |
89b24877 CW |
161 | * @return string |
162 | */ | |
163 | public static function encode($value) { | |
164 | if (is_array($value)) { | |
165 | return self::writeObject($value, TRUE); | |
166 | } | |
167 | $result = json_encode($value, JSON_UNESCAPED_SLASHES); | |
10515677 CW |
168 | // Convert double-quotes around string to single quotes |
169 | if (is_string($value) && substr($result, 0, 1) === '"' && substr($result, -1) === '"') { | |
3807fa18 CW |
170 | $backslash = chr(0) . 'backslash' . chr(0); |
171 | return "'" . str_replace(['\\\\', '\\"', "'", $backslash], [$backslash, '"', "\\'", '\\\\'], substr($result, 1, -1)) . "'"; | |
89b24877 CW |
172 | } |
173 | return $result; | |
174 | } | |
175 | ||
3203414a | 176 | /** |
9511ca30 | 177 | * Gets the properties of a javascript object/array WITHOUT decoding them. |
3203414a CW |
178 | * |
179 | * Useful when the object might contain js functions, expressions, etc. which cannot be decoded. | |
180 | * Returns an array with keys as property names and values as raw strings of js. | |
181 | * | |
9511ca30 CW |
182 | * Ex Input: {foo: getFoo(arg), 'bar': function() {return "bar";}} |
183 | * Returns: [ | |
3203414a | 184 | * 'foo' => 'getFoo(arg)', |
9511ca30 | 185 | * 'bar' => 'function() {return "bar";}', |
3203414a CW |
186 | * ] |
187 | * | |
188 | * @param $js | |
189 | * @return array | |
190 | * @throws \Exception | |
191 | */ | |
9511ca30 | 192 | public static function getRawProps($js) { |
3203414a | 193 | $js = trim($js); |
9511ca30 CW |
194 | if (!is_string($js) || $js === '' || !($js[0] === '{' || $js[0] === '[')) { |
195 | throw new Exception("Invalid js object string passed to CRM_Utils_JS::getRawProps"); | |
3203414a CW |
196 | } |
197 | $chars = str_split(substr($js, 1)); | |
9511ca30 CW |
198 | $isEscaped = $quote = NULL; |
199 | $type = $js[0] === '{' ? 'object' : 'array'; | |
200 | $key = $type == 'array' ? 0 : NULL; | |
3203414a | 201 | $item = ''; |
9511ca30 CW |
202 | $end = strlen($js) - 2; |
203 | $quotes = ['"', "'", '/']; | |
204 | $brackets = [ | |
3203414a CW |
205 | '}' => '{', |
206 | ')' => '(', | |
207 | ']' => '[', | |
9511ca30 | 208 | ':' => '?', |
3203414a | 209 | ]; |
9511ca30 | 210 | $enclosures = array_fill_keys($brackets, 0); |
3203414a CW |
211 | $result = []; |
212 | foreach ($chars as $index => $char) { | |
9511ca30 CW |
213 | if (!$isEscaped && in_array($char, $quotes, TRUE)) { |
214 | // Open quotes, taking care not to mistake the division symbol for opening a regex | |
215 | if (!$quote && !($char == '/' && preg_match('{[\w)]\s*$}', $item))) { | |
216 | $quote = $char; | |
217 | } | |
218 | // Close quotes | |
219 | elseif ($char === $quote) { | |
220 | $quote = NULL; | |
221 | } | |
3203414a CW |
222 | } |
223 | if (!$quote) { | |
3203414a | 224 | // Delineates property key |
9511ca30 | 225 | if ($char == ':' && !array_filter($enclosures) && !$key) { |
3203414a CW |
226 | $key = $item; |
227 | $item = ''; | |
3203414a CW |
228 | continue; |
229 | } | |
230 | // Delineates property value | |
9511ca30 CW |
231 | if (($char == ',' || $index == $end) && !array_filter($enclosures) && isset($key) && trim($item) !== '') { |
232 | // Trim, unquote, and unescape characters in key | |
233 | if ($type == 'object') { | |
234 | $key = trim($key); | |
235 | $key = in_array($key[0], $quotes) ? self::decode($key) : $key; | |
236 | } | |
237 | $result[$key] = trim($item); | |
238 | $key = $type == 'array' ? $key + 1 : NULL; | |
3203414a | 239 | $item = ''; |
3203414a CW |
240 | continue; |
241 | } | |
242 | // Open brackets - we'll ignore delineators inside | |
243 | if (isset($enclosures[$char])) { | |
244 | $enclosures[$char]++; | |
245 | } | |
246 | // Close brackets | |
9511ca30 CW |
247 | if (isset($brackets[$char]) && $enclosures[$brackets[$char]]) { |
248 | $enclosures[$brackets[$char]]--; | |
3203414a CW |
249 | } |
250 | } | |
251 | $item .= $char; | |
9511ca30 CW |
252 | // We are escaping the next char if this is a backslash not preceded by an odd number of backslashes |
253 | $isEscaped = $char === '\\' && ((strlen($item) - strlen(rtrim($item, '\\'))) % 2); | |
3203414a CW |
254 | } |
255 | return $result; | |
256 | } | |
257 | ||
9511ca30 CW |
258 | /** |
259 | * Converts a php array to javascript object/array notation (not strict JSON). | |
260 | * | |
261 | * Does not encode keys unless they contain special characters. | |
262 | * Does not encode values by default, so either specify $encodeValues = TRUE, | |
263 | * or pass strings of valid js/json as values (per output from getRawProps). | |
264 | * @see CRM_Utils_JS::getRawProps | |
265 | * | |
266 | * @param array $obj | |
267 | * @param bool $encodeValues | |
268 | * @return string | |
269 | */ | |
270 | public static function writeObject($obj, $encodeValues = FALSE) { | |
271 | $js = []; | |
272 | $brackets = isset($obj[0]) && array_keys($obj) === range(0, count($obj) - 1) ? ['[', ']'] : ['{', '}']; | |
273 | foreach ($obj as $key => $val) { | |
274 | if ($encodeValues) { | |
89b24877 | 275 | $val = self::encode($val); |
9511ca30 CW |
276 | } |
277 | if ($brackets[0] == '{') { | |
278 | // Enclose the key in quotes unless it is purely alphanumeric | |
279 | if (preg_match('/\W/', $key)) { | |
280 | // Prefer single quotes | |
281 | $key = preg_match('/^[\w "]+$/', $key) ? "'" . $key . "'" : json_encode($key, JSON_UNESCAPED_SLASHES); | |
282 | } | |
283 | $js[] = "$key: $val"; | |
284 | } | |
285 | else { | |
286 | $js[] = $val; | |
287 | } | |
288 | } | |
289 | return $brackets[0] . implode(', ', $js) . $brackets[1]; | |
290 | } | |
291 | ||
232624b1 | 292 | } |