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