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) { |
||
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) |
|
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) |
|
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) |
|
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) |
|
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() |
|
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() |
|
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() |
|
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() |
|
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: