MobileDetect   D
last analyzed

Complexity

Total Complexity 142

Size/Duplication

Total Lines 878
Duplicated Lines 12.76 %

Coupling/Cohesion

Components 2
Dependencies 10

Importance

Changes 30
Bugs 4 Features 9
Metric Value
wmc 142
c 30
b 4
f 9
lcom 2
cbo 10
dl 112
loc 878
rs 4.4444

39 Methods

Rating   Name   Duplication   Size   Complexity  
A getInstance() 0 8 2
A destroy() 0 4 1
F __construct() 0 74 14
A getHttpHeadersFromEnv() 0 12 4
B setUserAgent() 0 22 5
A setHeader() 0 7 1
A getHeader() 0 7 2
A getHeaders() 0 4 1
B standardizeHeader() 0 20 5
A getUserAgent() 0 4 1
A __callStatic() 0 14 3
A getKnownMatches() 0 4 1
A prepareVersion() 0 11 2
A prepareRegex() 0 15 2
D identityMatch() 0 31 9
C modelAndVersionMatch() 0 33 7
B modelMatch() 24 24 6
B versionMatch() 24 24 6
A matchEntity() 0 10 3
A detectDeviceModel() 8 8 2
A detectDeviceModelVersion() 8 8 2
A detectBrowserModel() 0 4 2
A detectBrowserVersion() 0 11 3
A detectOperatingSystemModel() 0 4 2
A detectOperatingSystemVersion() 0 4 1
F detect() 0 112 24
B searchForItemInDb() 0 31 6
A searchPhonesProvider() 11 11 3
A searchTabletsProvider() 11 11 3
A searchBrowsersProvider() 13 13 4
A searchOperatingSystemsProvider() 13 13 4
A regexMatch() 0 4 1
A regexErrorHandler() 0 10 2
A setRegexErrorHandler() 0 5 1
A restoreRegexErrorHandler() 0 5 1
A setCacheSetter() 0 6 1
A setCacheGetter() 0 6 1
A getFromCache() 0 10 2
A setCache() 0 10 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like MobileDetect often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MobileDetect, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Mobile Detect Library
4
 * =====================
5
 *
6
 * Motto: "Every business should have a mobile detection script to detect mobile readers"
7
 *
8
 * Mobile_Detect is a lightweight PHP class for detecting mobile devices (including tablets).
9
 * It uses the User-Agent string combined with specific HTTP headers to detect the mobile environment.
10
 *
11
 *              Current authors (2.x - 3.x)
12
 * @author      Serban Ghita <[email protected]>
13
 * @author      Nick Ilyin <[email protected]>
14
 *
15
 *              Original author (1.0)
16
 * @author      Victor Stanciu <[email protected]>
17
 *
18
 * @license     Code and contributions have 'MIT License'
19
 *              More details: https://github.com/serbanghita/Mobile-Detect/blob/master/LICENSE.txt
20
 *
21
 * @link        Homepage:     http://mobiledetect.net
22
 *              GitHub Repo:  https://github.com/serbanghita/Mobile-Detect
23
 *              Google Code:  http://code.google.com/p/php-mobile-detect/
24
 *              README:       https://github.com/serbanghita/Mobile-Detect/blob/master/README.md
25
 *              Examples:     https://github.com/serbanghita/Mobile-Detect/wiki/Code-examples
26
 *
27
 * @version     3.0.0-alpha
28
 */
29
namespace MobileDetect;
30
31
use MobileDetect\Device\DeviceInterface;
32
use MobileDetect\Exception\InvalidArgumentException;
33
use Psr\Http\Message\MessageInterface as HttpMessageInterface;
34
use MobileDetect\Providers\UserAgentHeaders;
35
use MobileDetect\Providers\HttpHeaders;
36
use MobileDetect\Providers\Browsers;
37
use MobileDetect\Providers\OperatingSystems;
38
use MobileDetect\Providers\Phones;
39
use MobileDetect\Providers\Tablets;
40
use MobileDetect\Device\DeviceType;
41
42
class MobileDetect
43
{
44
    const VERSION = '3.0.0-alpha';
45
    
46
    /**
47
     * An associative array of headers in standard format.
48
     * So the keys will be "User-Agent", and "Accepts" versus
49
     * the all caps PHP format.
50
     *
51
     * @var array
52
     */
53
    protected $headers = array();
54
55
    // Database.
56
    protected $userAgentHeaders;
57
    protected $recognizedHttpHeaders;
58
    protected $phonesProvider;
59
    protected $tabletsProvider;
60
    protected $browsersProvider;
61
    protected $operatingSystemsProvider;
62
63
    protected static $knownMatchTypes = array(
64
        'regex', //regular expression
65
        'strpos', //simple case-sensitive string within string check
66
        'stripos', //simple case-insensitive string within string check
67
    );
68
69
    /**
70
     * For static invocations of this class, this holds a singleton for those methods.
71
     *
72
     * @var MobileDetect
73
     */
74
    protected static $instance;
75
76
    /**
77
     * An instance of a device when using the static methods.
78
     *
79
     * @var DeviceInterface
80
     */
81
    protected static $device;
82
83
    /**
84
     * An optionally callable cache setter for attaching a caching implementation for caching the detection of a device.
85
     *
86
     * The expected signature:
87
     *      @param  $key        string      The key identifier (i.e. the User-Agent).
88
     *      @param  $obj        DeviceInterface the device being saved.
89
     *
90
     * @var \Closure
91
     */
92
    protected $cacheSet;
93
94
    /**
95
     * An optionally callable cache getter for attaching a caching implementation for caching the detection of a device.
96
     *
97
     * The expected signature:
98
     *     @param $key string The key identifier (i.e. the User-Agent).
99
     *     @return DeviceInterface|null
100
     *
101
     * @var \Closure
102
     */
103
    protected $cacheGet;
104
105
    /**
106
     * Generates or gets a singleton for use with the simple API.
107
     *
108
     * @return MobileDetect A single instance for using with the simple static API.
109
     */
110
    public static function getInstance()
111
    {
112
        if (!static::$instance) {
113
            static::$instance = new static();
114
        }
115
116
        return static::$instance;
117
    }
118
119
    public static function destroy()
120
    {
121
        static::$instance = null;
122
    }
123
124
    /**
125
     * @param $headers \Iterator|array|HttpMessageInterface|string When it's a string, it's assumed to be User-Agent.
126
     * @param Phones|null $phonesProvider
127
     * @param Tablets|null $tabletsProvider
128
     * @param Browsers|null $browsersProvider
129
     * @param OperatingSystems|null $operatingSystemsProvider
130
     * @param UserAgentHeaders $userAgentHeaders
131
     * @param HttpHeaders $recognizedHttpHeaders
132
     */
133
    public function __construct(
134
        $headers = null,
135
        Phones $phonesProvider = null,
136
        Tablets $tabletsProvider = null,
137
        Browsers $browsersProvider = null,
138
        OperatingSystems $operatingSystemsProvider = null,
139
        UserAgentHeaders $userAgentHeaders = null,
140
        HttpHeaders $recognizedHttpHeaders = null
141
    ) {
142
143
        if (!$userAgentHeaders) {
144
            $userAgentHeaders = new UserAgentHeaders;
145
        }
146
147
        if (!$recognizedHttpHeaders) {
148
            $recognizedHttpHeaders = new HttpHeaders;
149
        }
150
151
        $this->userAgentHeaders = $userAgentHeaders;
152
        $this->recognizedHttpHeaders = $recognizedHttpHeaders;
153
154
        //parse the various types of headers we could receive
155
        if ($headers instanceof HttpMessageInterface) {
156
            $headers = $headers->getHeaders();
157
        } elseif ($headers instanceof \Iterator) {
158
            $headers = iterator_to_array($headers, true);
159
        } elseif (is_string($headers)) {
160
            $headers = array('User-Agent' => $headers);
161
        } elseif ($headers === null) {
162
            $headers = static::getHttpHeadersFromEnv();
163
        } elseif (!is_array($headers)) {
164
            throw new InvalidArgumentException(sprintf('Unexpected headers argument type=%s', gettype($headers)));
165
        }
166
167
        //load up the headers
168
        foreach ($headers as $key => $value) {
169
            try {
170
                $standardKey = $this->standardizeHeader($key);
171
                $this->headers[$standardKey] = $value;
172
            } catch (Exception\InvalidArgumentException $e) {
173
                //ignore this key and move on
174
                continue;
175
            }
176
        }
177
178
        // When no param is passed, it is detected
179
        // based on all available headers.
180
        $this->setUserAgent();
181
182
183
        if (!$phonesProvider) {
184
            $phonesProvider = new Phones;
185
        }
186
        
187
        if (!$tabletsProvider) {
188
            $tabletsProvider = new Tablets;
189
        }
190
        
191
        if (!$browsersProvider) {
192
            $browsersProvider = new Browsers;
193
        }
194
        
195
        if (!$operatingSystemsProvider) {
196
            $operatingSystemsProvider = new OperatingSystems;
197
        }
198
199
200
201
        $this->phonesProvider = $phonesProvider;
202
        $this->tabletsProvider = $tabletsProvider;
203
        $this->browsersProvider = $browsersProvider;
204
        $this->operatingSystemsProvider = $operatingSystemsProvider;
205
206
    }
207
208
    /**
209
     * Makes a best attempt at extracting headers, starting with Apache then trying $_SERVER super global.
210
     *
211
     * @return array
212
     */
213
    public static function getHttpHeadersFromEnv()
0 ignored issues
show
Coding Style introduced by
getHttpHeadersFromEnv uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
214
    {
215
        if (function_exists('getallheaders')) {
216
            return getallheaders();
217
        } elseif (function_exists('apache_request_headers')) {
218
            return apache_request_headers();
219
        } elseif (isset($_SERVER)) {
220
            return $_SERVER;
221
        }
222
223
        return array();
224
    }
225
226
    /**
227
     * Set the User-Agent to be used.
228
     * @param string $userAgent The user agent string to set.
229
     * @return MobileDetect Fluent interface.
230
     */
231
    public function setUserAgent($userAgent = null)
232
    {
233
        if ($userAgent) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $userAgent 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...
234
            $this->headers['user-agent'] = trim($userAgent);
235
236
            return $this;
237
        }
238
239
        $ua = array();
240
241
        foreach ($this->userAgentHeaders->getAll() as $altHeader) {
242
            if ($header = $this->getHeader($altHeader)) {
243
                $ua[] = $header;
244
            }
245
        }
246
247
        if (count($ua)) {
248
            $this->headers['user-agent'] = implode(' ', $ua);
249
        }
250
251
        return $this;
252
    }
