1<?php 2/** 3 * Copyright 1999-2017 Horde LLC (http://www.horde.org/) 4 * 5 * See the enclosed file COPYING for license information (LGPL). If you 6 * did not receive this file, see http://www.horde.org/licenses/lgpl21. 7 * 8 * @author Chuck Hagenbuch <chuck@horde.org> 9 * @author Jon Parise <jon@horde.org> 10 * @category Horde 11 * @license http://www.horde.org/licenses/lgpl21 LGPL 12 * @package Browser 13 */ 14 15/** 16 * This provides capability information for the current web client. 17 * 18 * Browser identification is performed by examining the HTTP_USER_AGENT 19 * environment variable provided by the web server. 20 * 21 * @author Chuck Hagenbuch <chuck@horde.org> 22 * @author Jon Parise <jon@horde.org> 23 * @category Horde 24 * @copyright 1999-2017 Horde LLC 25 * @license http://www.horde.org/licenses/lgpl21 LGPL 26 * @package Browser 27 * @todo http://ajaxian.com/archives/parse-user-agent 28 */ 29class Horde_Browser 30{ 31 /** 32 * Major version number. 33 * 34 * @var integer 35 */ 36 protected $_majorVersion = 0; 37 38 /** 39 * Minor version number. 40 * 41 * @var integer 42 */ 43 protected $_minorVersion = 0; 44 45 /** 46 * Browser name. 47 * 48 * @var string 49 */ 50 protected $_browser = ''; 51 52 /** 53 * Full user agent string. 54 * 55 * @var string 56 */ 57 protected $_agent = ''; 58 59 /** 60 * Lower-case user agent string. 61 * 62 * @var string 63 */ 64 protected $_lowerAgent = ''; 65 66 /** 67 * HTTP_ACCEPT string 68 * 69 * @var string 70 */ 71 protected $_accept = ''; 72 73 /** 74 * Platform the browser is running on. 75 * 76 * @var string 77 */ 78 protected $_platform = ''; 79 80 /** 81 * Known robots. 82 * 83 * @var array 84 */ 85 protected $_robotAgents = array( 86 /* The most common ones. */ 87 'Slurp', 88 'Yahoo', 89 /* The rest alphabetically. */ 90 'appie', 91 'Arachnoidea', 92 'ArchitextSpider', 93 'Ask Jeeves', 94 'Baiduspider', 95 'cfetch', 96 'ConveraCrawler', 97 'ExtractorPro', 98 'FAST-WebCrawler', 99 'fido', 100 'findlinks', 101 'Francis', 102 'grub-client', 103 'Gulliver', 104 'HTTrack', 105 'ia_archiver', 106 'iaskspider', 107 'iCCrawler', 108 'InfoSeek', 109 'KIT-Fireball', 110 'larbin', 111 'LEIA', 112 'lmspider', 113 'lwp-trivial', 114 'Lycos_Spider', 115 'Mediapartners-Google', 116 'MuscatFerret', 117 'Pompos', 118 'Scooter', 119 'sogou spider', 120 'sproose', 121 'Teoma', 122 'Twiceler', 123 'Ultraseek', 124 'Vagabondo/Kliksafe', 125 'voyager', 126 'W3C-checklink', 127 'webbandit', 128 'www.almaden.ibm.com/cs/crawler', 129 'yacy', 130 'ZyBorg', 131 ); 132 133 /** 134 * Regexp for matching those robot strings. 135 * 136 * @var string 137 */ 138 protected $_robotAgentRegexp = null; 139 140 /** 141 * List of mobile user agents. 142 * 143 * Browsers like Mobile Safari (iPhone, iPod Touch) are much more 144 * full featured than OpenWave style browsers. This makes it dicey 145 * in some cases to treat all "mobile" browsers the same way. 146 * 147 * @TODO This list is not used in isMobile yet nor does it provide 148 * the same results as isMobile(). It is here for reference and 149 * future work. 150 */ 151 protected $_mobileAgents = array( 152 'Blackberry', 153 'Blazer', 154 'Handspring', 155 'iPhone', 156 'iPod', 157 'Kyocera', 158 'LG', 159 'Motorola', 160 'Nokia', 161 'Palm', 162 'PlayStation Portable', 163 'Samsung', 164 'Smartphone', 165 'SonyEricsson', 166 'Symbian', 167 'WAP', 168 'Windows CE', 169 ); 170 171 /** 172 * List of televison user agents. 173 * 174 * @TODO This list is not yet used anywhere. It is here for future 175 * media-type differentiation. 176 */ 177 protected $_tvAgents = array( 178 'Nintendo Wii', 179 'Playstation 3', 180 'WebTV', 181 ); 182 183 /** 184 * Is this a mobile browser? 185 * 186 * @var boolean 187 */ 188 protected $_mobile = false; 189 190 /** 191 * Is this a tablet browser? 192 * 193 * @var boolean 194 */ 195 protected $_tablet = false; 196 197 /** 198 * Features. 199 * 200 * @var array 201 */ 202 protected $_features = array( 203 'frames' => true, 204 'html' => true, 205 'images' => true, 206 'java' => true, 207 'javascript' => true, 208 'tables' => true 209 ); 210 211 /** 212 * Quirks. 213 * 214 * @var array 215 */ 216 protected $_quirks = array(); 217 218 /** 219 * List of viewable image MIME subtypes. 220 * This list of viewable images works for IE and Netscape/Mozilla. 221 * 222 * @var array 223 */ 224 protected $_images = array('jpeg', 'gif', 'png', 'pjpeg', 'x-png', 'bmp'); 225 226 /** 227 * Creates a browser instance (Constructor). 228 * 229 * @param string $userAgent The browser string to parse. 230 * @param string $accept The HTTP_ACCEPT settings to use. 231 */ 232 public function __construct($userAgent = null, $accept = null) 233 { 234 $this->match($userAgent, $accept); 235 } 236 237 /** 238 * Parses the user agent string and inititializes the object with all the 239 * known features and quirks for the given browser. 240 * 241 * @param string $userAgent The browser string to parse. 242 * @param string $accept The HTTP_ACCEPT settings to use. 243 */ 244 public function match($userAgent = null, $accept = null) 245 { 246 // Set our agent string. 247 if (is_null($userAgent)) { 248 if (isset($_SERVER['HTTP_USER_AGENT'])) { 249 $this->_agent = trim($_SERVER['HTTP_USER_AGENT']); 250 } 251 } else { 252 $this->_agent = $userAgent; 253 } 254 $this->_lowerAgent = Horde_String::lower($this->_agent); 255 256 // Set our accept string. 257 if (is_null($accept)) { 258 if (isset($_SERVER['HTTP_ACCEPT'])) { 259 $this->_accept = Horde_String::lower(trim($_SERVER['HTTP_ACCEPT'])); 260 } 261 } else { 262 $this->_accept = Horde_String::lower($accept); 263 } 264 265 // Check for UTF support. 266 if (isset($_SERVER['HTTP_ACCEPT_CHARSET'])) { 267 $this->setFeature('utf', strpos(Horde_String::lower($_SERVER['HTTP_ACCEPT_CHARSET']), 'utf') !== false); 268 } 269 270 if (empty($this->_agent)) { 271 return; 272 } 273 274 $this->_setPlatform(); 275 276 // Use local scope for frequently accessed variables. 277 $agent = $this->_agent; 278 $lowerAgent = $this->_lowerAgent; 279 280 if (strpos($lowerAgent, 'iemobile') !== false || 281 strpos($lowerAgent, 'mobileexplorer') !== false || 282 strpos($lowerAgent, 'openwave') !== false) { 283 $this->setFeature('frames', false); 284 $this->setFeature('javascript', false); 285 $this->setQuirk('avoid_popup_windows'); 286 $this->setMobile(true); 287 288 if (preg_match('|iemobile[/ ]([0-9.]+)|', $lowerAgent, $version)) { 289 list($this->_majorVersion, $this->_minorVersion) = explode('.', $version[1]); 290 if ($this->_majorVersion >= 7) { 291 // Windows Phone, Modern Browser 292 $this->setBrowser('msie'); 293 $this->setFeature('javascript'); 294 $this->setFeature('xmlhttpreq'); 295 $this->setFeature('ajax'); 296 $this->setFeature('dom'); 297 $this->setFeature('utf'); 298 $this->setFeature('rte'); 299 $this->setFeature('cite'); 300 } 301 } 302 } elseif (strpos($lowerAgent, 'opera mini') !== false || 303 strpos($lowerAgent, 'operamini') !== false) { 304 $this->setBrowser('opera'); 305 $this->setFeature('frames', false); 306 $this->setFeature('javascript'); 307 $this->setQuirk('avoid_popup_windows'); 308 $this->setMobile(true); 309 } elseif (preg_match('|Opera[/ ]([0-9.]+)|', $agent, $version)) { 310 $this->setBrowser('opera'); 311 list($this->_majorVersion, $this->_minorVersion) = explode('.', $version[1]); 312 $this->setFeature('javascript'); 313 $this->setQuirk('no_filename_spaces'); 314 315 /* Opera Mobile reports its screen resolution in the user 316 * agent strings. */ 317 if (preg_match('/; (120x160|240x280|240x320|320x320)\)/', $agent)) { 318 $this->setMobile(true); 319 } elseif (preg_match('|Tablet|', $agent)) { 320 $this->setMobile(true); 321 $this->setTablet(true); 322 } 323 324 if ($this->_majorVersion >= 7) { 325 if ($this->_majorVersion >= 8) { 326 $this->setFeature('xmlhttpreq'); 327 $this->setFeature('javascript', 1.5); 328 } 329 if ($this->_majorVersion >= 9) { 330 $this->setFeature('dataurl', 4100); 331 if ($this->_minorVersion >= 5) { 332 $this->setFeature('ajax'); 333 $this->setFeature('rte'); 334 } 335 } 336 $this->setFeature('dom'); 337 $this->setFeature('iframes'); 338 $this->setFeature('accesskey'); 339 $this->setFeature('optgroup'); 340 $this->setQuirk('double_linebreak_textarea'); 341 } 342 } elseif (strpos($lowerAgent, 'elaine/') !== false || 343 strpos($lowerAgent, 'palmsource') !== false || 344 strpos($lowerAgent, 'digital paths') !== false) { 345 $this->setBrowser('palm'); 346 $this->setFeature('images', false); 347 $this->setFeature('frames', false); 348 $this->setFeature('javascript', false); 349 $this->setQuirk('avoid_popup_windows'); 350 $this->setMobile(true); 351 } elseif ((preg_match('|MSIE ([0-9.]+)|', $agent, $version)) || 352 (preg_match('|Internet Explorer/([0-9.]+)|', $agent, $version)) || 353 (strpos($lowerAgent, 'trident/') !== false)) { 354 $this->setBrowser('msie'); 355 $this->setQuirk('cache_ssl_downloads'); 356 $this->setQuirk('cache_same_url'); 357 $this->setQuirk('break_disposition_filename'); 358 359 if (empty($version)) { 360 // IE 11 361 preg_match('|rv:(\d+)|', $lowerAgent, $version); 362 } 363 364 if (strpos($version[1], '.') !== false) { 365 list($this->_majorVersion, $this->_minorVersion) = explode('.', $version[1]); 366 } else { 367 $this->_majorVersion = $version[1]; 368 $this->_minorVersion = 0; 369 } 370 371 /* IE (< 7) on Windows does not support alpha transparency 372 * in PNG images. */ 373 if (($this->_majorVersion < 7) && 374 preg_match('/windows/i', $agent)) { 375 $this->setQuirk('png_transparency'); 376 } 377 378 /* Some Handhelds have their screen resolution in the user 379 * agent string, which we can use to look for mobile 380 * agents. */ 381 if (preg_match('/; (120x160|240x280|240x320|320x320)\)/', $agent)) { 382 $this->setMobile(true); 383 } 384 385 $this->setFeature('xmlhttpreq'); 386 387 switch ($this->_majorVersion) { 388 default: 389 case 11: 390 case 10: 391 case 9: 392 case 8: 393 case 7: 394 $this->setFeature('javascript', 1.4); 395 $this->setFeature('ajax'); 396 $this->setFeature('dom'); 397 $this->setFeature('iframes'); 398 $this->setFeature('utf'); 399 $this->setFeature('rte'); 400 $this->setFeature('homepage'); 401 $this->setFeature('accesskey'); 402 $this->setFeature('optgroup'); 403 if ($this->_majorVersion != 7) { 404 $this->setFeature('cite'); 405 $this->setFeature('dataurl', ($this->_majorVersion == 8) ? 32768 : true); 406 } 407 break; 408 409 case 6: 410 $this->setFeature('javascript', 1.4); 411 $this->setFeature('dom'); 412 $this->setFeature('iframes'); 413 $this->setFeature('utf'); 414 $this->setFeature('rte'); 415 $this->setFeature('homepage'); 416 $this->setFeature('accesskey'); 417 $this->setFeature('optgroup'); 418 $this->setQuirk('scrollbar_in_way'); 419 $this->setQuirk('broken_multipart_form'); 420 $this->setQuirk('windowed_controls'); 421 break; 422 423 case 5: 424 if ($this->getPlatform() == 'mac') { 425 $this->setFeature('javascript', 1.2); 426 $this->setFeature('optgroup'); 427 $this->setFeature('xmlhttpreq', false); 428 } else { 429 // MSIE 5 for Windows. 430 $this->setFeature('javascript', 1.4); 431 $this->setFeature('dom'); 432 if ($this->_minorVersion >= 5) { 433 $this->setFeature('rte'); 434 $this->setQuirk('windowed_controls'); 435 } 436 } 437 $this->setFeature('iframes'); 438 $this->setFeature('utf'); 439 $this->setFeature('homepage'); 440 $this->setFeature('accesskey'); 441 if ($this->_minorVersion == 5) { 442 $this->setQuirk('break_disposition_header'); 443 $this->setQuirk('broken_multipart_form'); 444 } 445 break; 446 447 case 4: 448 $this->setFeature('javascript', 1.2); 449 $this->setFeature('accesskey'); 450 $this->setFeature('xmlhttpreq', false); 451 if ($this->_minorVersion > 0) { 452 $this->setFeature('utf'); 453 } 454 break; 455 456 case 3: 457 $this->setFeature('javascript', 1.1); 458 $this->setQuirk('avoid_popup_windows'); 459 $this->setFeature('xmlhttpreq', false); 460 break; 461 } 462 } elseif (preg_match('|ANTFresco/([0-9]+)|', $agent, $version)) { 463 $this->setBrowser('fresco'); 464 $this->setFeature('javascript', 1.1); 465 $this->setQuirk('avoid_popup_windows'); 466 } elseif (strpos($lowerAgent, 'avantgo') !== false) { 467 $this->setBrowser('avantgo'); 468 $this->setMobile(true); 469 } elseif (preg_match('|Konqueror/([0-9]+)\.?([0-9]+)?|', $agent, $version) || 470 preg_match('|Safari/([0-9]+)\.?([0-9]+)?|', $agent, $version)) { 471 $this->setBrowser('webkit'); 472 $this->setQuirk('empty_file_input_value'); 473 $this->setQuirk('no_hidden_overflow_tables'); 474 $this->setFeature('dataurl'); 475 476 if (strpos($agent, 'Mobile') !== false || 477 strpos($agent, 'Android') !== false || 478 strpos($agent, 'SAMSUNG-GT') !== false || 479 ((strpos($agent, 'Nokia') !== false || 480 strpos($agent, 'Symbian') !== false) && 481 strpos($agent, 'WebKit') !== false) || 482 (strpos($agent, 'N900') !== false && 483 strpos($agent, 'Maemo Browser') !== false) || 484 (strpos($agent, 'MeeGo') !== false && 485 strpos($agent, 'NokiaN9') !== false)) { 486 // WebKit Mobile 487 $this->setFeature('frames', false); 488 $this->setFeature('javascript'); 489 $this->setQuirk('avoid_popup_windows'); 490 $this->setMobile(true); 491 } 492 493 $this->_majorVersion = $version[1]; 494 if (isset($version[2])) { 495 $this->_minorVersion = $version[2]; 496 } 497 498 if (stripos($agent, 'Chrome/') !== false || 499 stripos($agent, 'CriOS/') !== false) { 500 // Google Chrome. 501 $this->setFeature('ischrome'); 502 $this->setFeature('rte'); 503 $this->setFeature('utf'); 504 $this->setFeature('javascript', 1.4); 505 $this->setFeature('ajax'); 506 $this->setFeature('dom'); 507 $this->setFeature('iframes'); 508 $this->setFeature('accesskey'); 509 $this->setFeature('xmlhttpreq'); 510 $this->setQuirk('empty_file_input_value', 0); 511 512 if (preg_match('|Chrome/([0-9.]+)|i', $agent, $version_string)) { 513 list($this->_majorVersion, $this->_minorVersion) = explode('.', $version_string[1], 2); 514 } 515 } elseif (stripos($agent, 'Safari/') !== false && 516 $this->_majorVersion >= 60) { 517 // Safari. 518 $this->setFeature('issafari'); 519 520 // Truly annoying - Safari did not start putting real version 521 // numbers until Version 3. 522 if (preg_match('|Version/([0-9.]+)|', $agent, $version_string)) { 523 list($this->_majorVersion, $this->_minorVersion) = explode('.', $version_string[1], 2); 524 $this->_minorVersion = intval($this->_minorVersion); 525 $this->setFeature('ajax'); 526 $this->setFeature('rte'); 527 } elseif ($this->_majorVersion >= 412) { 528 $this->_majorVersion = 2; 529 $this->_minorVersion = 0; 530 } else { 531 if ($this->_majorVersion >= 312) { 532 $this->_minorVersion = 3; 533 } elseif ($this->_majorVersion >= 124) { 534 $this->_minorVersion = 2; 535 } else { 536 $this->_minorVersion = 0; 537 } 538 $this->_majorVersion = 1; 539 } 540 541 $this->setFeature('utf'); 542 $this->setFeature('javascript', 1.4); 543 $this->setFeature('dom'); 544 $this->setFeature('iframes'); 545 if ($this->_majorVersion > 1 || $this->_minorVersion > 2) { 546 // As of Safari 1.3 547 $this->setFeature('accesskey'); 548 $this->setFeature('xmlhttpreq'); 549 } 550 if ($this->_majorVersion >= 9) { 551 $this->setQuirk('empty_file_input_value', 0); 552 } 553 } else { 554 // Konqueror. 555 $this->setFeature('javascript', 1.1); 556 $this->setFeature('iskonqueror'); 557 switch ($this->_majorVersion) { 558 case 4: 559 case 3: 560 $this->setFeature('dom'); 561 $this->setFeature('iframes'); 562 if ($this->_minorVersion >= 5 || 563 $this->_majorVersion == 4) { 564 $this->setFeature('accesskey'); 565 $this->setFeature('xmlhttpreq'); 566 } 567 break; 568 } 569 } 570 } elseif (preg_match('|Mozilla/([0-9.]+)|', $agent, $version)) { 571 $this->setBrowser('mozilla'); 572 $this->setQuirk('must_cache_forms'); 573 574 list($this->_majorVersion, $this->_minorVersion) = explode('.', $version[1]); 575 switch ($this->_majorVersion) { 576 default: 577 case 5: 578 if ($this->getPlatform() == 'win') { 579 $this->setQuirk('break_disposition_filename'); 580 } 581 $this->setFeature('javascript', 1.4); 582 $this->setFeature('ajax'); 583 $this->setFeature('dom'); 584 $this->setFeature('accesskey'); 585 $this->setFeature('optgroup'); 586 $this->setFeature('xmlhttpreq'); 587 $this->setFeature('cite'); 588 if (preg_match('|rv:(.*)\)|', $agent, $revision)) { 589 if (version_compare($revision[1], '1', '>=')) { 590 $this->setFeature('iframes'); 591 } 592 if (version_compare($revision[1], '1.3', '>=')) { 593 $this->setFeature('rte'); 594 } 595 if (version_compare($revision[1], '1.8.1', '>=')) { 596 $this->setFeature('dataurl'); 597 } 598 if (version_compare($revision[1], '10.0', '>=')) { 599 $this->setFeature('utf'); 600 } 601 } 602 if (stripos($agent, 'mobile') !== false || 603 strpos($agent, 'RX-51 N900') !== false) { 604 $this->setMobile(true); 605 } elseif (stripos($agent, 'tablet') !== false) { 606 $this->setTablet(true); 607 $this->setMobile(true); 608 } 609 break; 610 611 case 4: 612 $this->setFeature('javascript', 1.3); 613 $this->setQuirk('buggy_compression'); 614 break; 615 616 case 3: 617 case 2: 618 case 1: 619 case 0: 620 $this->setFeature('javascript', 1); 621 $this->setQuirk('buggy_compression'); 622 break; 623 } 624 } elseif (preg_match('|Lynx/([0-9]+)|', $agent, $version)) { 625 $this->setBrowser('lynx'); 626 $this->setFeature('images', false); 627 $this->setFeature('frames', false); 628 $this->setFeature('javascript', false); 629 $this->setQuirk('avoid_popup_windows'); 630 } elseif (preg_match('|Links \(([0-9]+)|', $agent, $version)) { 631 $this->setBrowser('links'); 632 $this->setFeature('images', false); 633 $this->setFeature('frames', false); 634 $this->setFeature('javascript', false); 635 $this->setQuirk('avoid_popup_windows'); 636 } elseif (preg_match('|HotJava/([0-9]+)|', $agent, $version)) { 637 $this->setBrowser('hotjava'); 638 $this->setFeature('javascript', false); 639 } elseif (strpos($agent, 'UP/') !== false || 640 strpos($agent, 'UP.B') !== false || 641 strpos($agent, 'UP.L') !== false) { 642 $this->setBrowser('up'); 643 $this->setFeature('html', false); 644 $this->setFeature('javascript', false); 645 $this->setFeature('hdml'); 646 $this->setFeature('wml'); 647 648 if (strpos($agent, 'GUI') !== false && 649 strpos($agent, 'UP.Link') !== false) { 650 /* The device accepts Openwave GUI extensions for WML 651 * 1.3. Non-UP.Link gateways sometimes have problems, 652 * so exclude them. */ 653 $this->setQuirk('ow_gui_1.3'); 654 } 655 $this->setMobile(true); 656 } elseif (strpos($agent, 'Xiino/') !== false) { 657 $this->setBrowser('xiino'); 658 $this->setFeature('hdml'); 659 $this->setFeature('wml'); 660 $this->setMobile(true); 661 } elseif (strpos($agent, 'Palmscape/') !== false) { 662 $this->setBrowser('palmscape'); 663 $this->setFeature('javascript', false); 664 $this->setFeature('hdml'); 665 $this->setFeature('wml'); 666 $this->setMobile(true); 667 } elseif (strpos($agent, 'Nokia') !== false) { 668 $this->setBrowser('nokia'); 669 $this->setFeature('html', false); 670 $this->setFeature('wml'); 671 $this->setFeature('xhtml'); 672 $this->setMobile(true); 673 } elseif (strpos($agent, 'Ericsson') !== false) { 674 $this->setBrowser('ericsson'); 675 $this->setFeature('html', false); 676 $this->setFeature('wml'); 677 $this->setMobile(true); 678 } elseif (strpos($agent, 'Grundig') !== false) { 679 $this->setBrowser('grundig'); 680 $this->setFeature('xhtml'); 681 $this->setFeature('wml'); 682 $this->setMobile(true); 683 } elseif (strpos($agent, 'NetFront') !== false) { 684 $this->setBrowser('netfront'); 685 $this->setFeature('xhtml'); 686 $this->setFeature('wml'); 687 $this->setMobile(true); 688 } elseif (strpos($lowerAgent, 'wap') !== false) { 689 $this->setBrowser('wap'); 690 $this->setFeature('html', false); 691 $this->setFeature('javascript', false); 692 $this->setFeature('hdml'); 693 $this->setFeature('wml'); 694 $this->setMobile(true); 695 } elseif (strpos($lowerAgent, 'docomo') !== false || 696 strpos($lowerAgent, 'portalmmm') !== false) { 697 $this->setBrowser('imode'); 698 $this->setFeature('images', false); 699 $this->setMobile(true); 700 } elseif (preg_match('|BlackBerry.*?/([0-9.]+)|', $agent, $version)) { 701 list($this->_majorVersion, $this->_minorVersion) = explode('.', $version[1]); 702 $this->setBrowser('blackberry'); 703 $this->setFeature('html', false); 704 $this->setFeature('javascript', false); 705 $this->setFeature('hdml'); 706 $this->setFeature('wml'); 707 $this->setMobile(true); 708 if ($this->_majorVersion >= 5 || 709 ($this->_majorVersion == 4 && $this->_minorVersion >= 6)) { 710 $this->setFeature('ajax'); 711 $this->setFeature('iframes'); 712 $this->setFeature('javascript', 1.5); 713 $this->setFeature('dom'); 714 $this->setFeature('xmlhttpreq'); 715 } 716 } elseif (strpos($agent, 'MOT-') !== false) { 717 $this->setBrowser('motorola'); 718 $this->setFeature('html', false); 719 $this->setFeature('javascript', false); 720 $this->setFeature('hdml'); 721 $this->setFeature('wml'); 722 $this->setMobile(true); 723 } elseif (strpos($lowerAgent, 'j-') !== false) { 724 $this->setBrowser('mml'); 725 $this->setMobile(true); 726 } 727 } 728 729 /** 730 * Matches the platform of the browser. 731 * 732 * This is a pretty simplistic implementation, but it's intended to let us 733 * tell what line breaks to send, so it's good enough for its purpose. 734 */ 735 protected function _setPlatform() 736 { 737 if (strpos($this->_lowerAgent, 'wind') !== false) { 738 $this->_platform = 'win'; 739 } elseif (strpos($this->_lowerAgent, 'mac') !== false) { 740 $this->_platform = 'mac'; 741 } else { 742 $this->_platform = 'unix'; 743 } 744 } 745 746 /** 747 * Returns the currently matched platform. 748 * 749 * @return string The user's platform. 750 */ 751 public function getPlatform() 752 { 753 return $this->_platform; 754 } 755 756 /** 757 * Sets the current browser. 758 * 759 * @param string $browser The browser to set as current. 760 */ 761 public function setBrowser($browser) 762 { 763 $this->_browser = $browser; 764 } 765 766 /** 767 * Determines if the given browser is the same as the current. 768 * 769 * @param string $browser The browser to check. 770 * 771 * @return boolean Is the given browser the same as the current? 772 */ 773 public function isBrowser($browser) 774 { 775 return ($this->_browser === $browser); 776 } 777 778 /** 779 * Set this browser as a mobile device. 780 * 781 * @param boolean $mobile True if the browser is a mobile device. 782 */ 783 public function setMobile($mobile) 784 { 785 $this->_mobile = (bool)$mobile; 786 } 787 788 /** 789 * Is the current browser to be a mobile device? 790 * 791 * @return boolean True if we do, false if we don't. 792 */ 793 public function isMobile() 794 { 795 return $this->_mobile; 796 } 797 798 /** 799 * Set this browser as a tablet device. 800 * 801 * @since 2.1.0 802 * 803 * @param boolean $tablet True if the browser is a tablet device. 804 */ 805 public function setTablet($tablet) 806 { 807 $this->_tablet = (bool)$tablet; 808 } 809 810 /** 811 * Is the current browser a tablet device? This is not 100% reliable, as 812 * most browsers do not differentiate between smartphone and tablet 813 * versions. 814 * 815 * @since 2.1.0 816 * 817 * @return boolean True if we do, false if we don't. 818 */ 819 public function isTablet() 820 { 821 return $this->_tablet; 822 } 823 824 /** 825 * Is the browser a robot? 826 * 827 * @return boolean True if browser is a known robot. 828 */ 829 public function isRobot() 830 { 831 if (preg_match('/bot/i', $this->_agent)) { 832 return true; 833 } 834 835 if (is_null($this->_robotAgentRegexp)) { 836 $regex = array(); 837 foreach ($this->_robotAgents as $r) { 838 $regex[] = preg_quote($r, '/'); 839 } 840 $this->_robotAgentRegexp = '/' . implode('|', $regex) . '/'; 841 } 842 843 return (bool)preg_match($this->_robotAgentRegexp, $this->_agent); 844 } 845 846 /** 847 * Returns the current browser. 848 * 849 * @return string The current browser. 850 */ 851 public function getBrowser() 852 { 853 return $this->_browser; 854 } 855 856 /** 857 * Returns the current browser's major version. 858 * 859 * @return integer The current browser's major version. 860 */ 861 public function getMajor() 862 { 863 return $this->_majorVersion; 864 } 865 866 /** 867 * Returns the current browser's minor version. 868 * 869 * @return integer The current browser's minor version. 870 */ 871 public function getMinor() 872 { 873 return $this->_minorVersion; 874 } 875 876 /** 877 * Returns the current browser's version. 878 * 879 * @return string The current browser's version. 880 */ 881 public function getVersion() 882 { 883 return $this->_majorVersion . '.' . $this->_minorVersion; 884 } 885 886 /** 887 * Returns the full browser agent string. 888 * 889 * @return string The browser agent string. 890 */ 891 public function getAgentString() 892 { 893 return $this->_agent; 894 } 895 896 /** 897 * Sets unique behavior for the current browser. 898 * 899 * @param string $quirk The behavior to set. Quirks: 900 * - avoid_popup_windows 901 * - break_disposition_header 902 * - break_disposition_filename 903 * - broken_multipart_form 904 * - buggy_compression 905 * - cache_same_url 906 * - cache_ssl_downloads 907 * - double_linebreak_textarea 908 * - empty_file_input_value 909 * - must_cache_forms 910 * - no_filename_spaces 911 * - no_hidden_overflow_tables 912 * - ow_gui_1.3 913 * - png_transparency 914 * - scrollbar_in_way 915 * - scroll_tds 916 * - windowed_controls 917 * @param string $value Special behavior parameter. 918 */ 919 public function setQuirk($quirk, $value = true) 920 { 921 if ($value) { 922 $this->_quirks[$quirk] = $value; 923 } else { 924 unset($this->_quirks[$quirk]); 925 } 926 } 927 928 /** 929 * Checks unique behavior for the current browser. 930 * 931 * @param string $quirk The behavior to check. 932 * 933 * @return boolean Does the browser have the behavior set? 934 */ 935 public function hasQuirk($quirk) 936 { 937 return !empty($this->_quirks[$quirk]); 938 } 939 940 /** 941 * Returns unique behavior for the current browser. 942 * 943 * @param string $quirk The behavior to retrieve. 944 * 945 * @return string The value for the requested behavior. 946 */ 947 public function getQuirk($quirk) 948 { 949 return isset($this->_quirks[$quirk]) 950 ? $this->_quirks[$quirk] 951 : null; 952 } 953 954 /** 955 * Sets capabilities for the current browser. 956 * 957 * @param string $feature The capability to set. Features: 958 * - accesskey 959 * - ajax 960 * - cite 961 * - dataurl 962 * - dom 963 * - frames 964 * - hdml 965 * - html 966 * - homepage 967 * - iframes 968 * - images 969 * - ischrome 970 * - iskonqueror 971 * - issafari 972 * - java 973 * - javascript 974 * - optgroup 975 * - rte 976 * - tables 977 * - utf 978 * - wml 979 * - xmlhttpreq 980 * @param string $value Special capability parameter. 981 */ 982 public function setFeature($feature, $value = true) 983 { 984 if ($value) { 985 $this->_features[$feature] = $value; 986 } else { 987 unset($this->_features[$feature]); 988 } 989 } 990 991 /** 992 * Checks the current browser capabilities. 993 * 994 * @param string $feature The capability to check. 995 * 996 * @return boolean Does the browser have the capability set? 997 */ 998 public function hasFeature($feature) 999 { 1000 return !empty($this->_features[$feature]); 1001 } 1002 1003 /** 1004 * Returns the current browser capability. 1005 * 1006 * @param string $feature The capability to retrieve. 1007 * 1008 * @return string The value of the requested capability. 1009 */ 1010 public function getFeature($feature) 1011 { 1012 return isset($this->_features[$feature]) 1013 ? $this->_features[$feature] 1014 : null; 1015 } 1016 1017 /** 1018 * Determines if we are using a secure (SSL) connection. 1019 * 1020 * @return boolean True if using SSL, false if not. 1021 */ 1022 public function usingSSLConnection() 1023 { 1024 return ((isset($_SERVER['HTTPS']) && 1025 ($_SERVER['HTTPS'] == 'on')) || 1026 getenv('SSL_PROTOCOL_VERSION')); 1027 } 1028 1029 /** 1030 * Returns the server protocol in use on the current server. 1031 * 1032 * @return string The HTTP server protocol version. 1033 */ 1034 public function getHTTPProtocol() 1035 { 1036 return (isset($_SERVER['SERVER_PROTOCOL']) && ($pos = strrpos($_SERVER['SERVER_PROTOCOL'], '/'))) 1037 ? substr($_SERVER['SERVER_PROTOCOL'], $pos + 1) 1038 : null; 1039 } 1040 1041 /** 1042 * Returns the IP address of the client. 1043 * 1044 * @return string The client IP address. 1045 */ 1046 public function getIPAddress() 1047 { 1048 return empty($_SERVER['HTTP_X_FORWARDED_FOR']) 1049 ? $_SERVER['REMOTE_ADDR'] 1050 : $_SERVER['HTTP_X_FORWARDED_FOR']; 1051 } 1052 1053 /** 1054 * Determines if files can be uploaded to the system. 1055 * 1056 * @return integer If uploads allowed, returns the maximum size of the 1057 * upload in bytes. Returns 0 if uploads are not 1058 * allowed. 1059 */ 1060 public static function allowFileUploads() 1061 { 1062 if (!ini_get('file_uploads') || 1063 (($dir = ini_get('upload_tmp_dir')) && 1064 !is_writable($dir))) { 1065 return 0; 1066 } 1067 1068 $filesize = ini_get('upload_max_filesize'); 1069 switch (Horde_String::lower(substr($filesize, -1, 1))) { 1070 case 'k': 1071 $filesize = intval(floatval($filesize) * 1024); 1072 break; 1073 1074 case 'm': 1075 $filesize = intval(floatval($filesize) * 1024 * 1024); 1076 break; 1077 1078 case 'g': 1079 $filesize = intval(floatval($filesize) * 1024 * 1024 * 1024); 1080 break; 1081 1082 default: 1083 $filesize = intval($filesize); 1084 break; 1085 } 1086 1087 $postsize = ini_get('post_max_size'); 1088 switch (Horde_String::lower(substr($postsize, -1, 1))) { 1089 case 'k': 1090 $postsize = intval(floatval($postsize) * 1024); 1091 break; 1092 1093 case 'm': 1094 $postsize = intval(floatval($postsize) * 1024 * 1024); 1095 break; 1096 1097 case 'g': 1098 $postsize = intval(floatval($postsize) * 1024 * 1024 * 1024); 1099 break; 1100 1101 default: 1102 $postsize = intval($postsize); 1103 break; 1104 } 1105 1106 // post_max_size == 0 disables the limit . 1107 // http://php.net/manual/en/ini.core.php#ini.post-max-size 1108 return $postsize == 0 1109 ? $filesize 1110 : min($filesize, $postsize); 1111 } 1112 1113 /** 1114 * Determines if the file was uploaded or not. If not, will return the 1115 * appropriate error message. 1116 * 1117 * @param string $field The name of the field containing the uploaded 1118 * file. 1119 * @param string $name The file description string to use in the error 1120 * message. Default: 'file'. 1121 * 1122 * @throws Horde_Browser_Exception 1123 */ 1124 public function wasFileUploaded($field, $name = null) 1125 { 1126 if (is_null($name)) { 1127 $name = 'file'; 1128 } 1129 1130 if (!($uploadSize = self::allowFileUploads())) { 1131 throw new Horde_Browser_Exception(Horde_Browser_Translation::t("File uploads not supported.")); 1132 } 1133 1134 /* Get any index on the field name. */ 1135 $index = Horde_Array::getArrayParts($field, $base, $keys); 1136 1137 if ($index) { 1138 /* Index present, fetch the error var to check. */ 1139 $keys_path = array_merge(array($base, 'error'), $keys); 1140 $error = Horde_Array::getElement($_FILES, $keys_path); 1141 1142 /* Index present, fetch the tmp_name var to check. */ 1143 $keys_path = array_merge(array($base, 'tmp_name'), $keys); 1144 $tmp_name = Horde_Array::getElement($_FILES, $keys_path); 1145 } else { 1146 /* No index, simple set up of vars to check. */ 1147 if (!isset($_FILES[$field])) { 1148 throw new Horde_Browser_Exception(Horde_Browser_Translation::t("No file uploaded"), UPLOAD_ERR_NO_FILE); 1149 } 1150 $error = $_FILES[$field]['error']; 1151 if (is_array($error)) { 1152 $error = reset($error); 1153 } 1154 $tmp_name = $_FILES[$field]['tmp_name']; 1155 if (is_array($tmp_name)) { 1156 $tmp_name = reset($tmp_name); 1157 } 1158 } 1159 1160 if (empty($_FILES)) { 1161 $error = UPLOAD_ERR_NO_FILE; 1162 } 1163 1164 switch ($error) { 1165 case UPLOAD_ERR_NO_FILE: 1166 throw new Horde_Browser_Exception(sprintf(Horde_Browser_Translation::t("There was a problem with the file upload: No %s was uploaded."), $name), UPLOAD_ERR_NO_FILE); 1167 1168 case UPLOAD_ERR_OK: 1169 if (is_uploaded_file($tmp_name) && !filesize($tmp_name)) { 1170 throw new Horde_Browser_Exception(Horde_Browser_Translation::t("The uploaded file appears to be empty. It may not exist on your computer."), UPLOAD_ERR_NO_FILE); 1171 } 1172 // SUCCESS 1173 break; 1174 1175 case UPLOAD_ERR_INI_SIZE: 1176 case UPLOAD_ERR_FORM_SIZE: 1177 throw new Horde_Browser_Exception(sprintf(Horde_Browser_Translation::t("There was a problem with the file upload: The %s was larger than the maximum allowed size (%d bytes)."), $name, $uploadSize), $error); 1178 1179 case UPLOAD_ERR_PARTIAL: 1180 throw new Horde_Browser_Exception(sprintf(Horde_Browser_Translation::t("There was a problem with the file upload: The %s was only partially uploaded."), $name), $error); 1181 1182 case UPLOAD_ERR_NO_TMP_DIR: 1183 throw new Horde_Browser_Exception( 1184 Horde_Browser_Translation::t("There was a problem with the file upload: The temporary folder used to store the upload data is missing."), 1185 $error 1186 ); 1187 1188 case UPLOAD_ERR_CANT_WRITE: 1189 // No reason to try to explain to user what a "PHP extension" is. 1190 case UPLOAD_ERR_EXTENSION: 1191 throw new Horde_Browser_Exception( 1192 Horde_Browser_Translation::t("There was a problem with the file upload: Can't write the uploaded data to the server."), 1193 $error 1194 ); 1195 } 1196 } 1197 1198 /** 1199 * Returns the headers for a browser download. 1200 * 1201 * @param string $filename The filename of the download. 1202 * @param string $cType The content-type description of the file. 1203 * @param boolean $inline True if inline, false if attachment. 1204 * @param string $cLength The content-length of this file. 1205 */ 1206 public function downloadHeaders( 1207 $filename = 'unknown', $cType = null, $inline = false, $cLength = null 1208 ) 1209 { 1210 /* Remove linebreaks from file names. */ 1211 $filename = str_replace(array("\r\n", "\r", "\n"), ' ', $filename); 1212 1213 /* Remove control characters from file names. */ 1214 $filename = preg_replace('/[\x00-\x1f]+/', '', $filename); 1215 1216 /* Some browsers don't like spaces in the filename. */ 1217 if ($this->hasQuirk('no_filename_spaces')) { 1218 $filename = strtr($filename, ' ', '_'); 1219 } 1220 1221 /* MSIE doesn't like multiple periods in the file name. Convert all 1222 * periods (except the last one) to underscores. */ 1223 if ($this->isBrowser('msie')) { 1224 if (($pos = strrpos($filename, '.'))) { 1225 $filename = strtr(substr($filename, 0, $pos), '.', '_') . substr($filename, $pos); 1226 } 1227 1228 /* Encode the filename so IE downloads it correctly. (Bug #129) */ 1229 $filename = rawurlencode($filename); 1230 } 1231 1232 /* Content-Type/Content-Disposition Header. */ 1233 if ($inline) { 1234 if (!is_null($cType)) { 1235 header('Content-Type: ' . trim($cType)); 1236 } elseif ($this->isBrowser('msie')) { 1237 header('Content-Type: application/x-msdownload'); 1238 } else { 1239 header('Content-Type: application/octet-stream'); 1240 } 1241 header('Content-Disposition: inline; filename="' . $filename . '"'); 1242 } else { 1243 if ($this->isBrowser('msie')) { 1244 header('Content-Type: application/x-msdownload'); 1245 } elseif (!is_null($cType)) { 1246 header('Content-Type: ' . trim($cType)); 1247 } else { 1248 header('Content-Type: application/octet-stream'); 1249 } 1250 1251 if ($this->hasQuirk('break_disposition_header')) { 1252 header('Content-Disposition: filename="' . $filename . '"'); 1253 } else { 1254 header('Content-Disposition: attachment; filename="' . $filename . '"'); 1255 } 1256 } 1257 1258 /* Content-Length Header. Only send if we are not compressing 1259 * output. */ 1260 if (!is_null($cLength) && 1261 !in_array('ob_gzhandler', ob_list_handlers())) { 1262 header('Content-Length: ' . $cLength); 1263 } 1264 1265 /* Overwrite Pragma: and other caching headers for IE. */ 1266 if ($this->hasQuirk('cache_ssl_downloads')) { 1267 header('Expires: 0'); 1268 header('Cache-Control: must-revalidate'); 1269 header('Pragma: public'); 1270 } 1271 } 1272 1273 /** 1274 * Determines if a browser can display a given MIME type. 1275 * 1276 * @param string $mimetype The MIME type to check. 1277 * 1278 * @return boolean True if the browser can display the MIME type. 1279 */ 1280 public function isViewable($mimetype) 1281 { 1282 $mimetype = Horde_String::lower($mimetype); 1283 list($type, $subtype) = explode('/', $mimetype); 1284 1285 if (!empty($this->_accept)) { 1286 $wildcard_match = false; 1287 1288 if (strpos($this->_accept, $mimetype) !== false) { 1289 return true; 1290 } 1291 1292 if (strpos($this->_accept, '*/*') !== false) { 1293 $wildcard_match = true; 1294 if ($type != 'image') { 1295 return true; 1296 } 1297 } 1298 1299 /* image/jpeg and image/pjpeg *appear* to be the same entity, but 1300 * Mozilla doesn't seem to want to accept the latter. For our 1301 * purposes, we will treat them the same. */ 1302 if ($this->isBrowser('mozilla') && 1303 ($mimetype == 'image/pjpeg') && 1304 (strpos($this->_accept, 'image/jpeg') !== false)) { 1305 return true; 1306 } 1307 1308 if (!$wildcard_match) { 1309 return false; 1310 } 1311 } 1312 1313 if (!$this->hasFeature('images') || ($type != 'image')) { 1314 return false; 1315 } 1316 1317 return in_array($subtype, $this->_images); 1318 } 1319 1320} 1321