1<?php
2/*
3 * vim:set softtabstop=4 shiftwidth=4 expandtab:
4 *
5 * LICENSE: GNU Affero General Public License, version 3 (AGPL-3.0-or-later)
6 * Copyright 2001 - 2020 Ampache.org
7 *
8 * This program is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU Affero General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 * GNU Affero General Public License for more details.
17 *
18 * You should have received a copy of the GNU Affero General Public License
19 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
20 *
21 */
22
23namespace Ampache\Module\Util;
24
25/**
26 *  #####################################################################
27 *  #                               Warning                             #
28 *  #                               #######                             #
29 *  # This external file is Ampache-adapted and probably unsynced with  #
30 *  # origin because abandoned by its original authors.                #
31 *  #                                                                   #
32 *  #####################################################################
33 *
34 * This provides capability information for the current web client.
35 *
36 * Browser identification is performed by examining the HTTP_USER_AGENT
37 * environment variable provided by the web server.
38 *
39 * @TODO http://ajaxian.com/archives/parse-user-agent
40 *
41 * Copyright 1999-2013 Horde LLC (http://www.horde.org/)
42 * Copyright 2011 Paul MacIain (local changes for Ampache)
43 *
44 * See the enclosed file COPYING for license information (LGPL). If you
45 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
46 *
47 * @author   Chuck Hagenbuch <chuck@horde.org>
48 * @author   Jon Parise <jon@horde.org>
49 * @category Horde
50 * @license  http://www.horde.org/licenses/lgpl21 LGPL
51 * @package  Browser
52 */
53class Horde_Browser
54{
55    /**
56     * Major version number.
57     *
58     * @var integer
59     */
60    private $_majorVersion = 0;
61
62    /**
63     * Minor version number.
64     *
65     * @var integer
66     */
67    private $_minorVersion = 0;
68
69    /**
70     * Browser name.
71     *
72     * @var string
73     */
74    private $_browser = '';
75
76    /**
77     * Full user agent string.
78     *
79     * @var string
80     */
81    private $_agent = '';
82
83    /**
84     * Lower-case user agent string.
85     *
86     * @var string
87     */
88    private $_lowerAgent = '';
89
90    /**
91     * HTTP_ACCEPT string
92     *
93     * @var string
94     */
95    private $_accept = '';
96
97    /**
98     * Platform the browser is running on.
99     *
100     * @var string
101     */
102    private $_platform = '';
103
104    /**
105     * Is this a mobile browser?
106     *
107     * @var boolean
108     */
109    private $_mobile = false;
110
111    /**
112     * Is this a tablet browser?
113     *
114     * @var boolean
115     */
116    private $_tablet = false;
117
118    /**
119     * Features.
120     *
121     * @var array
122     */
123    private $_features = array(
124        'frames' => true,
125        'html' => true,
126        'images' => true,
127        'java' => true,
128        'javascript' => true,
129        'tables' => true
130    );
131
132    /**
133     * Quirks.
134     *
135     * @var array
136     */
137    private $_quirks = array();
138
139    /**
140     * Creates a browser instance (Constructor).
141     */
142    public function __construct()
143    {
144        $this->match();
145    }
146
147    /**
148     * Parses the user agent string and initializes the object with all the
149     * known features and quirks for the given browser.
150     *
151     * @param string $userAgent The browser string to parse.
152     * @param string $accept The HTTP_ACCEPT settings to use.
153     */
154    public function match($userAgent = null, $accept = null)
155    {
156        // Set our agent string.
157        if ($userAgent == null) {
158            if (filter_has_var(INPUT_SERVER, 'HTTP_USER_AGENT')) {
159                $this->_agent = trim($_SERVER['HTTP_USER_AGENT']);
160            }
161        } else {
162            $this->_agent = $userAgent;
163        }
164        $this->_lowerAgent = strtolower($this->_agent);
165
166        // Set our accept string.
167        if ($accept === null) {
168            if (filter_has_var(INPUT_SERVER, 'HTTP_ACCEPT')) {
169                $this->_accept = strtolower(trim($_SERVER['HTTP_ACCEPT']));
170            }
171        } else {
172            $this->_accept = strtolower($accept);
173        }
174
175        // Check for UTF support.
176        if (filter_has_var(INPUT_SERVER, 'HTTP_ACCEPT_CHARSET')) {
177            $this->setFeature('utf', strpos(strtolower($_SERVER['HTTP_ACCEPT_CHARSET']), 'utf') !== false);
178        }
179
180        if (empty($this->_agent)) {
181            return;
182        }
183
184        $this->setPlatform();
185
186        // Use local scope for frequently accessed variables.
187        $agent      = $this->_agent;
188        $lowerAgent = $this->_lowerAgent;
189
190        if (strpos($lowerAgent, 'iemobile') !== false || strpos($lowerAgent,
191                'mobileexplorer') !== false || strpos($lowerAgent, 'openwave') !== false) {
192            $this->setFeature('frames', false);
193            $this->setFeature('javascript', false);
194            $this->setQuirk('avoid_popup_windows');
195            $this->_mobile = true;
196
197            if (preg_match('|iemobile[/ ]([0-9.]+)|', $lowerAgent, $version)) {
198                list($this->_majorVersion, $this->_minorVersion) = explode('.', $version[1]);
199                if ($this->_majorVersion >= 7) {
200                    // Windows Phone, Modern Browser
201                    $this->setBrowser('msie');
202                    $this->setFeature('javascript');
203                    $this->setFeature('xmlhttpreq');
204                    $this->setFeature('ajax');
205                    $this->setFeature('dom');
206                    $this->setFeature('utf');
207                    $this->setFeature('rte');
208                    $this->setFeature('cite');
209                }
210            }
211        } elseif (strpos($lowerAgent, 'opera mini') !== false || strpos($lowerAgent, 'operamini') !== false) {
212            $this->setBrowser('opera');
213            $this->setFeature('frames', false);
214            $this->setFeature('javascript');
215            $this->setQuirk('avoid_popup_windows');
216            $this->_mobile = true;
217        } elseif (preg_match('|Opera[/ ]([0-9.]+)|', $agent, $version)) {
218            $this->setBrowser('opera');
219            list($this->_majorVersion, $this->_minorVersion) = explode('.', $version[1]);
220            $this->setFeature('javascript');
221            $this->setQuirk('no_filename_spaces');
222
223            /* Opera Mobile reports its screen resolution in the user
224             * agent strings. */
225            if (preg_match('/; (120x160|240x280|240x320|320x320)\)/', $agent)) {
226                $this->_mobile = true;
227            } elseif (preg_match('|Tablet|', $agent)) {
228                $this->_mobile = true;
229                $this->_tablet = true;
230            }
231
232            if ($this->_majorVersion >= 7) {
233                if ($this->_majorVersion >= 8) {
234                    $this->setFeature('xmlhttpreq');
235                    $this->setFeature('javascript', 1.5);
236                }
237                if ($this->_majorVersion >= 9) {
238                    $this->setFeature('dataurl', 4100);
239                    if ($this->_minorVersion >= 5) {
240                        $this->setFeature('ajax');
241                        $this->setFeature('rte');
242                    }
243                }
244                $this->setFeature('dom');
245                $this->setFeature('iframes');
246                $this->setFeature('accesskey');
247                $this->setFeature('optgroup');
248                $this->setQuirk('double_linebreak_textarea');
249            }
250        } elseif (strpos($lowerAgent, 'elaine/') !== false || strpos($lowerAgent,
251                'palmsource') !== false || strpos($lowerAgent, 'digital paths') !== false) {
252            $this->setBrowser('palm');
253            $this->setFeature('images', false);
254            $this->setFeature('frames', false);
255            $this->setFeature('javascript', false);
256            $this->setQuirk('avoid_popup_windows');
257            $this->_mobile = true;
258        } elseif ((preg_match('|MSIE ([0-9.]+)|', $agent, $version)) || (preg_match('|Internet Explorer/([0-9.]+)|',
259                $agent, $version))) {
260            $this->setBrowser('msie');
261            $this->setQuirk('cache_ssl_downloads');
262            $this->setQuirk('cache_same_url');
263            $this->setQuirk('break_disposition_filename');
264
265            if (strpos($version[1], '.') !== false) {
266                list($this->_majorVersion, $this->_minorVersion) = explode('.', $version[1]);
267            } else {
268                $this->_majorVersion = $version[1];
269                $this->_minorVersion = 0;
270            }
271
272            /* IE (< 7) on Windows does not support alpha transparency
273             * in PNG images. */
274            if (($this->_majorVersion < 7) && preg_match('/windows/i', $agent)) {
275                $this->setQuirk('png_transparency');
276            }
277
278            /* Some Handhelds have their screen resolution in the user
279             * agent string, which we can use to look for mobile
280             * agents. */
281            if (preg_match('/; (120x160|240x280|240x320|320x320)\)/', $agent)) {
282                $this->_mobile = true;
283            }
284
285            $this->setFeature('xmlhttpreq');
286
287            switch ($this->_majorVersion) {
288                default:
289                case 10:
290                case 9:
291                case 8:
292                case 7:
293                    $this->setFeature('javascript', 1.4);
294                    $this->setFeature('ajax');
295                    $this->setFeature('dom');
296                    $this->setFeature('iframes');
297                    $this->setFeature('utf');
298                    $this->setFeature('rte');
299                    $this->setFeature('homepage');
300                    $this->setFeature('accesskey');
301                    $this->setFeature('optgroup');
302                    if ($this->_majorVersion != 7) {
303                        $this->setFeature('cite');
304                        $this->setFeature('dataurl', ($this->_majorVersion == 8) ? 32768 : true);
305                    }
306                    break;
307                case 6:
308                    $this->setFeature('javascript', 1.4);
309                    $this->setFeature('dom');
310                    $this->setFeature('iframes');
311                    $this->setFeature('utf');
312                    $this->setFeature('rte');
313                    $this->setFeature('homepage');
314                    $this->setFeature('accesskey');
315                    $this->setFeature('optgroup');
316                    $this->setQuirk('scrollbar_in_way');
317                    $this->setQuirk('broken_multipart_form');
318                    $this->setQuirk('windowed_controls');
319                    break;
320                case 5:
321                    if ($this->getPlatform() == 'mac') {
322                        $this->setFeature('javascript', 1.2);
323                        $this->setFeature('optgroup');
324                        $this->setFeature('xmlhttpreq', false);
325                    } else {
326                        // MSIE 5 for Windows.
327                        $this->setFeature('javascript', 1.4);
328                        $this->setFeature('dom');
329                        if ($this->_minorVersion >= 5) {
330                            $this->setFeature('rte');
331                            $this->setQuirk('windowed_controls');
332                        }
333                    }
334                    $this->setFeature('iframes');
335                    $this->setFeature('utf');
336                    $this->setFeature('homepage');
337                    $this->setFeature('accesskey');
338                    if ($this->_minorVersion == 5) {
339                        $this->setQuirk('break_disposition_header');
340                        $this->setQuirk('broken_multipart_form');
341                    }
342                    break;
343                case 4:
344                    $this->setFeature('javascript', 1.2);
345                    $this->setFeature('accesskey');
346                    $this->setFeature('xmlhttpreq', false);
347                    if ($this->_minorVersion > 0) {
348                        $this->setFeature('utf');
349                    }
350                    break;
351                case 3:
352                    $this->setFeature('javascript', 1.1);
353                    $this->setQuirk('avoid_popup_windows');
354                    $this->setFeature('xmlhttpreq', false);
355                    break;
356            }
357        } elseif (preg_match('|ANTFresco/([0-9]+)|', $agent, $version)) {
358            $this->setBrowser('fresco');
359            $this->setFeature('javascript', 1.1);
360            $this->setQuirk('avoid_popup_windows');
361        } elseif (strpos($lowerAgent, 'avantgo') !== false) {
362            $this->setBrowser('avantgo');
363            $this->_mobile = true;
364        } elseif (preg_match('|Konqueror/([0-9]+)\.?([0-9]+)?|', $agent,
365                $version) || preg_match('|Safari/([0-9]+)\.?([0-9]+)?|', $agent, $version)) {
366            $this->setBrowser('webkit');
367            $this->setQuirk('empty_file_input_value');
368            $this->setQuirk('no_hidden_overflow_tables');
369            $this->setFeature('dataurl');
370
371            if (strpos($agent, 'Mobile') !== false || strpos($agent, 'Android') !== false || strpos($agent,
372                    'SAMSUNG-GT') !== false || ((strpos($agent, 'Nokia') !== false || strpos($agent,
373                            'Symbian') !== false) && strpos($agent, 'WebKit') !== false) || (strpos($agent,
374                        'N900') !== false && strpos($agent, 'Maemo Browser') !== false) || (strpos($agent,
375                        'MeeGo') !== false && strpos($agent, 'NokiaN9') !== false)) {
376                // WebKit Mobile
377                $this->setFeature('frames', false);
378                $this->setFeature('javascript');
379                $this->setQuirk('avoid_popup_windows');
380                $this->_mobile = true;
381            }
382
383            $this->_majorVersion = $version[1];
384            if (isset($version[2])) {
385                $this->_minorVersion = $version[2];
386            }
387
388            if (stripos($agent, 'Chrome/') !== false || stripos($agent, 'CriOS/') !== false) {
389                // Google Chrome.
390                $this->setFeature('ischrome');
391                $this->setFeature('rte');
392                $this->setFeature('utf');
393                $this->setFeature('javascript', 1.4);
394                $this->setFeature('ajax');
395                $this->setFeature('dom');
396                $this->setFeature('iframes');
397                $this->setFeature('accesskey');
398                $this->setFeature('xmlhttpreq');
399                $this->setQuirk('empty_file_input_value', 0);
400
401                if (preg_match('|Chrome/([0-9.]+)|i', $agent, $version_string)) {
402                    list($this->_majorVersion, $this->_minorVersion) = explode('.', $version_string[1], 2);
403                }
404            } elseif (stripos($agent, 'Safari/') !== false && $this->_majorVersion >= 60) {
405                // Safari.
406                $this->setFeature('issafari');
407
408                // Truly annoying - Safari did not start putting real version
409                // numbers until Version 3.
410                if (preg_match('|Version/([0-9.]+)|', $agent, $version_string)) {
411                    list($this->_majorVersion, $this->_minorVersion) = explode('.', $version_string[1], 2);
412                    $this->_minorVersion                             = (int)($this->_minorVersion);
413                    $this->setFeature('ajax');
414                    $this->setFeature('rte');
415                } elseif ($this->_majorVersion >= 412) {
416                    $this->_majorVersion = 2;
417                    $this->_minorVersion = 0;
418                } else {
419                    if ($this->_majorVersion >= 312) {
420                        $this->_minorVersion = 3;
421                    } elseif ($this->_majorVersion >= 124) {
422                        $this->_minorVersion = 2;
423                    } else {
424                        $this->_minorVersion = 0;
425                    }
426                    $this->_majorVersion = 1;
427                }
428
429                $this->setFeature('utf');
430                $this->setFeature('javascript', 1.4);
431                $this->setFeature('dom');
432                $this->setFeature('iframes');
433                if ($this->_majorVersion > 1 || $this->_minorVersion > 2) {
434                    // As of Safari 1.3
435                    $this->setFeature('accesskey');
436                    $this->setFeature('xmlhttpreq');
437                }
438            } else {
439                // Konqueror.
440                $this->setFeature('javascript', 1.1);
441                $this->setFeature('iskonqueror');
442                switch ($this->_majorVersion) {
443                    case 4:
444                    case 3:
445                        $this->setFeature('dom');
446                        $this->setFeature('iframes');
447                        if ($this->_minorVersion >= 5 || $this->_majorVersion == 4) {
448                            $this->setFeature('accesskey');
449                            $this->setFeature('xmlhttpreq');
450                        }
451                        break;
452                }
453            }
454        } elseif (preg_match('|Mozilla/([0-9.]+)|', $agent, $version)) {
455            $this->setBrowser('mozilla');
456            $this->setQuirk('must_cache_forms');
457
458            list($this->_majorVersion, $this->_minorVersion) = explode('.', $version[1]);
459            switch ($this->_majorVersion) {
460                default:
461                case 5:
462                    if ($this->getPlatform() == 'win') {
463                        $this->setQuirk('break_disposition_filename');
464                    }
465                    $this->setFeature('javascript', 1.4);
466                    $this->setFeature('ajax');
467                    $this->setFeature('dom');
468                    $this->setFeature('accesskey');
469                    $this->setFeature('optgroup');
470                    $this->setFeature('xmlhttpreq');
471                    $this->setFeature('cite');
472                    if (preg_match('|rv:(.*)\)|', $agent, $revision)) {
473                        if (version_compare($revision[1], '1', '>=')) {
474                            $this->setFeature('iframes');
475                        }
476                        if (version_compare($revision[1], '1.3', '>=')) {
477                            $this->setFeature('rte');
478                        }
479                        if (version_compare($revision[1], '1.8.1', '>=')) {
480                            $this->setFeature('dataurl');
481                        }
482                        if (version_compare($revision[1], '10.0', '>=')) {
483                            $this->setFeature('utf');
484                        }
485                    }
486                    if (stripos($agent, 'mobile') !== false) {
487                        $this->_mobile = true;
488                    } elseif (stripos($agent, 'tablet') !== false) {
489                        $this->_tablet = true;
490                        $this->_mobile = true;
491                    }
492                    break;
493                case 4:
494                    $this->setFeature('javascript', 1.3);
495                    $this->setQuirk('buggy_compression');
496                    break;
497                case 3:
498                case 2:
499                case 1:
500                case 0:
501                    $this->setFeature('javascript', 1);
502                    $this->setQuirk('buggy_compression');
503                    break;
504            }
505        } elseif (preg_match('|Lynx/([0-9]+)|', $agent, $version)) {
506            $this->setBrowser('lynx');
507            $this->setFeature('images', false);
508            $this->setFeature('frames', false);
509            $this->setFeature('javascript', false);
510            $this->setQuirk('avoid_popup_windows');
511        } elseif (preg_match('|Links \(([0-9]+)|', $agent, $version)) {
512            $this->setBrowser('links');
513            $this->setFeature('images', false);
514            $this->setFeature('frames', false);
515            $this->setFeature('javascript', false);
516            $this->setQuirk('avoid_popup_windows');
517        } elseif (preg_match('|HotJava/([0-9]+)|', $agent, $version)) {
518            $this->setBrowser('hotjava');
519            $this->setFeature('javascript', false);
520        } elseif (strpos($agent, 'UP/') !== false || strpos($agent, 'UP.B') !== false || strpos($agent,
521                'UP.L') !== false) {
522            $this->setBrowser('up');
523            $this->setFeature('html', false);
524            $this->setFeature('javascript', false);
525            $this->setFeature('hdml');
526            $this->setFeature('wml');
527
528            if (strpos($agent, 'GUI') !== false && strpos($agent, 'UP.Link') !== false) {
529                /* The device accepts Openwave GUI extensions for WML
530                 * 1.3. Non-UP.Link gateways sometimes have problems,
531                 * so exclude them. */
532                $this->setQuirk('ow_gui_1.3');
533            }
534            $this->_mobile = true;
535        } elseif (strpos($agent, 'Xiino/') !== false) {
536            $this->setBrowser('xiino');
537            $this->setFeature('hdml');
538            $this->setFeature('wml');
539            $this->_mobile = true;
540        } elseif (strpos($agent, 'Palmscape/') !== false) {
541            $this->setBrowser('palmscape');
542            $this->setFeature('javascript', false);
543            $this->setFeature('hdml');
544            $this->setFeature('wml');
545            $this->_mobile = true;
546        } elseif (strpos($agent, 'Nokia') !== false) {
547            $this->setBrowser('nokia');
548            $this->setFeature('html', false);
549            $this->setFeature('wml');
550            $this->setFeature('xhtml');
551            $this->_mobile = true;
552        } elseif (strpos($agent, 'Ericsson') !== false) {
553            $this->setBrowser('ericsson');
554            $this->setFeature('html', false);
555            $this->setFeature('wml');
556            $this->_mobile = true;
557        } elseif (strpos($agent, 'Grundig') !== false) {
558            $this->setBrowser('grundig');
559            $this->setFeature('xhtml');
560            $this->setFeature('wml');
561            $this->_mobile = true;
562        } elseif (strpos($agent, 'NetFront') !== false) {
563            $this->setBrowser('netfront');
564            $this->setFeature('xhtml');
565            $this->setFeature('wml');
566            $this->_mobile = true;
567        } elseif (strpos($lowerAgent, 'wap') !== false) {
568            $this->setBrowser('wap');
569            $this->setFeature('html', false);
570            $this->setFeature('javascript', false);
571            $this->setFeature('hdml');
572            $this->setFeature('wml');
573            $this->_mobile = true;
574        } elseif (strpos($lowerAgent, 'docomo') !== false || strpos($lowerAgent, 'portalmmm') !== false) {
575            $this->setBrowser('imode');
576            $this->setFeature('images', false);
577            $this->_mobile = true;
578        } elseif (preg_match('|BlackBerry.*?/([0-9.]+)|', $agent, $version)) {
579            list($this->_majorVersion, $this->_minorVersion) = explode('.', $version[1]);
580            $this->setBrowser('blackberry');
581            $this->setFeature('html', false);
582            $this->setFeature('javascript', false);
583            $this->setFeature('hdml');
584            $this->setFeature('wml');
585            $this->_mobile = true;
586            if ($this->_majorVersion >= 5 || ($this->_majorVersion == 4 && $this->_minorVersion >= 6)) {
587                $this->setFeature('ajax');
588                $this->setFeature('iframes');
589                $this->setFeature('javascript', 1.5);
590                $this->setFeature('dom');
591                $this->setFeature('xmlhttpreq');
592            }
593        } elseif (strpos($agent, 'MOT-') !== false) {
594            $this->setBrowser('motorola');
595            $this->setFeature('html', false);
596            $this->setFeature('javascript', false);
597            $this->setFeature('hdml');
598            $this->setFeature('wml');
599            $this->_mobile = true;
600        } elseif (strpos($lowerAgent, 'j-') !== false) {
601            $this->setBrowser('mml');
602            $this->_mobile = true;
603        }
604    }
605
606    /**
607     * Matches the platform of the browser.
608     *
609     * This is a pretty simplistic implementation, but it's intended to let us
610     * tell what line breaks to send, so it's good enough for its purpose.
611     */
612    private function setPlatform()
613    {
614        if (strpos($this->_lowerAgent, 'wind') !== false) {
615            $this->_platform = 'win';
616        } elseif (strpos($this->_lowerAgent, 'mac') !== false) {
617            $this->_platform = 'mac';
618        } else {
619            $this->_platform = 'unix';
620        }
621    }
622
623    /**
624     * Returns the currently matched platform.
625     *
626     * @return string  The user's platform.
627     */
628    private function getPlatform()
629    {
630        return $this->_platform;
631    }
632
633    /**
634     * Sets the current browser.
635     *
636     * @param string $browser The browser to set as current.
637     */
638    private function setBrowser($browser)
639    {
640        $this->_browser = $browser;
641    }
642
643    /**
644     * Determines if the given browser is the same as the current.
645     *
646     * @param string $browser The browser to check.
647     *
648     * @return boolean  Is the given browser the same as the current?
649     */
650    public function isBrowser($browser)
651    {
652        return ($this->_browser === $browser);
653    }
654
655    /**
656     * Sets unique behavior for the current browser.
657     *
658     * @param string $quirk The behavior to set. Quirks:
659     *   - avoid_popup_windows
660     *   - break_disposition_header
661     *   - break_disposition_filename
662     *   - broken_multipart_form
663     *   - buggy_compression
664     *   - cache_same_url
665     *   - cache_ssl_downloads
666     *   - double_linebreak_textarea
667     *   - empty_file_input_value
668     *   - must_cache_forms
669     *   - no_filename_spaces
670     *   - no_hidden_overflow_tables
671     *   - ow_gui_1.3
672     *   - png_transparency
673     *   - scrollbar_in_way
674     *   - scroll_tds
675     *   - windowed_controls
676     * @param boolean $value Special behavior parameter.
677     */
678    private function setQuirk($quirk, $value = true)
679    {
680        if ($value) {
681            $this->_quirks[$quirk] = $value;
682        } else {
683            unset($this->_quirks[$quirk]);
684        }
685    }
686
687    /**
688     * Checks unique behavior for the current browser.
689     *
690     * @param string $quirk The behavior to check.
691     *
692     * @return boolean  Does the browser have the behavior set?
693     */
694    private function hasQuirk($quirk)
695    {
696        return !empty($this->_quirks[$quirk]);
697    }
698
699    /**
700     * Sets capabilities for the current browser.
701     *
702     * @param string $feature The capability to set. Features:
703     *   - accesskey
704     *   - ajax
705     *   - cite
706     *   - dataurl
707     *   - dom
708     *   - frames
709     *   - hdml
710     *   - html
711     *   - homepage
712     *   - iframes
713     *   - images
714     *   - ischrome
715     *   - iskonqueror
716     *   - issafari
717     *   - java
718     *   - javascript
719     *   - optgroup
720     *   - rte
721     *   - tables
722     *   - utf
723     *   - wml
724     *   - xmlhttpreq
725     * @param boolean $value Special capability parameter.
726     */
727    public function setFeature($feature, $value = true)
728    {
729        if ($value) {
730            $this->_features[$feature] = $value;
731        } else {
732            unset($this->_features[$feature]);
733        }
734    }
735
736    /**
737     * Returns the headers for a browser download.
738     *
739     * @param string $filename The filename of the download.
740     * @param string $cType The content-type description of the file.
741     * @param boolean $inline True if inline, false if attachment.
742     * @param string $cLength The content-length of this file.
743     *
744     * @return string[]
745     */
746    public function getDownloadHeaders(
747        $filename = 'unknown',
748        $cType = null,
749        $inline = false,
750        $cLength = null
751    ): array {
752        /* Remove linebreaks from file names. */
753        $filename = str_replace(array("\r\n", "\r", "\n"), ' ', $filename);
754
755        /* Some browsers don't like spaces in the filename. */
756        if ($this->hasQuirk('no_filename_spaces')) {
757            $filename = strtr($filename, ' ', '_');
758        }
759
760        /* MSIE doesn't like multiple periods in the file name. Convert all
761         * periods (except the last one) to underscores. */
762        if ($this->isBrowser('msie')) {
763            if (($pos = strrpos($filename, '.'))) {
764                $filename = strtr(substr($filename, 0, $pos), '.', '_') . substr($filename, $pos);
765            }
766
767            /* Encode the filename so IE downloads it correctly. (Bug #129) */
768            $filename = rawurlencode($filename);
769        }
770
771        $headers = [];
772
773        /* Content-Type/Content-Disposition Header. */
774        if ($inline) {
775            if ($cType !== null) {
776                $headers['Content-Type'] = trim($cType);
777            } elseif ($this->isBrowser('msie')) {
778                $headers['Content-Type'] = 'application/x-msdownload';
779            } else {
780                $headers['Content-Type'] = 'application/octet-stream';
781            }
782            $headers['Content-Disposition'] = 'inline; filename="' . $filename . '"';
783        } else {
784            if ($this->isBrowser('msie')) {
785                $headers['Content-Type'] = 'application/x-msdownload';
786            } elseif ($cType !== null) {
787                $headers['Content-Type'] = trim($cType);
788            } else {
789                $headers['Content-Type'] = 'application/octet-stream';
790            }
791
792            if ($this->hasQuirk('break_disposition_header')) {
793                $headers['Content-Disposition'] = 'filename="' . $filename . '"';
794            } else {
795                $headers['Content-Disposition'] = 'attachment; filename="' . $filename . '"';
796            }
797        }
798
799        /* Content-Length Header. Only send if we are not compressing
800         * output. */
801        if ($cLength !== null && !in_array('ob_gzhandler', ob_list_handlers())) {
802            $headers['Content-Length'] = $cLength;
803        }
804
805        /* Overwrite Pragma: and other caching headers for IE. */
806        if ($this->hasQuirk('cache_ssl_downloads')) {
807            $headers['Expires']       = 0;
808            $headers['Cache-Control'] = 'must-revalidate';
809            $headers['Pragma']        = 'public';
810        }
811
812        return $headers;
813    }
814}
815