This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** @noinspection PhpComposerExtensionStubsInspection */ |
||
3 | |||
4 | namespace Gettext\Extractors; |
||
5 | |||
6 | use DOMAttr; |
||
7 | use DOMDocument; |
||
8 | use DOMElement; |
||
9 | use DOMNode; |
||
10 | use Exception; |
||
11 | use Gettext\Translations; |
||
12 | use Gettext\Utils\FunctionsScanner; |
||
13 | |||
14 | /** |
||
15 | * Class to get gettext strings from VueJS template files. |
||
16 | */ |
||
17 | class VueJs extends Extractor implements ExtractorInterface, ExtractorMultiInterface |
||
18 | { |
||
19 | public static $options = [ |
||
20 | 'constants' => [], |
||
21 | |||
22 | 'functions' => [ |
||
23 | 'gettext' => 'gettext', |
||
24 | '__' => 'gettext', |
||
25 | 'ngettext' => 'ngettext', |
||
26 | 'n__' => 'ngettext', |
||
27 | 'pgettext' => 'pgettext', |
||
28 | 'p__' => 'pgettext', |
||
29 | 'dgettext' => 'dgettext', |
||
30 | 'd__' => 'dgettext', |
||
31 | 'dngettext' => 'dngettext', |
||
32 | 'dn__' => 'dngettext', |
||
33 | 'dpgettext' => 'dpgettext', |
||
34 | 'dp__' => 'dpgettext', |
||
35 | 'npgettext' => 'npgettext', |
||
36 | 'np__' => 'npgettext', |
||
37 | 'dnpgettext' => 'dnpgettext', |
||
38 | 'dnp__' => 'dnpgettext', |
||
39 | 'noop' => 'noop', |
||
40 | 'noop__' => 'noop', |
||
41 | ], |
||
42 | ]; |
||
43 | |||
44 | protected static $functionsScannerClass = 'Gettext\Utils\JsFunctionsScanner'; |
||
45 | |||
46 | /** |
||
47 | * @inheritDoc |
||
48 | * @throws Exception |
||
49 | */ |
||
50 | View Code Duplication | public static function fromFileMultiple($file, array $translations, array $options = []) |
|
0 ignored issues
–
show
|
|||
51 | { |
||
52 | foreach (static::getFiles($file) as $file) { |
||
53 | $options['file'] = $file; |
||
54 | static::fromStringMultiple(static::readFile($file), $translations, $options); |
||
55 | } |
||
56 | } |
||
57 | |||
58 | /** |
||
59 | * @inheritdoc |
||
60 | * @throws Exception |
||
61 | */ |
||
62 | public static function fromString($string, Translations $translations, array $options = []) |
||
63 | { |
||
64 | static::fromStringMultiple($string, [$translations], $options); |
||
65 | } |
||
66 | |||
67 | /** |
||
68 | * @inheritDoc |
||
69 | * @throws Exception |
||
70 | */ |
||
71 | public static function fromStringMultiple($string, array $translations, array $options = []) |
||
72 | { |
||
73 | $options += static::$options; |
||
74 | $options += [ |
||
75 | // HTML attribute prefixes we parse as JS which could contain translations (are JS expressions) |
||
76 | 'attributePrefixes' => [ |
||
77 | ':', |
||
78 | 'v-bind:', |
||
79 | 'v-on:', |
||
80 | 'v-text', |
||
81 | ], |
||
82 | // HTML Tags to parse |
||
83 | 'tagNames' => [ |
||
84 | 'translate', |
||
85 | ], |
||
86 | // HTML tags to parse when attribute exists |
||
87 | 'tagAttributes' => [ |
||
88 | 'v-translate', |
||
89 | ], |
||
90 | // Comments |
||
91 | 'commentAttributes' => [ |
||
92 | 'translate-comment', |
||
93 | ], |
||
94 | 'contextAttributes' => [ |
||
95 | 'translate-context', |
||
96 | ], |
||
97 | // Attribute with plural content |
||
98 | 'pluralAttributes' => [ |
||
99 | 'translate-plural', |
||
100 | ], |
||
101 | ]; |
||
102 | |||
103 | // Ok, this is the weirdest hack, but let me explain: |
||
104 | // On Linux (Mac is fine), when converting HTML to DOM, new lines get trimmed after the first tag. |
||
105 | // So if there are new lines between <template> and next element, they are lost |
||
106 | // So we insert a "." which is a text node, and it will prevent that newlines are stripped between elements. |
||
107 | // Same thing happens between template and script tag. |
||
108 | $string = str_replace('<template>', '<template>.', $string); |
||
109 | $string = str_replace('</template>', '</template>.', $string); |
||
110 | |||
111 | // Normalize newlines |
||
112 | $string = str_replace(["\r\n", "\n\r", "\r"], "\n", $string); |
||
113 | |||
114 | // VueJS files are valid HTML files, we will operate with the DOM here |
||
115 | $dom = static::convertHtmlToDom($string); |
||
116 | |||
117 | $script = static::extractScriptTag($string); |
||
118 | |||
119 | // Parse the script part as a regular JS code |
||
120 | if ($script) { |
||
121 | $scriptLineNumber = $dom->getElementsByTagName('script')->item(0)->getLineNo(); |
||
122 | static::getScriptTranslationsFromString( |
||
123 | $script, |
||
0 ignored issues
–
show
It seems like
$script defined by static::extractScriptTag($string) on line 117 can also be of type boolean ; however, Gettext\Extractors\VueJs...ranslationsFromString() does only seem to accept string , maybe add an additional type check?
If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check: /**
* @return array|string
*/
function returnsDifferentValues($x) {
if ($x) {
return 'foo';
}
return array();
}
$x = returnsDifferentValues($y);
if (is_array($x)) {
// $x is an array.
}
If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue. ![]() |
|||
124 | $translations, |
||
125 | $options, |
||
126 | $scriptLineNumber - 1 |
||
127 | ); |
||
128 | } |
||
129 | |||
130 | // Template part is parsed separately, all variables will be extracted |
||
131 | // and handled as a regular JS code |
||
132 | $template = $dom->getElementsByTagName('template')->item(0); |
||
133 | if ($template) { |
||
134 | static::getTemplateTranslations( |
||
135 | $template, |
||
136 | $translations, |
||
137 | $options, |
||
138 | $template->getLineNo() - 1 |
||
139 | ); |
||
140 | } |
||
141 | } |
||
142 | |||
143 | /** |
||
144 | * Extracts script tag contents using regex instead of DOM operations. |
||
145 | * If we parse using DOM, some contents may change, for example, tags within strings will be stripped |
||
146 | * |
||
147 | * @param $string |
||
148 | * @return bool|string |
||
149 | */ |
||
150 | protected static function extractScriptTag($string) |
||
151 | { |
||
152 | if (preg_match('#<\s*?script\b[^>]*>(.*?)</script\b[^>]*>#s', $string, $matches)) { |
||
153 | return $matches[1]; |
||
154 | } |
||
155 | |||
156 | return ''; |
||
157 | } |
||
158 | |||
159 | /** |
||
160 | * @param string $html |
||
161 | * @return DOMDocument |
||
162 | */ |
||
163 | protected static function convertHtmlToDom($html) |
||
164 | { |
||
165 | $dom = new DOMDocument; |
||
166 | |||
167 | libxml_use_internal_errors(true); |
||
168 | $dom->loadHTML($html); |
||
169 | |||
170 | libxml_clear_errors(); |
||
171 | |||
172 | return $dom; |
||
173 | } |
||
174 | |||
175 | /** |
||
176 | * Extract translations from script part |
||
177 | * |
||
178 | * @param string $scriptContents Only script tag contents, not the whole template |
||
179 | * @param Translations|Translations[] $translations One or multiple domain Translation objects |
||
180 | * @param array $options |
||
181 | * @param int $lineOffset Number of lines the script is offset in the vue template file |
||
182 | * @throws Exception |
||
183 | */ |
||
184 | protected static function getScriptTranslationsFromString( |
||
185 | $scriptContents, |
||
186 | $translations, |
||
187 | array $options = [], |
||
188 | $lineOffset = 0 |
||
189 | ) { |
||
190 | /** @var FunctionsScanner $functions */ |
||
191 | $functions = new static::$functionsScannerClass($scriptContents); |
||
192 | $options['lineOffset'] = $lineOffset; |
||
193 | $functions->saveGettextFunctions($translations, $options); |
||
194 | } |
||
195 | |||
196 | /** |
||
197 | * Parse template to extract all translations (element content and dynamic element attributes) |
||
198 | * |
||
199 | * @param DOMNode $dom |
||
200 | * @param Translations|Translations[] $translations One or multiple domain Translation objects |
||
201 | * @param array $options |
||
202 | * @param int $lineOffset Line number where the template part starts in the vue file |
||
203 | * @throws Exception |
||
204 | */ |
||
205 | protected static function getTemplateTranslations( |
||
206 | DOMNode $dom, |
||
207 | $translations, |
||
208 | array $options, |
||
209 | $lineOffset = 0 |
||
210 | ) { |
||
211 | // Build a JS string from all template attribute expressions |
||
212 | $fakeAttributeJs = static::getTemplateAttributeFakeJs($options, $dom); |
||
213 | |||
214 | // 1 line offset is necessary because parent template element was ignored when converting to DOM |
||
215 | static::getScriptTranslationsFromString($fakeAttributeJs, $translations, $options, $lineOffset); |
||
216 | |||
217 | // Build a JS string from template element content expressions |
||
218 | $fakeTemplateJs = static::getTemplateFakeJs($dom); |
||
219 | static::getScriptTranslationsFromString($fakeTemplateJs, $translations, $options, $lineOffset); |
||
220 | |||
221 | static::getTagTranslations($options, $dom, $translations); |
||
222 | } |
||
223 | |||
224 | /** |
||
225 | * @param array $options |
||
226 | * @param DOMNode $dom |
||
227 | * @param Translations|Translations[] $translations |
||
228 | */ |
||
229 | protected static function getTagTranslations(array $options, DOMNode $dom, $translations) |
||
230 | { |
||
231 | // Since tag scanning does not support domains, we always use the first translation given |
||
232 | $translations = is_array($translations) ? reset($translations) : $translations; |
||
233 | |||
234 | $children = $dom->childNodes; |
||
235 | for ($i = 0; $i < $children->length; $i++) { |
||
236 | $node = $children->item($i); |
||
237 | |||
238 | if (!($node instanceof DOMElement)) { |
||
239 | continue; |
||
240 | } |
||
241 | |||
242 | $translatable = false; |
||
243 | |||
244 | if (in_array($node->tagName, $options['tagNames'], true)) { |
||
245 | $translatable = true; |
||
246 | } |
||
247 | |||
248 | $attrList = $node->attributes; |
||
249 | $context = null; |
||
250 | $plural = ""; |
||
251 | $comment = null; |
||
252 | |||
253 | for ($j = 0; $j < $attrList->length; $j++) { |
||
0 ignored issues
–
show
The property
length does not seem to exist in DOMNamedNodeMap .
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name. If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading. ![]() |
|||
254 | /** @var DOMAttr $domAttr */ |
||
255 | $domAttr = $attrList->item($j); |
||
256 | // Check if this is a dynamic vue attribute |
||
257 | if (in_array($domAttr->name, $options['tagAttributes'])) { |
||
258 | $translatable = true; |
||
259 | } |
||
260 | if (in_array($domAttr->name, $options['contextAttributes'])) { |
||
261 | $context = $domAttr->value; |
||
262 | } |
||
263 | if (in_array($domAttr->name, $options['pluralAttributes'])) { |
||
264 | $plural = $domAttr->value; |
||
265 | } |
||
266 | if (in_array($domAttr->name, $options['commentAttributes'])) { |
||
267 | $comment = $domAttr->value; |
||
268 | } |
||
269 | } |
||
270 | |||
271 | if ($translatable) { |
||
272 | $translation = $translations->insert($context, trim($node->textContent), $plural); |
||
273 | $translation->addReference($options['file'], $node->getLineNo()); |
||
274 | if ($comment) { |
||
0 ignored issues
–
show
The expression
$comment of type string|null is loosely compared to true ; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.
In PHP, under loose comparison (like For '' == false // true
'' == null // true
'ab' == false // false
'ab' == null // false
// It is often better to use strict comparison
'' === false // false
'' === null // false
![]() |
|||
275 | $translation->addExtractedComment($comment); |
||
276 | } |
||
277 | } |
||
278 | |||
279 | if ($node->hasChildNodes()) { |
||
280 | static::getTagTranslations($options, $node, $translations); |
||
281 | } |
||
282 | } |
||
283 | } |
||
284 | |||
285 | /** |
||
286 | * Extract JS expressions from element attribute bindings (excluding text within elements) |
||
287 | * For example: <span :title="__('extract this')"> skip element content </span> |
||
288 | * |
||
289 | * @param array $options |
||
290 | * @param DOMNode $dom |
||
291 | * @return string JS code |
||
292 | */ |
||
293 | protected static function getTemplateAttributeFakeJs(array $options, DOMNode $dom) |
||
294 | { |
||
295 | $expressionsByLine = static::getVueAttributeExpressions($options['attributePrefixes'], $dom); |
||
296 | |||
297 | if (empty($expressionsByLine)) { |
||
298 | return ''; |
||
299 | } |
||
300 | |||
301 | $maxLines = max(array_keys($expressionsByLine)); |
||
302 | $fakeJs = ''; |
||
303 | |||
304 | for ($line = 1; $line <= $maxLines; $line++) { |
||
305 | if (isset($expressionsByLine[$line])) { |
||
306 | $fakeJs .= implode("; ", $expressionsByLine[$line]); |
||
307 | } |
||
308 | $fakeJs .= "\n"; |
||
309 | } |
||
310 | |||
311 | return $fakeJs; |
||
312 | } |
||
313 | |||
314 | /** |
||
315 | * Loop DOM element recursively and parse out all dynamic vue attributes which are basically JS expressions |
||
316 | * |
||
317 | * @param array $attributePrefixes List of attribute prefixes we parse as JS (may contain translations) |
||
318 | * @param DOMNode $dom |
||
319 | * @param array $expressionByLine [lineNumber => [jsExpression, ..], ..] |
||
320 | * @return array [lineNumber => [jsExpression, ..], ..] |
||
321 | */ |
||
322 | protected static function getVueAttributeExpressions( |
||
323 | array $attributePrefixes, |
||
324 | DOMNode $dom, |
||
325 | array &$expressionByLine = [] |
||
326 | ) { |
||
327 | $children = $dom->childNodes; |
||
328 | |||
329 | for ($i = 0; $i < $children->length; $i++) { |
||
330 | $node = $children->item($i); |
||
331 | |||
332 | if (!($node instanceof DOMElement)) { |
||
333 | continue; |
||
334 | } |
||
335 | $attrList = $node->attributes; |
||
336 | |||
337 | for ($j = 0; $j < $attrList->length; $j++) { |
||
0 ignored issues
–
show
The property
length does not seem to exist in DOMNamedNodeMap .
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name. If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading. ![]() |
|||
338 | /** @var DOMAttr $domAttr */ |
||
339 | $domAttr = $attrList->item($j); |
||
340 | |||
341 | // Check if this is a dynamic vue attribute |
||
342 | if (static::isAttributeMatching($domAttr->name, $attributePrefixes)) { |
||
343 | $line = $domAttr->getLineNo(); |
||
344 | $expressionByLine += [$line => []]; |
||
345 | $expressionByLine[$line][] = $domAttr->value; |
||
346 | } |
||
347 | } |
||
348 | |||
349 | if ($node->hasChildNodes()) { |
||
350 | $expressionByLine = static::getVueAttributeExpressions($attributePrefixes, $node, $expressionByLine); |
||
351 | } |
||
352 | } |
||
353 | |||
354 | return $expressionByLine; |
||
355 | } |
||
356 | |||
357 | /** |
||
358 | * Check if this attribute name should be parsed for translations |
||
359 | * |
||
360 | * @param string $attributeName |
||
361 | * @param string[] $attributePrefixes |
||
362 | * @return bool |
||
363 | */ |
||
364 | protected static function isAttributeMatching($attributeName, $attributePrefixes) |
||
365 | { |
||
366 | foreach ($attributePrefixes as $prefix) { |
||
367 | if (strpos($attributeName, $prefix) === 0) { |
||
368 | return true; |
||
369 | } |
||
370 | } |
||
371 | return false; |
||
372 | } |
||
373 | |||
374 | /** |
||
375 | * Extract JS expressions from within template elements (excluding attributes) |
||
376 | * For example: <span :title="skip attributes"> {{__("extract element content")}} </span> |
||
377 | * |
||
378 | * @param DOMNode $dom |
||
379 | * @return string JS code |
||
380 | */ |
||
381 | protected static function getTemplateFakeJs(DOMNode $dom) |
||
382 | { |
||
383 | $fakeJs = ''; |
||
384 | $lines = explode("\n", $dom->textContent); |
||
385 | |||
386 | // Build a fake JS file from template by extracting JS expressions within each template line |
||
387 | foreach ($lines as $line) { |
||
388 | $expressionMatched = static::parseOneTemplateLine($line); |
||
389 | |||
390 | $fakeJs .= implode("; ", $expressionMatched) . "\n"; |
||
391 | } |
||
392 | |||
393 | return $fakeJs; |
||
394 | } |
||
395 | |||
396 | /** |
||
397 | * Match JS expressions in a template line |
||
398 | * |
||
399 | * @param string $line |
||
400 | * @return string[] |
||
401 | */ |
||
402 | protected static function parseOneTemplateLine($line) |
||
403 | { |
||
404 | $line = trim($line); |
||
405 | |||
406 | if (!$line) { |
||
407 | return []; |
||
408 | } |
||
409 | |||
410 | $regex = '#\{\{(.*?)\}\}#'; |
||
411 | |||
412 | preg_match_all($regex, $line, $matches); |
||
413 | |||
414 | $matched = array_map(function ($v) { |
||
415 | return trim($v, '\'"{}'); |
||
416 | }, $matches[1]); |
||
417 | |||
418 | return $matched; |
||
419 | } |
||
420 | } |
||
421 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.