253
254
    /**
255
     * Set an HTTP header.
256
     *
257
     * @param $key
258
     * @param $value
259
     * @return MobileDetect                       Fluent interface.
260
     * @throws Exception\InvalidArgumentException When the $key isn't a valid HTTP request header name.
261
     */
262
    public function setHeader($key, $value)
263
    {
264
        $key = $this->standardizeHeader($key);
265
        $this->headers[$key] = trim($value);
266
267
        return $this;
268
    }
269
270
    /**
271
     * Retrieves a header.
272
     *
273
     * @param $key string The header.
274
     * @return string|null If the header is available, it's returned. Null otherwise.
275
     */
276
    public function getHeader($key)
277
    {
278
        //normalized since access might be with a variety of cases
279
        $key = strtolower($key);
280
281
        return isset($this->headers[$key]) ? $this->headers[$key] : null;
282
    }
283
284
    public function getHeaders()
285
    {
286
        return $this->headers;
287
    }
288
289
    /**
290
     * @param $headerName string
291
     * @param $force bool Forces the header set even if it's not standard or doesn't start with "X-"
292
     * @return string The header, normalized, so HTTP_USER_AGENT becomes user-agent
293
     * @throws Exception\InvalidArgumentException When the $headerName isn't a valid HTTP request header name.
294
     */
295
    protected function standardizeHeader($headerName, $force = false)
296
    {
297
        if (strpos($headerName, 'HTTP_') === 0) {
298
            $headerName = substr($headerName, 5);
299
            $headerBits = explode('_', $headerName);
300
            $headerName = implode('-', $headerBits);
301
        }
302
303
        //all lower case to make it easier to find later
304
        $headerName = strtolower($headerName);
305
306
        //check for non-extension headers that are not standard
307
        if (!$force && $headerName[0] != 'x' && !in_array($headerName, $this->recognizedHttpHeaders->getAll())) {
308
            throw new Exception\InvalidArgumentException(
309
                sprintf("The request header %s isn't a recognized HTTP header name", $headerName)
310
            );
311
        }
312
313
        return $headerName;
314
    }
315
316
    /**
317
     * Retrieves the user agent header.
318
     * @return null|string The value or null if it doesn't exist.
319
     */
320
    public function getUserAgent()
321
    {
322
        return $this->getHeader('User-Agent');
323
    }
324
325
    /**
326
     * This method allows for static calling of methods that get proxied to the device methods. For example,
327
     * when calling Detected::getOperatingSystem() it will be proxied to static::$device->getOperatingSystem().
328
     * Since reflection is used in combination with call_user_func_array(), this method is relatively expensive
329
     * and should not be used if the developer cares about performance. This is merely a convenience method
330
     * for beginners using this detection library.
331
     *
332
     * @param string $method The method name being invoked.
333
     * @param array  $args   Arguments for the called method.
334
     *
335
     * @return mixed
336
     *
337
     * @throws \BadMethodCallException
338
     */
339
    public static function __callStatic($method, array $args = array())
340
    {
341
        //this method must exist as an instance method on self::$device
342
        if (!static::$device) {
343
            static::$device = static::getInstance()->detect();
344
        }
345
346
        if (method_exists(static::$device, $method)) {
347
            return call_user_func_array(array(static::$device, $method), $args);
348
        }
349
350
        //method not found, so yeah...
351
        throw new \BadMethodCallException(sprintf('No such method "%s" exists in Device class.', $method));
352
    }
353
354
    public static function getKnownMatches()
355
    {
356
        return self::$knownMatchTypes;
357
    }
358
359
    /**
360
     * @param $version string The string to convert to a standard version.
361
     * @param bool $asArray
362
     * @return array|string A string or an array if $asArray is passed as true.
363
     */
364
    protected function prepareVersion($version, $asArray = false)
365
    {
366
        $version = str_replace('_', '.', $version);
367
        // @todo Need to remove extra characters from resulting
368
        // versions like '2.1-' or '2.1.'
369
        if ($asArray) {
370
            return explode('.', $version);
371
        } else {
372
            return $version;
373
        }
374
    }
375
376
    /**
377
     * Converts the quasi-regex into a full regex, replacing various common placeholders such
378
     * as [VER] or [MODEL].
379
     *
380
     * @param $regex string|array
381
     *
382
     * @return string
383
     */
384
    protected function prepareRegex($regex)
385
    {
386
        // Regex can be an array, because we have some really long
387
        // expressions (eg. Samsung) and other programming languages
388
        // cannot cope with the length. See #352
389
        if (is_array($regex)) {
390
            $regex = implode('', $regex);
391
        }
392
393
        $regex = sprintf('/%s/i', addcslashes($regex, '/'));
394
        $regex = str_replace('[VER]', '(?<version>[0-9\._-]+)', $regex);
395
        $regex = str_replace('[MODEL]', '(?<model>[a-zA-Z0-9]+)', $regex);
396
397
        return $regex;
398
    }
399
400
    /**
401
     * Given a type of match, this method will check if a valid match is found.
402
     *
403
     * @param  string                             $type    The type {{@see $this->knownMatchTypes}}.
404
     * @param  string                             $test    The test subject.
405
     * @param  string                             $against The pattern (for regex) or substring (for str[i]pos).
406
     * @return bool                               True if matched successfully.
407
     * @throws Exception\InvalidArgumentException If $against isn't a string or $type is invalid.
408
     */
409
    protected function identityMatch($type, $test, $against)
410
    {
411
        if (!in_array($type, $this->getKnownMatches())) {
412
            throw new Exception\InvalidArgumentException(
413
                sprintf('Unknown match type: %s', $type)
414
            );
415
        }
416
417
        // Always take a string.
418
        if (!is_string($against)) {
419
            throw new Exception\InvalidArgumentException(
420
                sprintf('Invalid %s pattern of type "%s" passed for "%s"', $type, gettype($against), $test)
421
            );
422
        }
423
424
        if ($type == 'regex') {
425
            if ($this->regexMatch($this->prepareRegex($test), $against)) {
426
                return true;
427
            }
428
        } elseif ($type == 'strpos') {
429
            if (false !== strpos($against, $test)) {
430
                return true;
431
            }
432
        } elseif ($type == 'stripos') {
433
            if (false !== stripos($against, $test)) {
434
                return true;
435
            }
436
        }
437
438
        return false;
439
    }
440
441
    /**
442
     * Attempts to match the model and extracts
443
     * the version and model if available.
444
     *
445
     * @param $tests array Various tests.
446
     * @param $against string The test.
447
     *
448
     * @return array|bool False if no match, hash of match data otherwise.
449
     */
450
    protected function modelAndVersionMatch($tests, $against)
451
    {
452
        // Model match must be an array.
453
        if (!is_array($tests) || !count($tests)) {
454
            return false;
455
        }
456
457
        $this->setRegexErrorHandler();
458
459
        $matchReturn = array();
460
461
        foreach ($tests as $test) {
462
            $regex = $this->prepareRegex($test);
463
464
            if ($this->regexMatch($regex, $against, $matches)) {
465
                // If the match contained a version, save it.
466
                if (isset($matches['version'])) {
467
                    $matchReturn['version'] = $this->prepareVersion($matches['version']);
468
                }
469
470
                // If the match contained a model, save it.
471
                if (isset($matches['model'])) {
472
                    $matchReturn['model'] = $matches['model'];
473
                }
474
475
                $this->restoreRegexErrorHandler();
476
                return $matchReturn;
477
            }
478
        }
479
480
        $this->restoreRegexErrorHandler();
481
        return false;
482
    }
483
484
    /**
485
     * @param $tests
486
     * @param $against
487
     * @return null
488
     */
489 View Code Duplication
    protected function modelMatch($tests, $against)
0 ignored issues
show
Duplication introduced by
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...
490
    {
491
        // Model match must be an array.
492
        if (!is_array($tests) || !count($tests)) {
493
            return null;
494
        }
495
496
        $this->setRegexErrorHandler();
497
498
        foreach ($tests as $test) {
499
            $regex = $this->prepareRegex($test);
500
501
            if ($this->regexMatch($regex, $against, $matches)) {
502
                // If the match contained a model, save it.
503
                if (isset($matches['model'])) {
504
                    $this->restoreRegexErrorHandler();
505
                    return $matches['model'];
506
                }
507
            }
508
        }
509
510
        $this->restoreRegexErrorHandler();
511
        return null;
512
    }
513
514 View Code Duplication
    protected function versionMatch($tests, $against)
0 ignored issues
show
Duplication introduced by
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...
515
    {
516
        // Model match must be an array.
517
        if (!is_array($tests) || !count($tests)) {
518
            return null;
519
        }
520
521
        $this->setRegexErrorHandler();
522
523
        foreach ($tests as $test) {
524
            $regex = $this->prepareRegex($test);
525
526
            if ($this->regexMatch($regex, $against, $matches)) {
527
                // If the match contained a version, save it.
528
                if (isset($matches['version'])) {
529
                    $this->restoreRegexErrorHandler();
530
                    return $this->prepareVersion($matches['version']);
531
                }
532
            }
533
        }
534
535
        $this->restoreRegexErrorHandler();
536
        return null;
537
    }
538
539
    protected function matchEntity($entity, $tests, $against)
540
    {
541
        if ($entity == 'version') {
542
            return $this->versionMatch($tests, $against);
543
        }
544
545
        if ($entity == 'model') {
546
            return $this->modelMatch($tests, $against);
547
        }
548
    }
549
550
    // @todo: Reduce scope of $deviceInfoFromDb
551 View Code Duplication
    protected function detectDeviceModel(array $deviceInfoFromDb)
0 ignored issues
show
Duplication introduced by
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...
552
    {
553
        if (!isset($deviceInfoFromDb['modelMatches'])) {
554
            return null;
555
        }
556
557
        return $this->matchEntity('model', $deviceInfoFromDb['modelMatches'], $this->getUserAgent());
558
    }
