Completed
Push — master ( df3dfe...6032cf )
by Delete
03:27
created

Reader::getBrowser()   C

Complexity

Conditions 9
Paths 80

Size

Total Lines 94
Code Lines 68

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 94
rs 5.0297
cc 9
eloc 68
nc 80
nop 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
356
            }
357
        }
358
359
        return $this->sqliteVersion;
360
    }
361
362
    /**
363
     * @param string $userAgent
364
     *
365
     * @return int
366
     * @throws ParserConditionNotSatisfiedException
367
     * @throws ParserRuntimeException
368
     * @throws UnexpectedValueException
369
     */
370
    protected function getBrowserId(string $userAgent) : int
371
    {
372
        if ($this->browserId === null) {
373
            $this->findBrowser($userAgent);
374
        }
375
376
        return $this->browserId;
377
    }
378
379
    /**
380
     * @param string $userAgent
381
     *
382
     * @return int
383
     * @throws ParserConditionNotSatisfiedException
384
     * @throws ParserRuntimeException
385
     * @throws UnexpectedValueException
386
     */
387
    protected function getBrowserParentId(string $userAgent) : int
388
    {
389
        if ($this->browserParentId === null) {
390
            $this->findBrowser($userAgent);
391
        }
392
393
        return $this->browserParentId;
394
    }
395
396
    /**
397
     * @param string $userAgent
398
     *
399
     * @throws ParserConditionNotSatisfiedException
400
     * @throws ParserRuntimeException
401
     * @throws UnexpectedValueException
402
     */
403
    protected function findBrowser(string $userAgent)
404
    {
405
        // Check each keyword table for the browser pattern
406
        $this->findBrowserInKeywordTables($userAgent);
407
408
        // If no match found in keyword tables, check the default table
409
        // (this also includes the '*' pattern for the default match).
410
        if ($this->browserId === null) {
411
            $this->findBrowserInDefaultTable($userAgent);
412
        }
413
414
        // Check if data found (the last step should always find the default settings)
415
        if ($this->browserId === null) {
416
            throw new ParserRuntimeException(
417
                "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...
418
            );
419
        }
420
    }
421
422
    /**
423
     * @param string $userAgent
424
     * @throws ParserConditionNotSatisfiedException
425
     * @throws ParserRuntimeException
426
     * @throws UnexpectedValueException
427
     */
428
    protected function findBrowserInKeywordTables(string $userAgent)
429
    {
430
        $query  = 'SELECT browser_id, browser_pattern_length FROM "search_[keyword]" ';
431
        $query .= 'WHERE browser_pattern_length >= :length AND :agent GLOB browser_pattern';
432
433
        $userAgentLowered = strtolower($userAgent);
434
        $maxLength = 0;
435
        $browserId = null;
436
        foreach ($this->getPatternKeywords() as $patternKeyword) {
437
            if (strpos($userAgentLowered, $patternKeyword) !== false) {
438
                $statement = $this->getAdapter()->prepare(str_replace('[keyword]', $patternKeyword, $query));
439
440
                /** @noinspection PdoApiUsageInspection */
441
                foreach ($statement->execute(['length' => $maxLength, 'agent' => $userAgentLowered]) as $row) {
442
                    $tmpLength = (int)$row['browser_pattern_length'];
443
                    $tmpBrowserId = (int)$row['browser_id'];
444
445
                    if ($tmpLength < $maxLength) {
446
                        continue; // @codeCoverageIgnore
447
                    }
448
                    if ($tmpLength === $maxLength && $browserId !== null && $tmpBrowserId < $browserId) {
449
                        continue; // @codeCoverageIgnore
450
                    }
451
452
                    $browserId = (int)$row['browser_id'];
453
                    $maxLength = $tmpLength;
454
                }
455
            }
456
        }
457
458
        if ($browserId !== null) {
459
            $this->browserId = $browserId;
460
461
            $query = 'SELECT browser_parent_id FROM browser WHERE browser_id = :id';
462
            $statement = $this->getAdapter()->prepare($query);
463
            /** @noinspection PdoApiUsageInspection */
464
            foreach ($statement->execute(['id' => $browserId]) as $row) {
465
                $this->browserParentId = (int)$row['browser_parent_id'];
466
            }
467
        }
468
    }
469
470
    /**
471
     * @param string $userAgent
472
     *
473
     * @throws ParserConditionNotSatisfiedException
474
     * @throws ParserRuntimeException
475
     * @throws UnexpectedValueException
476
     */
477
    protected function findBrowserInDefaultTable(string $userAgent)
478
    {
479
        // Build query with default table
480
        $query  = 'SELECT t2.browser_id, t2.browser_parent_id FROM ';
481
        $query .= '(SELECT MIN(browser_id) AS browser_id FROM search WHERE :agent GLOB browser_pattern) t1 ';
482
        $query .= 'JOIN browser t2 ON t2.browser_id = t1.browser_id';
483
        $statement = $this->getAdapter()->prepare($query);
484
485
        /** @noinspection PdoApiUsageInspection */
486
        foreach ($statement->execute(['agent' => strtolower($userAgent)]) as $row) {
487
            $this->browserId = (int)$row['browser_id'];
488
            $this->browserParentId = (int)$row['browser_parent_id'];
489
        }
490
    }
491
492
    /**
493
     * @return array
494
     * @throws ParserConditionNotSatisfiedException
495
     * @throws ParserRuntimeException
496
     * @throws UnexpectedValueException
497
     */
498
    protected function getPatternKeywords() : array
499
    {
500
        if ($this->browserPatternKeywords === null) {
501
            $this->browserPatternKeywords = [];
502
            $query = 'SELECT keyword_value FROM keyword ORDER BY keyword_id';
503
            foreach ($this->getAdapter()->query($query) as $row) {
504
                $this->browserPatternKeywords[] = $row['keyword_value'];
505
            }
506
        }
507
508
        return $this->browserPatternKeywords;
509
    }
510
511
    /**
512
     * @param array $properties
513
     *
514
     * @return array
515
     */
516
    protected function sortProperties(array $properties) : array
517
    {
518
        ksort($properties);
519
520
        return $properties;
521
    }
522
}
523