1 <?php
2 /**
3 * Part of the Joomla Framework Application Package
4 *
5 * @copyright Copyright (C) 2005 - 2016 Open Source Matters, Inc. All rights reserved.
6 * @license GNU General Public License version 2 or later; see LICENSE
7 */
8
9 namespace Joomla\Application\Web;
10
11
12 /**
13 * Class to model a Web Client.
14 *
15 * @property-read integer $platform The detected platform on which the web client runs.
16 * @property-read boolean $mobile True if the web client is a mobile device.
17 * @property-read integer $engine The detected rendering engine used by the web client.
18 * @property-read integer $browser The detected browser used by the web client.
19 * @property-read string $browserVersion The detected browser version used by the web client.
20 * @property-read array $languages The priority order detected accepted languages for the client.
21 * @property-read array $encodings The priority order detected accepted encodings for the client.
22 * @property-read string $userAgent The web client's user agent string.
23 * @property-read string $acceptEncoding The web client's accepted encoding string.
24 * @property-read string $acceptLanguage The web client's accepted languages string.
25 * @property-read array $detection An array of flags determining whether or not a detection routine has been run.
26 * @property-read boolean $robot True if the web client is a robot
27 * @property-read array $headers An array of all headers sent by client
28 *
29 * @since 1.0
30 */
31 class WebClient
32 {
33 const WINDOWS = 1;
34 const WINDOWS_PHONE = 2;
35 const WINDOWS_CE = 3;
36 const IPHONE = 4;
37 const IPAD = 5;
38 const IPOD = 6;
39 const MAC = 7;
40 const BLACKBERRY = 8;
41 const ANDROID = 9;
42 const LINUX = 10;
43 const TRIDENT = 11;
44 const WEBKIT = 12;
45 const GECKO = 13;
46 const PRESTO = 14;
47 const KHTML = 15;
48 const AMAYA = 16;
49 const IE = 17;
50 const FIREFOX = 18;
51 const CHROME = 19;
52 const SAFARI = 20;
53 const OPERA = 21;
54 const ANDROIDTABLET = 22;
55 const EDGE = 23;
56 const BLINK = 24;
57
58 /**
59 * @var integer The detected platform on which the web client runs.
60 * @since 1.0
61 */
62 protected $platform;
63
64 /**
65 * @var boolean True if the web client is a mobile device.
66 * @since 1.0
67 */
68 protected $mobile = false;
69
70 /**
71 * @var integer The detected rendering engine used by the web client.
72 * @since 1.0
73 */
74 protected $engine;
75
76 /**
77 * @var integer The detected browser used by the web client.
78 * @since 1.0
79 */
80 protected $browser;
81
82 /**
83 * @var string The detected browser version used by the web client.
84 * @since 1.0
85 */
86 protected $browserVersion;
87
88 /**
89 * @var array The priority order detected accepted languages for the client.
90 * @since 1.0
91 */
92 protected $languages = array();
93
94 /**
95 * @var array The priority order detected accepted encodings for the client.
96 * @since 1.0
97 */
98 protected $encodings = array();
99
100 /**
101 * @var string The web client's user agent string.
102 * @since 1.0
103 */
104 protected $userAgent;
105
106 /**
107 * @var string The web client's accepted encoding string.
108 * @since 1.0
109 */
110 protected $acceptEncoding;
111
112 /**
113 * @var string The web client's accepted languages string.
114 * @since 1.0
115 */
116 protected $acceptLanguage;
117
118 /**
119 * @var boolean True if the web client is a robot.
120 * @since 1.0
121 */
122 protected $robot = false;
123
124 /**
125 * @var array An array of flags determining whether or not a detection routine has been run.
126 * @since 1.0
127 */
128 protected $detection = array();
129
130 /**
131 * @var array An array of headers sent by client
132 * @since 1.3.0
133 */
134 protected $headers;
135
136 /**
137 * Class constructor.
138 *
139 * @param string $userAgent The optional user-agent string to parse.
140 * @param string $acceptEncoding The optional client accept encoding string to parse.
141 * @param string $acceptLanguage The optional client accept language string to parse.
142 *
143 * @since 1.0
144 */
145 public function __construct($userAgent = null, $acceptEncoding = null, $acceptLanguage = null)
146 {
147 // If no explicit user agent string was given attempt to use the implicit one from server environment.
148 if (empty($userAgent) && isset($_SERVER['HTTP_USER_AGENT']))
149 {
150 $this->userAgent = $_SERVER['HTTP_USER_AGENT'];
151 }
152 else
153 {
154 $this->userAgent = $userAgent;
155 }
156
157 // If no explicit acceptable encoding string was given attempt to use the implicit one from server environment.
158 if (empty($acceptEncoding) && isset($_SERVER['HTTP_ACCEPT_ENCODING']))
159 {
160 $this->acceptEncoding = $_SERVER['HTTP_ACCEPT_ENCODING'];
161 }
162 else
163 {
164 $this->acceptEncoding = $acceptEncoding;
165 }
166
167 // If no explicit acceptable languages string was given attempt to use the implicit one from server environment.
168 if (empty($acceptLanguage) && isset($_SERVER['HTTP_ACCEPT_LANGUAGE']))
169 {
170 $this->acceptLanguage = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
171 }
172 else
173 {
174 $this->acceptLanguage = $acceptLanguage;
175 }
176 }
177
178 /**
179 * Magic method to get an object property's value by name.
180 *
181 * @param string $name Name of the property for which to return a value.
182 *
183 * @return mixed The requested value if it exists.
184 *
185 * @since 1.0
186 */
187 public function __get($name)
188 {
189 switch ($name)
190 {
191 case 'mobile':
192 case 'platform':
193 if (empty($this->detection['platform']))
194 {
195 $this->detectPlatform($this->userAgent);
196 }
197 break;
198
199 case 'engine':
200 if (empty($this->detection['engine']))
201 {
202 $this->detectEngine($this->userAgent);
203 }
204 break;
205
206 case 'browser':
207 case 'browserVersion':
208 if (empty($this->detection['browser']))
209 {
210 $this->detectBrowser($this->userAgent);
211 }
212 break;
213
214 case 'languages':
215 if (empty($this->detection['acceptLanguage']))
216 {
217 $this->detectLanguage($this->acceptLanguage);
218 }
219 break;
220
221 case 'encodings':
222 if (empty($this->detection['acceptEncoding']))
223 {
224 $this->detectEncoding($this->acceptEncoding);
225 }
226 break;
227
228 case 'robot':
229 if (empty($this->detection['robot']))
230 {
231 $this->detectRobot($this->userAgent);
232 }
233 break;
234 case 'headers':
235 if (empty($this->detection['headers']))
236 {
237 $this->detectHeaders();
238 }
239 break;
240 }
241
242 // Return the property if it exists.
243 if (isset($this->$name))
244 {
245 return $this->$name;
246 }
247 }
248
249 /**
250 * Detects the client browser and version in a user agent string.
251 *
252 * @param string $userAgent The user-agent string to parse.
253 *
254 * @return void
255 *
256 * @since 1.0
257 */
258 protected function detectBrowser($userAgent)
259 {
260 // Attempt to detect the browser type. Obviously we are only worried about major browsers.
261 if ((stripos($userAgent, 'MSIE') !== false) && (stripos($userAgent, 'Opera') === false))
262 {
263 $this->browser = self::IE;
264 $patternBrowser = 'MSIE';
265 }
266 elseif (stripos($userAgent, 'Trident') !== false)
267 {
268 $this->browser = self::IE;
269 $patternBrowser = ' rv';
270 }
271 elseif (stripos($userAgent, 'Edge') !== false)
272 {
273 $this->browser = self::EDGE;
274 $patternBrowser = 'Edge';
275 }
276 elseif ((stripos($userAgent, 'Firefox') !== false) && (stripos($userAgent, 'like Firefox') === false))
277 {
278 $this->browser = self::FIREFOX;
279 $patternBrowser = 'Firefox';
280 }
281 elseif (stripos($userAgent, 'OPR') !== false)
282 {
283 $this->browser = self::OPERA;
284 $patternBrowser = 'OPR';
285 }
286 elseif (stripos($userAgent, 'Chrome') !== false)
287 {
288 $this->browser = self::CHROME;
289 $patternBrowser = 'Chrome';
290 }
291 elseif (stripos($userAgent, 'Safari') !== false)
292 {
293 $this->browser = self::SAFARI;
294 $patternBrowser = 'Safari';
295 }
296 elseif (stripos($userAgent, 'Opera') !== false)
297 {
298 $this->browser = self::OPERA;
299 $patternBrowser = 'Opera';
300 }
301
302 // If we detected a known browser let's attempt to determine the version.
303 if ($this->browser)
304 {
305 // Build the REGEX pattern to match the browser version string within the user agent string.
306 $pattern = '#(?<browser>Version|' . $patternBrowser . ')[/ :]+(?<version>[0-9.|a-zA-Z.]*)#';
307
308 // Attempt to find version strings in the user agent string.
309 $matches = array();
310
311 if (preg_match_all($pattern, $userAgent, $matches))
312 {
313 // Do we have both a Version and browser match?
314 if (count($matches['browser']) == 2)
315 {
316 // See whether Version or browser came first, and use the number accordingly.
317 if (strripos($userAgent, 'Version') < strripos($userAgent, $patternBrowser))
318 {
319 $this->browserVersion = $matches['version'][0];
320 }
321 else
322 {
323 $this->browserVersion = $matches['version'][1];
324 }
325 }
326 elseif (count($matches['browser']) > 2)
327 {
328 $key = array_search('Version', $matches['browser']);
329
330 if ($key)
331 {
332 $this->browserVersion = $matches['version'][$key];
333 }
334 }
335 else
336 // We only have a Version or a browser so use what we have.
337 {
338 $this->browserVersion = $matches['version'][0];
339 }
340 }
341 }
342
343 // Mark this detection routine as run.
344 $this->detection['browser'] = true;
345 }
346
347 /**
348 * Method to detect the accepted response encoding by the client.
349 *
350 * @param string $acceptEncoding The client accept encoding string to parse.
351 *
352 * @return void
353 *
354 * @since 1.0
355 */
356 protected function detectEncoding($acceptEncoding)
357 {
358 // Parse the accepted encodings.
359 $this->encodings = array_map('trim', (array) explode(',', $acceptEncoding));
360
361 // Mark this detection routine as run.
362 $this->detection['acceptEncoding'] = true;
363 }
364
365 /**
366 * Detects the client rendering engine in a user agent string.
367 *
368 * @param string $userAgent The user-agent string to parse.
369 *
370 * @return void
371 *
372 * @since 1.0
373 */
374 protected function detectEngine($userAgent)
375 {
376 if (stripos($userAgent, 'MSIE') !== false || stripos($userAgent, 'Trident') !== false)
377 {
378 // Attempt to detect the client engine -- starting with the most popular ... for now.
379 $this->engine = self::TRIDENT;
380 }
381 elseif (stripos($userAgent, 'Edge') !== false || stripos($userAgent, 'EdgeHTML') !== false)
382 {
383 $this->engine = self::EDGE;
384 }
385 elseif (stripos($userAgent, 'Chrome') !== false)
386 {
387 $result = explode('/', stristr($userAgent, 'Chrome'));
388 $version = explode(' ', $result[1]);
389
390 if ($version[0] >= 28)
391 {
392 $this->engine = self::BLINK;
393 }
394 else
395 {
396 $this->engine = self::WEBKIT;
397 }
398 }
399 elseif (stripos($userAgent, 'AppleWebKit') !== false || stripos($userAgent, 'blackberry') !== false)
400 {
401 if (stripos($userAgent, 'AppleWebKit') !== false)
402 {
403 $result = explode('/', stristr($userAgent, 'AppleWebKit'));
404 $version = explode(' ', $result[1]);
405
406 if ($version[0] === 537.36)
407 {
408 // AppleWebKit/537.36 is Blink engine specific, exception is Blink emulated IEMobile, Trident or Edge
409 $this->engine = self::BLINK;
410 }
411 }
412
413 // Evidently blackberry uses WebKit and doesn't necessarily report it. Bad RIM.
414 $this->engine = self::WEBKIT;
415 }
416 elseif (stripos($userAgent, 'Gecko') !== false && stripos($userAgent, 'like Gecko') === false)
417 {
418 // We have to check for like Gecko because some other browsers spoof Gecko.
419 $this->engine = self::GECKO;
420 }
421 elseif (stripos($userAgent, 'Opera') !== false || stripos($userAgent, 'Presto') !== false)
422 {
423 $result = explode('/', stristr($userAgent, 'Opera'));
424 $version = explode(' ', $result[1]);
425
426 if ($version[0] >= 15)
427 {
428 $this->engine = self::BLINK;
429 }
430
431 // Sometimes Opera browsers don't say Presto.
432 $this->engine = self::PRESTO;
433 }
434 elseif (stripos($userAgent, 'KHTML') !== false)
435 {
436 // *sigh*
437 $this->engine = self::KHTML;
438 }
439 elseif (stripos($userAgent, 'Amaya') !== false)
440 {
441 // Lesser known engine but it finishes off the major list from Wikipedia :-)
442 $this->engine = self::AMAYA;
443 }
444
445 // Mark this detection routine as run.
446 $this->detection['engine'] = true;
447 }
448
449 /**
450 * Method to detect the accepted languages by the client.
451 *
452 * @param mixed $acceptLanguage The client accept language string to parse.
453 *
454 * @return void
455 *
456 * @since 1.0
457 */
458 protected function detectLanguage($acceptLanguage)
459 {
460 // Parse the accepted encodings.
461 $this->languages = array_map('trim', (array) explode(',', $acceptLanguage));
462
463 // Mark this detection routine as run.
464 $this->detection['acceptLanguage'] = true;
465 }
466
467 /**
468 * Detects the client platform in a user agent string.
469 *
470 * @param string $userAgent The user-agent string to parse.
471 *
472 * @return void
473 *
474 * @since 1.0
475 */
476 protected function detectPlatform($userAgent)
477 {
478 // Attempt to detect the client platform.
479 if (stripos($userAgent, 'Windows') !== false)
480 {
481 $this->platform = self::WINDOWS;
482
483 // Let's look at the specific mobile options in the Windows space.
484 if (stripos($userAgent, 'Windows Phone') !== false)
485 {
486 $this->mobile = true;
487 $this->platform = self::WINDOWS_PHONE;
488 }
489 elseif (stripos($userAgent, 'Windows CE') !== false)
490 {
491 $this->mobile = true;
492 $this->platform = self::WINDOWS_CE;
493 }
494 }
495 elseif (stripos($userAgent, 'iPhone') !== false)
496 {
497 // Interestingly 'iPhone' is present in all iOS devices so far including iPad and iPods.
498 $this->mobile = true;
499 $this->platform = self::IPHONE;
500
501 // Let's look at the specific mobile options in the iOS space.
502 if (stripos($userAgent, 'iPad') !== false)
503 {
504 $this->platform = self::IPAD;
505 }
506 elseif (stripos($userAgent, 'iPod') !== false)
507 {
508 $this->platform = self::IPOD;
509 }
510 }
511 elseif (stripos($userAgent, 'iPad') !== false)
512 {
513 // In case where iPhone is not mentioed in iPad user agent string
514 $this->mobile = true;
515 $this->platform = self::IPAD;
516 }
517 elseif (stripos($userAgent, 'iPod') !== false)
518 {
519 // In case where iPhone is not mentioed in iPod user agent string
520 $this->mobile = true;
521 $this->platform = self::IPOD;
522 }
523 elseif (preg_match('/macintosh|mac os x/i', $userAgent))
524 {
525 // This has to come after the iPhone check because mac strings are also present in iOS devices.
526 $this->platform = self::MAC;
527 }
528 elseif (stripos($userAgent, 'Blackberry') !== false)
529 {
530 $this->mobile = true;
531 $this->platform = self::BLACKBERRY;
532 }
533 elseif (stripos($userAgent, 'Android') !== false)
534 {
535 $this->mobile = true;
536 $this->platform = self::ANDROID;
537 /**
538 * Attempt to distinguish between Android phones and tablets
539 * There is no totally foolproof method but certain rules almost always hold
540 * Android 3.x is only used for tablets
541 * Some devices and browsers encourage users to change their UA string to include Tablet.
542 * Google encourages manufacturers to exclude the string Mobile from tablet device UA strings.
543 * In some modes Kindle Android devices include the string Mobile but they include the string Silk.
544 */
545 if (stripos($userAgent, 'Android 3') !== false || stripos($userAgent, 'Tablet') !== false
546 || stripos($userAgent, 'Mobile') === false || stripos($userAgent, 'Silk') !== false )
547 {
548 $this->platform = self::ANDROIDTABLET;
549 }
550 }
551 elseif (stripos($userAgent, 'Linux') !== false)
552 {
553 $this->platform = self::LINUX;
554 }
555
556 // Mark this detection routine as run.
557 $this->detection['platform'] = true;
558 }
559
560 /**
561 * Determines if the browser is a robot or not.
562 *
563 * @param string $userAgent The user-agent string to parse.
564 *
565 * @return void
566 *
567 * @since 1.0
568 */
569 protected function detectRobot($userAgent)
570 {
571 if (preg_match('/http|bot|bingbot|googlebot|robot|spider|slurp|crawler|curl|^$/i', $userAgent))
572 {
573 $this->robot = true;
574 }
575 else
576 {
577 $this->robot = false;
578 }
579
580 $this->detection['robot'] = true;
581 }
582
583 /**
584 * Fills internal array of headers
585 *
586 * @return void
587 *
588 * @since 1.3.0
589 */
590 protected function detectHeaders()
591 {
592 if (function_exists('getallheaders'))
593 // If php is working under Apache, there is a special function
594 {
595 $this->headers = getallheaders();
596 }
597 else
598 // Else we fill headers from $_SERVER variable
599 {
600 $this->headers = array();
601
602 foreach ($_SERVER as $name => $value)
603 {
604 if (substr($name, 0, 5) == 'HTTP_')
605 {
606 $this->headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
607 }
608 }
609 }
610
611 // Mark this detection routine as run.
612 $this->detection['headers'] = true;
613 }
614 }
615