559
560
    // @todo: temporary duplicated code
561 View Code Duplication
    protected function detectDeviceModelVersion(array $deviceInfoFromDb)
0 ignored issues
show
Duplication introduced by
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...
562
    {
563
        if (!isset($deviceInfoFromDb['modelMatches'])) {
564
            return null;
565
        }
566
567
        return $this->matchEntity('version', $deviceInfoFromDb['modelMatches'], $this->getUserAgent());
568
    }
569
570
    protected function detectBrowserModel(array $browserInfoFromDb)
571
    {
572
        return (isset($browserInfoFromDb['model']) ? $browserInfoFromDb['model'] : null);
573
    }
574
575
    protected function detectBrowserVersion(array $browserInfoFromDb)
576
    {
577
        $browserVersionRaw = $this->matchEntity('version', $browserInfoFromDb['versionMatches'], $this->getUserAgent());
578
        if ($browserInfoFromDb['versionHelper']) {
579
            $funcName = $browserInfoFromDb['versionHelper'];
580
            if ($browserVersionDataFound = $this->browsersProvider->$funcName($browserVersionRaw)) {
581
                return $browserVersionDataFound['version'];
582
            }
583
        }
584
        return $browserVersionRaw;
585
    }
586
587
    protected function detectOperatingSystemModel(array $operatingSystemInfoFromDb)
588
    {
589
        return (isset($operatingSystemInfoFromDb['model']) ? $operatingSystemInfoFromDb['model'] : null);
590
    }
591
592
    protected function detectOperatingSystemVersion(array $operatingSystemInfoFromDb)
593
    {
594
        return $this->matchEntity('version', $operatingSystemInfoFromDb['versionMatches'], $this->getUserAgent());
595
    }
596
597
    /**
598
     * Creates a device with all the necessary context to determine all the given
599
     * properties of a device, including OS, browser, and any other context-based properties.
600
     *
601
     * @param DeviceInterface $deviceClass (optional)
602
     *        The class to use. It can be anything that's derived from DeviceInterface.
603
     *
604
     * {@see DeviceInterface}
605
     *
606
     * @return DeviceInterface
607
     *
608
     * @throws Exception\InvalidArgumentException When an invalid class is used.
609
     */
610
    public function detect($deviceClass = null)
611
    {
612
        if ($deviceClass) {
613
            if (!is_subclass_of($deviceClass, __NAMESPACE__ . '\Device\DeviceInterface')) {
614
                $type = gettype($deviceClass);
615
                if ($type == 'object') {
616
                    $type = get_class($deviceClass);
617
                } elseif ($type == 'string') {
618
                    $type = $deviceClass;
619
                } else {
620
                    $type = sprintf('Invalid type %s', $type);
621
                }
622
                throw new Exception\InvalidArgumentException(sprintf('Invalid class specified: %s.', $type));
623
            }
624
        } else {
625
            // default implementation
626
            $deviceClass = __NAMESPACE__ . '\Device\Device';
627
        }
628
629
        // Cache check.
630
        if (($cached = $this->getFromCache($this->getUserAgent()))) {
631
            //make sure it's also of type that's requested
632
            if (is_subclass_of($cached, $deviceClass) || (is_object($cached) && $cached instanceof DeviceInterface)) {
633
                return $cached;
634
            }
635
        }
636
        
637
        $prop = [
638
            'userAgent' => null,
639
            'deviceType' => null,
640
            'deviceModel' => null,
641
            'deviceModelVersion' => null,
642
            'operatingSystemModel' => null,
643
            'operatingSystemVersion' => null,
644
            'browserModel' => null,
645
            'browserVersion' => null,
646
            'vendor' => null
647
        ];
648
649
        // Search phone OR tablet database.
650
        // Get the device type.
651
        $prop['deviceType'] = DeviceType::DESKTOP;
652
653
        if ($phoneResult = $this->searchPhonesProvider()) {
654
            $prop['deviceType'] = DeviceType::MOBILE;
655
            $prop['deviceResult'] = $phoneResult;
656
        }
657
658
        if ($tabletResult = $this->searchTabletsProvider()) {
659
            $prop['deviceType'] = DeviceType::TABLET;
660
            $prop['deviceResult'] = $tabletResult;
661
        }
662
663
        // If we know the device,
664
        // get model and version of the physical device (if possible).
665
        $deviceResult = isset($prop['deviceResult']) ? $prop['deviceResult'] : null;
666
        if (!is_null($deviceResult)) {
667
            if (isset($deviceResult['model'])) {
668
                // Device model is already known from the DB.
669
                $prop['deviceModel'] = $deviceResult['model'];
670
            } else {
671
                $prop['deviceModel'] = $this->detectDeviceModel($deviceResult);
672
                $prop['deviceModelVersion'] = $this->detectDeviceModelVersion($deviceResult);
673
            }
674
        }
675
676
        // Get model and version of the browser (if possible).
677
        $browserResult = $this->searchBrowsersProvider();
678
        $prop['browserResult'] = $browserResult;
679
680
        if ($browserResult) {
681
            $prop['browserModel'] = $this->detectBrowserModel($browserResult);
682
            $prop['browserVersion'] = $this->detectBrowserVersion($browserResult);
683
        }
684
685
        // Get model and version of the operating system (if possible).
686
        $operatingSystemResult = $this->searchOperatingSystemsProvider();
687
        $prop['operatingSystemResult'] = $operatingSystemResult;
688
689
        if ($operatingSystemResult) {
690
            $prop['operatingSystemModel'] = $this->detectOperatingSystemModel($operatingSystemResult);
691
            $prop['operatingSystemVersion'] = $this->detectOperatingSystemVersion($operatingSystemResult);
692
        }
693
694
        // Fallback if no device was found (phone or tablet)
695
        // and try to set the device type if the found browser
696
        // or operating system are mobile.
697
        if (null === $deviceResult &&
698
            (
699
                (
700
                    $browserResult &&
701
                    isset($browserResult['isMobile']) && $browserResult['isMobile']
702
                )
703
                    ||
704
                (
705
                    $operatingSystemResult &&
706
                    isset($operatingSystemResult['isMobile']) && $operatingSystemResult['isMobile']
707
                )
708
            )
709
        ) {
710
            $prop['deviceType'] = DeviceType::MOBILE;
711
        }
712
713
        $prop['vendor'] = !is_null($deviceResult) ? $deviceResult['vendor'] : null;
714
        $prop['userAgent'] = $this->getUserAgent();
715
716
        // Add to cache.
717
        $device = $deviceClass::create($prop);
718
        $this->setCache($prop['userAgent'], $device);
719
        
720
        return $device;
721
    }
722
723
    private function searchForItemInDb(array $itemData)
724
    {
725
        // Check matching type, and assume regex if not present.
726
        if (!isset($itemData['matchType'])) {
727
            $itemData['matchType'] = 'regex';
728
        }
729
730
        if (!isset($itemData['vendor'])) {
731
            throw new Exception\InvalidDeviceSpecificationException(
732
                sprintf('Invalid spec for item. Missing %s key.', 'vendor')
733
            );
734
        }
735
736
        if (!isset($itemData['identityMatches'])) {
737
            throw new Exception\InvalidDeviceSpecificationException(
738
                sprintf('Invalid spec for item. Missing %s key.', 'identityMatches')
739
            );
740
        } elseif ($itemData['identityMatches'] === false) {
741
            // This is often case with vendors of phones that we
742
            // do not want to specifically detect, but we keep the record
743
            // for vendor matches purposes. (eg. Acer)
744
            return false;
745
        }
746
747
        if ($this->identityMatch($itemData['matchType'], $itemData['identityMatches'], $this->getUserAgent())) {
748
            // Found the matching item.
749
            return $itemData;
750
        }
751
752
        return false;
753
    }
754
755 View Code Duplication
    protected function searchPhonesProvider()
0 ignored issues
show
Duplication introduced by
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...
756
    {
757
        foreach ($this->phonesProvider->getAll() as $vendorKey => $itemData) {
758
            $result = $this->searchForItemInDb($itemData);
759
            if ($result !== false) {
760
                return $result;
761
            }
762
        }
763
764
        return false;
765
    }
