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