This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
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
|
|||
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
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 For '' == false // true
'' == null // true
'ab' == false // false
'ab' == null // false
// It is often better to use strict comparison
'' === false // false
'' === null // false
![]() |
|||
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
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. ![]() |
|||
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
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. ![]() |
|||
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
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. ![]() |
|||
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
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. ![]() |
|||
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
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. ![]() |
|||
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
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. ![]() |
|||
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
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. ![]() |
|||
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
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. ![]() |
|||
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 | } |
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: