Issues (40)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Extractors/VueJs.php (5 issues)

Upgrade to new PHP Analysis Engine

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
This method seems to be duplicated in your project.

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.

Loading history...
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.

Loading history...
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.

Loading history...
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
Bug Best Practice introduced by
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 ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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.

Loading history...
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