766
767 View Code Duplication
    protected function searchTabletsProvider()
0 ignored issues
show
Duplication introduced by
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...
768
    {
769
        foreach ($this->tabletsProvider->getAll() as $vendorKey => $itemData) {
770
            $result = $this->searchForItemInDb($itemData);
771
            if ($result !== false) {
772
                return $result;
773
            }
774
        }
775
776
        return false;
777
    }
778
779 View Code Duplication
    protected function searchBrowsersProvider()
0 ignored issues
show
Duplication introduced by
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...
780
    {
781
        foreach ($this->browsersProvider->getAll() as $familyName => $items) {
782
            foreach ($items as $itemName => $itemData) {
783
                $result = $this->searchForItemInDb($itemData);
784
                if ($result !== false) {
785
                    return $result;
786
                }
787
            }
788
        }
789
790
        return false;
791
    }
792
793 View Code Duplication
    protected function searchOperatingSystemsProvider()
0 ignored issues
show
Duplication introduced by
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...
794
    {
795
        foreach ($this->operatingSystemsProvider->getAll() as $familyName => $items) {
796
            foreach ($items as $itemName => $itemData) {
797
                $result = $this->searchForItemInDb($itemData);
798
                if ($result !== false) {
799
                    return $result;
800
                }
801
            }
802
        }
803
804
        return false;
805
    }
806
807
    /**
808
     * @param $regex
809
     * @param $against
810
     * @param null $matches
811
     * @return int
812
     */
813
    private function regexMatch($regex, $against, &$matches = null)
814
    {
815
        return preg_match($regex, $against, $matches);
816
    }
817
818
    /**
819
     * An error handler that gets registered to watch only for regex errors and convert
820
     * to an exception.
821
     *
822
     * @param $code int
823
     * @param $msg string
824
     * @param $file string
825
     * @param $line int
826
     * @param $context array
827
     *
828
     * @return bool False to indicate this is not a regex error to be handled.
829
     *
830
     * @throws Exception\RegexCompileException When there is a regex error.
831
     */
832
    public function regexErrorHandler($code, $msg, $file, $line, $context)
833
    {
834
        if (strpos($msg, 'preg_') !== false) {
835
            // we only want to deal with preg match errors
836
            throw new Exception\RegexCompileException($msg, $code, $file, $line, $context);
837
838
        }
839
840
        return false;
841
    }
842
843
    private function setRegexErrorHandler()
844
    {
845
        // graceful handling of pcre errors
846
        set_error_handler(array($this, 'regexErrorHandler'));
847
    }
848
849
    private function restoreRegexErrorHandler()
850
    {
851
        // restore previous
852
        restore_error_handler();
853
    }
854
855
    /**
856
     * Set the cache setter lambda.
857
     *
858
     * @param \Closure $cb
859
     *
860
     * @return MobileDetect
861
     */
862
    public function setCacheSetter(\Closure $cb)
863
    {
864
        $this->cacheSet = $cb;
865
866
        return $this;
867
    }
868
869
    /**
870
     * Set the cache getter lambda.
871
     *
872
     * @param \Closure $cb
873
     *
874
     * @return $this
875
     */
876
    public function setCacheGetter(\Closure $cb)
877
    {
878
        $this->cacheGet = $cb;
879
880
        return $this;
881
    }
882
883
    /**
884
     * Try to get the device from cache if available.
885
     *
886
     * @param $key string The key.
887
     *
888
     * @return DeviceInterface|null
889
     */
890
    public function getFromCache($key)
891
    {
892
        if (is_callable($this->cacheGet)) {
893
            $cb = $this->cacheGet;
894
895
            return $cb($key);
896
        }
897
898
        return null;
899
    }
900
901
    /**
902
     * Try to save the detected device in cache.
903
     *
904
     * @param $key string The key.
905
     * @param DeviceInterface $obj The device.
906
     *
907
     * @return bool false if not succeeded.
908
     */
909
    public function setCache($key, DeviceInterface $obj)
910
    {
911
        if (is_callable($this->cacheSet)) {
912
            $cb = $this->cacheSet;
913
914
            return $cb($key, $obj);
915
        }
916
917
        return false;
918
    }
919
}