Issues (27)

Security Analysis    not enabled

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/Parser/Sqlite/Reader.php (6 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
declare(strict_types=1);
3
4
namespace Crossjoin\Browscap\Parser\Sqlite;
5
6
use Crossjoin\Browscap\Exception\ParserConditionNotSatisfiedException;
7
use Crossjoin\Browscap\Exception\ParserRuntimeException;
8
use Crossjoin\Browscap\Exception\UnexpectedValueException;
9
use Crossjoin\Browscap\Parser\ReaderInterface;
10
use Crossjoin\Browscap\Parser\Sqlite\Adapter\AdapterFactory;
11
use Crossjoin\Browscap\Parser\Sqlite\Adapter\AdapterInterface;
12
use Crossjoin\Browscap\PropertyFilter\PropertyFilterTrait;
13
use Crossjoin\Browscap\Source\Ini\GetRegExpForPatternTrait;
14
use Crossjoin\Browscap\Type;
15
16
/**
17
 * Class Reader
18
 *
19
 * @package Crossjoin\Browscap\Parser\Sqlite
20
 * @author Christoph Ziegenberg <[email protected]>
21
 * @link https://github.com/crossjoin/browscap
22
 */
23
class Reader implements ReaderInterface
24
{
25
    use DataDirectoryTrait;
26
    use DataVersionHashTrait;
27
    use PropertyFilterTrait;
28
    use GetRegExpForPatternTrait;
29
    
30
    /**
31
     * @var AdapterInterface
32
     */
33
    protected $adapter;
34
35
    /**
36
     * @var string
37
     */
38
    protected $databaseFile;
39
40
    /**
41
     * @var int
42
     */
43
    protected $browserId;
44
45
    /**
46
     * @var int
47
     */
48
    protected $browserParentId;
49
50
    /**
51
     * @var array
52
     */
53
    protected $browserPatternKeywords;
54
55
    /**
56
     * @var string
57
     */
58
    protected $sqliteVersion;
59
    
60
    /**
61
     * Writer constructor.
62
     *
63
     * @param string $dataDirectory
64
     *
65
     * @throws ParserConditionNotSatisfiedException
66
     */
67
    public function __construct(string $dataDirectory)
68
    {
69
        $this->setDataDirectory($dataDirectory);
70
    }
71
72
    /**
73
     * @inheritdoc
74
     *
75
     * @throws ParserConditionNotSatisfiedException
76
     * @throws ParserRuntimeException
77
     * @throws UnexpectedValueException
78
     */
79 View Code Duplication
    protected function getAdapter() : AdapterInterface
80
    {
81
        if ($this->adapter === null) {
82
            $databaseFile = $this->getDatabasePath();
83
            $adapter = AdapterFactory::getInstance($databaseFile);
84
            $this->setAdapter($adapter);
85
        }
86
87
        return $this->adapter;
88
    }
89
90
    /**
91
     * @return string
92
     *
93
     * @throws ParserRuntimeException
94
     */
95
    protected function getDatabasePath() : string
96
    {
97
        $databasePath = $this->getDataDirectory() . DIRECTORY_SEPARATOR . $this->getDatabaseFileName();
98
99
        if (!$this->isFileReadable($databasePath)) {
100
            if (!file_exists($databasePath)) {
101
                throw new ParserRuntimeException(
102
                    "Linked database file '$databasePath' not found. Parser data need to be generated.",
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $databasePath instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
103
                    1458898365
104
                );
105
            } else {
106
                throw new ParserRuntimeException("Linked database file '$databasePath' is not readable.", 1458898366);
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $databasePath instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
107
            }
108
        } elseif (!is_file($databasePath)) {
109
            throw new ParserRuntimeException("Invalid database file name '$databasePath' in link file.", 1458898367);
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $databasePath instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
110
        }
111
112
        return $databasePath;
113
    }
114
115
    /**
116
     * @return string
117
     *
118
     * @throws ParserRuntimeException
119
     */
120
    protected function getDatabaseFileName() : string
121
    {
122
        // Get database to use, saved in the link file (as symlinks are not available or only
123
        // with admin permissions on Windows).
124
        $linkFile = $this->getDataDirectory() . DIRECTORY_SEPARATOR . Parser::LINK_FILENAME;
125
        if ($this->isFileReadable($linkFile)) {
126
            return (string)file_get_contents($linkFile);
127
        } elseif (!file_exists($linkFile)) {
128
            throw new ParserRuntimeException(
129
                "Database link file '$linkFile' not found. Parser data need to be generated.",
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $linkFile instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
130
                1458898368
131
            );
132
        } else {
133
            throw new ParserRuntimeException("Database link file '$linkFile' is not readable.", 1458898369);
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $linkFile instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
134
        }
135
    }
136
137
    /**
138
     * @param $file
139
     *
140
     * @return bool
141
     */
142
    protected function isFileReadable(string $file) : bool
143
    {
144
        return is_readable($file);
145
    }
146
147
    /**
148
     * @param AdapterInterface $adapter
149
     */
150
    protected function setAdapter(AdapterInterface $adapter)
151
    {
152
        $this->adapter = $adapter;
153
    }
154
155
    /**
156
     * @inheritdoc
157
     *
158
     * @throws UnexpectedValueException
159
     */
160
    public function isUpdateRequired() : bool
161
    {
162
        try {
163
            $query = 'SELECT data_hash FROM info LIMIT 1';
164
            $dataHash = '';
165
            foreach ($this->getAdapter()->query($query) as $row) {
166
                $dataHash = $row['data_hash'];
167
            }
168
            $return = ($dataHash !== $this->getDataVersionHash());
169
        } catch (ParserConditionNotSatisfiedException $e) {
170
            $return = true;
171
        } catch (ParserRuntimeException $e) {
172
            $return = true;
173
        }
174
175
        return $return;
176
    }
177
178
    /**
179
     * @inheritdoc
180
     *
181
     * @throws ParserConditionNotSatisfiedException
182
     * @throws ParserRuntimeException
183
     * @throws UnexpectedValueException
184
     */
185
    public function getReleaseTime() : int
186
    {
187
        $query = 'SELECT release_time FROM info LIMIT 1';
188
189
        $releaseTime = 0;
190
        foreach ($this->getAdapter()->query($query) as $row) {
191
            $releaseTime = (int)$row['release_time'];
192
        }
193
194
        return $releaseTime;
195
    }
196
197
    /**
198
     * @inheritdoc
199
     *
200
     * @throws ParserConditionNotSatisfiedException
201
     * @throws ParserRuntimeException
202
     * @throws UnexpectedValueException
203
     */
204
    public function getVersion() : int
205
    {
206
        $query = 'SELECT version_id FROM info LIMIT 1';
207
208
        $versionId = 0;
209
        foreach ($this->getAdapter()->query($query) as $row) {
210
            $versionId = (int)$row['version_id'];
211
        }
212
213
        return $versionId;
214
    }
215
216
    /**
217
     * @inheritdoc
218
     *
219
     * @throws ParserConditionNotSatisfiedException
220
     * @throws ParserRuntimeException
221
     * @throws UnexpectedValueException
222
     */
223
    public function getType() : int
224
    {
225
        $query = 'SELECT type_id FROM info LIMIT 1';
226
227
        $typeId = Type::UNKNOWN;
228
        foreach ($this->getAdapter()->query($query) as $row) {
229
            $typeId = (int)$row['type_id'];
230
        }
231
232
        return $typeId;
233
    }
234
235
    /**
236
     * @inheritdoc
237
     *
238
     * @throws ParserConditionNotSatisfiedException
239
     * @throws ParserRuntimeException
240
     * @throws UnexpectedValueException
241
     */
242
    public function getBrowser(string $userAgent) : array
243
    {
244
        // Init variables
245
        $browserId = $this->getBrowserId($userAgent);
246
        $browserParentId = $this->getBrowserParentId($userAgent);
247
248
        // Get all settings for the found browser
249
        //
250
        // It's best to use a recursive query here, but some linux distributions like CentOS and RHEL
251
        // use old sqlite library versions (like 3.6.20 or 3.7.17), so that we have to check the
252
        // version here and fall back to multiple single queries in case of older versions.
253
        if (version_compare($this->getSqliteVersion(), '3.8.3', '>=')) {
254
            $query  = 'SELECT t3.property_key, t4.property_value FROM (';
255
            $query .= 'WITH RECURSIVE browser_recursive(browser_id,browser_parent_id) AS (';
256
            $query .= 'SELECT browser_id, browser_parent_id FROM browser WHERE browser_id = :id';
257
            $query .= ' UNION ALL ';
258
            $query .= 'SELECT browser.browser_id, browser.browser_parent_id FROM browser, browser_recursive ';
259
            $query .= 'WHERE browser_recursive.browser_parent_id IS NOT NULL ';
260
            $query .= 'AND browser.browser_id = browser_recursive.browser_parent_id';
261
            $query .= ') ';
262
            $query .= 'SELECT MAX(t2.browser_id) AS browser_id, t2.property_key_id FROM browser_recursive t1 ';
263
            $query .= 'JOIN browser_property t2 ON t2.browser_id = t1.browser_id ';
264
            $query .= 'GROUP BY t2.property_key_id';
265
            $query .= ') t1 ';
266
            $query .= 'JOIN browser_property t2 ON t2.browser_id = t1.browser_id ';
267
            $query .= 'AND t2.property_key_id = t1.property_key_id ';
268
            $query .= 'JOIN browser_property_key t3 ON t3.property_key_id = t2.property_key_id ';
269
            $query .= 'JOIN browser_property_value t4 ON t4.property_value_id = t2.property_value_id';
270
            $query .= ' UNION ALL ';
271
            $query .= 'SELECT \'browser_name_pattern\' AS property_key, browser_pattern AS property_value ';
272
            $query .= 'FROM browser WHERE browser_id = :id';
273
            $query .= ' UNION ALL ';
274
            $query .= 'SELECT \'Parent\' AS property_key, browser_pattern AS property_value ';
275
            $query .= 'FROM browser WHERE browser_id = :parent';
276
            $statement = $this->getAdapter()->prepare($query);
277
        } else {
278
            $query = 'SELECT browser_id, browser_parent_id FROM browser WHERE browser_id = :id';
279
            $statement = $this->getAdapter()->prepare($query);
280
            $browserIds = [];
281
            $lastBrowserId = $browserId;
282
            while ($lastBrowserId !== null) {
283
                $result = $statement->execute(['id' => $lastBrowserId]);
284
285
                $lastBrowserId = null;
286
                if (count($result) > 0) {
287
                    $browserIds[] = (int)$result[0]['browser_id'];
288
                    if ($result[0]['browser_parent_id'] !== null) {
289
                        $lastBrowserId = (int)$result[0]['browser_parent_id'];
290
                    }
291
                }
292
            }
293
294
            $query  = 'SELECT t3.property_key, t4.property_value FROM (';
295
            $query .= 'SELECT MAX(browser_id) AS browser_id, property_key_id ';
296
            $query .= 'FROM browser_property WHERE browser_id IN (' . implode(', ', $browserIds) . ') ';
297
            $query .= 'GROUP BY property_key_id';
298
            $query .= ') t1 ';
299
            $query .= 'JOIN browser_property t2 ON t2.browser_id = t1.browser_id ';
300
            $query .= 'AND t2.property_key_id = t1.property_key_id ';
301
            $query .= 'JOIN browser_property_key t3 ON t3.property_key_id = t2.property_key_id ';
302
            $query .= 'JOIN browser_property_value t4 ON t4.property_value_id = t2.property_value_id';
303
            $query .= ' UNION ALL ';
304
            $query .= 'SELECT \'browser_name_pattern\' AS property_key, browser_pattern AS property_value ';
305
            $query .= 'FROM browser WHERE browser_id = :id';
306
            $query .= ' UNION ALL ';
307
            $query .= 'SELECT \'Parent\' AS property_key, browser_pattern AS property_value ';
308
            $query .= 'FROM browser WHERE browser_id = :parent';
309
            $statement = $this->getAdapter()->prepare($query);
310
        }
311
312
        $properties = [];
313
        foreach ($statement->execute(['id' => $browserId, 'parent' => $browserParentId]) as $row) {
314
            $properties[$row['property_key']] = $row['property_value'];
315
        }
316
317
        // Set regular expression properties
318
        $propertyFilter = $this->getPropertyFilter();
319
        if (!$propertyFilter->isFiltered('browser_name_regex')) {
320
            $properties['browser_name_regex'] = $this->getRegExpForPattern($properties['browser_name_pattern']);
321
        }
322
        if ($propertyFilter->isFiltered('browser_name_pattern')) {
323
            unset($properties['browser_name_pattern']);
324
        }
325
        if ($propertyFilter->isFiltered('Parent')) {
326
            unset($properties['Parent']);
327
        }
328
329
        // IMPORTANT: Reset browserId and browserParentId for next call
330
        $this->browserId = null;
331
        $this->browserParentId = null;
332
333
        // The settings are in random order, so sort them
334
        return $this->sortProperties($properties);
335
    }
336
337
    /**
338
     * @return string
339
     * @throws \Crossjoin\Browscap\Exception\UnexpectedValueException
340
     * @throws \Crossjoin\Browscap\Exception\ParserConditionNotSatisfiedException
341
     */
342
    protected function getSqliteVersion(): string
343
    {
344
        if ($this->sqliteVersion === null) {
345
346
            $this->sqliteVersion = '0.0.0';
347
348
            // Try to get the version number
349
            $query = 'SELECT sqlite_version() AS version';
350
            try {
351
                $result = $this->getAdapter()->query($query);
352
                if (count($result) > 0) {
353
                    $this->sqliteVersion = $result[0]['version'];
354
                }
355
            } catch (ParserRuntimeException $exception) {
356
                // Use default if version could not be detected
357
            }
358
        }
359
360
        return $this->sqliteVersion;
361
    }
362
363
    /**
364
     * @param string $userAgent
365
     *
366
     * @return int
367
     * @throws ParserConditionNotSatisfiedException
368
     * @throws ParserRuntimeException
369
     * @throws UnexpectedValueException
370
     */
371
    protected function getBrowserId(string $userAgent) : int
372
    {
373
        if ($this->browserId === null) {
374
            $this->findBrowser($userAgent);
375
        }
376
377
        return $this->browserId;
378
    }
379
380
    /**
381
     * @param string $userAgent
382
     *
383
     * @return int
384
     * @throws ParserConditionNotSatisfiedException
385
     * @throws ParserRuntimeException
386
     * @throws UnexpectedValueException
387
     */
388
    protected function getBrowserParentId(string $userAgent) : int
389
    {
390
        if ($this->browserParentId === null) {
391
            $this->findBrowser($userAgent);
392
        }
393
394
        return $this->browserParentId;
395
    }
396
397
    /**
398
     * @param string $userAgent
399
     *
400
     * @throws ParserConditionNotSatisfiedException
401
     * @throws ParserRuntimeException
402
     * @throws UnexpectedValueException
403
     */
404
    protected function findBrowser(string $userAgent)
405
    {
406
        // Check each keyword table for the browser pattern
407
        $this->findBrowserInKeywordTables($userAgent);
408
409
        // If no match found in keyword tables, check the default table
410
        // (this also includes the '*' pattern for the default match).
411
        if ($this->browserId === null) {
412
            $this->findBrowserInDefaultTable($userAgent);
413
        }
414
415
        // Check if data found (the last step should always find the default settings)
416
        if ($this->browserId === null) {
417
            throw new ParserRuntimeException(
418
                "No result found for user agent '$userAgent'. There seems to be something wrong with the data."
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $userAgent instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
419
            );
420
        }
421
    }
422
423
    /**
424
     * @param string $userAgent
425
     * @throws ParserConditionNotSatisfiedException
426
     * @throws ParserRuntimeException
427
     * @throws UnexpectedValueException
428
     */
429
    protected function findBrowserInKeywordTables(string $userAgent)
430
    {
431
        $query  = 'SELECT browser_id, browser_pattern_length FROM "search_[keyword]" ';
432
        $query .= 'WHERE browser_pattern_length >= :length AND :agent GLOB browser_pattern';
433
434
        $userAgentLowered = strtolower($userAgent);
435
        $maxLength = 0;
436
        $browserId = null;
437
        foreach ($this->getPatternKeywords() as $patternKeyword) {
438
            if (strpos($userAgentLowered, $patternKeyword) !== false) {
439
                $statement = $this->getAdapter()->prepare(str_replace('[keyword]', $patternKeyword, $query));
440
441
                /** @noinspection PdoApiUsageInspection */
442
                foreach ($statement->execute(['length' => $maxLength, 'agent' => $userAgentLowered]) as $row) {
443
                    $tmpLength = (int)$row['browser_pattern_length'];
444
                    $tmpBrowserId = (int)$row['browser_id'];
445
446
                    if ($tmpLength < $maxLength) {
447
                        continue; // @codeCoverageIgnore
448
                    }
449
                    if ($tmpLength === $maxLength && $browserId !== null && $tmpBrowserId < $browserId) {
450
                        continue; // @codeCoverageIgnore
451
                    }
452
453
                    $browserId = (int)$row['browser_id'];
454
                    $maxLength = $tmpLength;
455
                }
456
            }
457
        }
458
459
        if ($browserId !== null) {
460
            $this->browserId = $browserId;
461
462
            $query = 'SELECT browser_parent_id FROM browser WHERE browser_id = :id';
463
            $statement = $this->getAdapter()->prepare($query);
464
            /** @noinspection PdoApiUsageInspection */
465
            foreach ($statement->execute(['id' => $browserId]) as $row) {
466
                $this->browserParentId = (int)$row['browser_parent_id'];
467
            }
468
        }
469
    }
470
471
    /**
472
     * @param string $userAgent
473
     *
474
     * @throws ParserConditionNotSatisfiedException
475
     * @throws ParserRuntimeException
476
     * @throws UnexpectedValueException
477
     */
478
    protected function findBrowserInDefaultTable(string $userAgent)
479
    {
480
        // Build query with default table
481
        $query  = 'SELECT t2.browser_id, t2.browser_parent_id FROM ';
482
        $query .= '(';
483
        $query .= 'SELECT browser_id ';
484
        $query .= 'FROM search ';
485
        $query .= 'WHERE :agent GLOB browser_pattern ';
486
        $query .= 'ORDER BY browser_pattern_length DESC, browser_id ';
487
        $query .= 'LIMIT 1';
488
        $query .= ') t1 ';
489
        $query .= 'JOIN browser t2 ON t2.browser_id = t1.browser_id';
490
        $statement = $this->getAdapter()->prepare($query);
491
492
        /** @noinspection PdoApiUsageInspection */
493
        foreach ($statement->execute(['agent' => strtolower($userAgent)]) as $row) {
494
            $this->browserId = (int)$row['browser_id'];
495
            $this->browserParentId = (int)$row['browser_parent_id'];
496
        }
497
    }
498
499
    /**
500
     * @return array
501
     * @throws ParserConditionNotSatisfiedException
502
     * @throws ParserRuntimeException
503
     * @throws UnexpectedValueException
504
     */
505
    protected function getPatternKeywords() : array
506
    {
507
        if ($this->browserPatternKeywords === null) {
508
            $this->browserPatternKeywords = [];
509
            $query = 'SELECT keyword_value FROM keyword ORDER BY keyword_id';
510
            foreach ($this->getAdapter()->query($query) as $row) {
511
                $this->browserPatternKeywords[] = $row['keyword_value'];
512
            }
513
        }
514
515
        return $this->browserPatternKeywords;
516
    }
517
518
    /**
519
     * @param array $properties
520
     *
521
     * @return array
522
     */
523
    protected function sortProperties(array $properties) : array
524
    {
525
        ksort($properties);
526
527
        return $properties;
528
    }
529
}
530