Overview

Packages

  • Browser

Classes

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