MDL-66625 forumreport_summary: Adding behat
[moodle.git] / lib / classes / useragent.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Environment class to aid with the detection and establishment of the working environment.
19  *
20  * @package    core
21  * @copyright  2013 Sam Hemelryk
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 /**
26  * The user agent class.
27  *
28  * It's important to note that we do not like browser sniffing and its use in core code is highly discouraged.
29  * No new uses of this API will be integrated unless there is absolutely no alternative.
30  *
31  * This API supports the few browser checks we do have in core, all of which one day will hopefully be removed.
32  * The API will remain to support any third party use out there, however at some point like all code it will be deprecated.
33  *
34  * Use sparingly and only with good cause!
35  *
36  * @package    core
37  * @copyright  2013 Sam Hemelryk
38  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39  */
40 class core_useragent {
42     /**
43      * The default for devices, think of as a computer.
44      */
45     const DEVICETYPE_DEFAULT = 'default';
46     /**
47      * Legacy devices, or at least legacy browsers. These are older devices/browsers
48      * that don't support standards.
49      */
50     const DEVICETYPE_LEGACY = 'legacy';
51     /**
52      * Mobile devices like your cell phone or hand held gaming device.
53      */
54     const DEVICETYPE_MOBILE = 'mobile';
55     /**
56      * Tables, larger than hand held, but still easily portable and smaller than a laptop.
57      */
58     const DEVICETYPE_TABLET = 'tablet';
60     /**
61      * An instance of this class.
62      * @var core_useragent
63      */
64     protected static $instance = null;
66     /**
67      * The device types we track.
68      * @var array
69      */
70     public static $devicetypes = array(
71         self::DEVICETYPE_DEFAULT,
72         self::DEVICETYPE_LEGACY,
73         self::DEVICETYPE_MOBILE,
74         self::DEVICETYPE_TABLET,
75     );
77     /**
78      * The current requests user agent string if there was one.
79      * @var string|bool|null Null until initialised, false if none available, or string when available.
80      */
81     protected $useragent = null;
83     /**
84      * The users device type, one of self::DEVICETYPE_*.
85      * @var string null until initialised
86      */
87     protected $devicetype = null;
89     /**
90      * Custom device types entered into the admin interface.
91      * @var array
92      */
93     protected $devicetypecustoms = array();
95     /**
96      * True if the user agent supports the display of svg images. False if not.
97      * @var bool|null Null until initialised, then true or false.
98      */
99     protected $supportssvg = null;
101     /**
102      * Get an instance of the user agent object.
103      *
104      * @param bool $reload If set to true the user agent will be reset and all ascertations remade.
105      * @param string $forceuseragent The string to force as the user agent, don't use unless absolutely unavoidable.
106      * @return core_useragent
107      */
108     public static function instance($reload = false, $forceuseragent = null) {
109         if (!self::$instance || $reload) {
110             self::$instance = new core_useragent($forceuseragent);
111         }
112         return self::$instance;
113     }
115     /**
116      * Constructs a new user agent object. Publically you must use the instance method above.
117      *
118      * @param string|null $forceuseragent Optional a user agent to force.
119      */
120     protected function __construct($forceuseragent = null) {
121         global $CFG;
122         if (!empty($CFG->devicedetectregex)) {
123             $this->devicetypecustoms = json_decode($CFG->devicedetectregex, true);
124         }
125         if ($this->devicetypecustoms === null) {
126             // This shouldn't happen unless you're hardcoding the config value.
127             debugging('Config devicedetectregex is not valid JSON object');
128             $this->devicetypecustoms = array();
129         }
130         if ($forceuseragent !== null) {
131             $this->useragent = $forceuseragent;
132         } else if (!empty($_SERVER['HTTP_USER_AGENT'])) {
133             $this->useragent = $_SERVER['HTTP_USER_AGENT'];
134         } else {
135             $this->useragent = false;
136             $this->devicetype = self::DEVICETYPE_DEFAULT;
137         }
138     }
140     /**
141      * Get the MoodleBot UserAgent for this site.
142      *
143      * @return string UserAgent
144      */
145     public static function get_moodlebot_useragent() {
146         global $CFG;
148         $version = moodle_major_version(); // Only major version for security.
149         return "MoodleBot/$version (+{$CFG->wwwroot})";
150     }
152     /**
153      * Returns the user agent string.
154      * @return bool|string The user agent string or false if one isn't available.
155      */
156     public static function get_user_agent_string() {
157         $instance = self::instance();
158         return $instance->useragent;
159     }
161     /**
162      * Returns the device type we believe is being used.
163      * @return string
164      */
165     public static function get_device_type() {
166         $instance = self::instance();
167         if ($instance->devicetype === null) {
168             return $instance->guess_device_type();
169         }
170         return $instance->devicetype;
171     }
173     /**
174      * Guesses the device type the user agent is running on.
175      *
176      * @return string
177      */
178     protected function guess_device_type() {
179         global $CFG;
180         if (empty($CFG->enabledevicedetection)) {
181             $this->devicetype = self::DEVICETYPE_DEFAULT;
182             return $this->devicetype;
183         }
184         foreach ($this->devicetypecustoms as $value => $regex) {
185             if (preg_match($regex, $this->useragent)) {
186                 $this->devicetype = $value;
187                 return $this->devicetype;
188             }
189         }
190         if ($this->is_useragent_mobile()) {
191             $this->devicetype = self::DEVICETYPE_MOBILE;
192         } else if ($this->is_useragent_tablet()) {
193             $this->devicetype = self::DEVICETYPE_TABLET;
194         } else if (self::check_ie_version('0') && !self::check_ie_version('7.0')) {
195             // IE 6 and before are considered legacy.
196             $this->devicetype = self::DEVICETYPE_LEGACY;
197         } else {
198             $this->devicetype = self::DEVICETYPE_DEFAULT;
199         }
200         return $this->devicetype;
201     }
203     /**
204      * Returns true if the user appears to be on a mobile device.
205      * @return bool
206      */
207     protected function is_useragent_mobile() {
208         // Mobile detection PHP direct copy from open source detectmobilebrowser.com.
209         $phonesregex = '/android .+ mobile|avantgo|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i';
210         $modelsregex = '/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|e\-|e\/|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(di|rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|xda(\-|2|g)|yas\-|your|zeto|zte\-/i';
211         return (preg_match($phonesregex, $this->useragent) || preg_match($modelsregex, substr($this->useragent, 0, 4)));
212     }
214     /**
215      * Returns true if the user appears to be on a tablet.
216      *
217      * @return int
218      */
219     protected function is_useragent_tablet() {
220         $tabletregex = '/Tablet browser|android|iPad|iProd|GT-P1000|GT-I9000|SHW-M180S|SGH-T849|SCH-I800|Build\/ERE27|sholest/i';
221         return (preg_match($tabletregex, $this->useragent));
222     }
224     /**
225      * Whether the user agent relates to a web crawler.
226      * This includes all types of web crawler.
227      * @return bool
228      */
229     protected function is_useragent_web_crawler() {
230         $regex = '/MoodleBot|Googlebot|google\.com|Yahoo! Slurp|\[ZSEBOT\]|msnbot|bingbot|BingPreview|Yandex|AltaVista'
231                 .'|Baiduspider|Teoma/i';
232         return (preg_match($regex, $this->useragent));
233     }
235     /**
236      * Gets a list of known device types.
237      *
238      * @param bool $includecustomtypes If set to true we'll include types that have been added by the admin.
239      * @return array
240      */
241     public static function get_device_type_list($includecustomtypes = true) {
242         $types = self::$devicetypes;
243         if ($includecustomtypes) {
244             $instance = self::instance();
245             $types = array_merge($types, array_keys($instance->devicetypecustoms));
246         }
247         return $types;
248     }
250     /**
251      * Returns the theme to use for the given device type.
252      *
253      * This used to be get_selected_theme_for_device_type.
254      * @param null|string $devicetype The device type to find out for. Defaults to the device the user is using,
255      * @return bool
256      */
257     public static function get_device_type_theme($devicetype = null) {
258         global $CFG;
259         if ($devicetype === null) {
260             $devicetype = self::get_device_type();
261         }
262         $themevarname = self::get_device_type_cfg_var_name($devicetype);
263         if (empty($CFG->$themevarname)) {
264             return false;
265         }
266         return $CFG->$themevarname;
267     }
269     /**
270      * Returns the CFG var used to find the theme to use for the given device.
271      *
272      * Used to be get_device_cfg_var_name.
273      *
274      * @param null|string $devicetype The device type to find out for. Defaults to the device the user is using,
275      * @return string
276      */
277     public static function get_device_type_cfg_var_name($devicetype = null) {
278         if ($devicetype == self::DEVICETYPE_DEFAULT || empty($devicetype)) {
279             return 'theme';
280         }
281         return 'theme' . $devicetype;
282     }
284     /**
285      * Gets the device type the user is currently using.
286      * @return string
287      */
288     public static function get_user_device_type() {
289         $device = self::get_device_type();
290         $switched = get_user_preferences('switchdevice'.$device, false);
291         if ($switched != false) {
292             return $switched;
293         }
294         return $device;
295     }
297     /**
298      * Switches the device type we think the user is using to what ever was given.
299      * @param string $newdevice
300      * @return bool
301      * @throws coding_exception
302      */
303     public static function set_user_device_type($newdevice) {
304         $devicetype = self::get_device_type();
305         if ($newdevice == $devicetype) {
306             unset_user_preference('switchdevice'.$devicetype);
307             return true;
308         } else {
309             $devicetypes = self::get_device_type_list();
310             if (in_array($newdevice, $devicetypes)) {
311                 set_user_preference('switchdevice'.$devicetype, $newdevice);
312                 return true;
313             }
314         }
315         throw new coding_exception('Invalid device type provided to set_user_device_type');
316     }
318     /**
319      * Returns true if the user agent matches the given brand and the version is equal to or greater than that specified.
320      *
321      * @param string $brand The branch to check for.
322      * @param scalar $version The version if we need to find out if it is equal to or greater than that specified.
323      * @return bool
324      */
325     public static function check_browser_version($brand, $version = null) {
326         switch ($brand) {
328             case 'MSIE':
329                 // Internet Explorer.
330                 return self::check_ie_version($version);
332             case 'Edge':
333                 // Microsoft Edge.
334                 return self::check_edge_version($version);
336             case 'Firefox':
337                 // Mozilla Firefox browsers.
338                 return self::check_firefox_version($version);
340             case 'Chrome':
341                 return self::check_chrome_version($version);
343             case 'Opera':
344                 // Opera.
345                 return self::check_opera_version($version);
347             case 'Safari':
348                 // Desktop version of Apple Safari browser - no mobile or touch devices.
349                 return self::check_safari_version($version);
351             case 'Safari iOS':
352                 // Safari on iPhone, iPad and iPod touch.
353                 return self::check_safari_ios_version($version);
355             case 'WebKit':
356                 // WebKit based browser - everything derived from it (Safari, Chrome, iOS, Android and other mobiles).
357                 return self::check_webkit_version($version);
359             case 'Gecko':
360                 // Gecko based browsers.
361                 return self::check_gecko_version($version);
363             case 'WebKit Android':
364                 // WebKit browser on Android.
365                 return self::check_webkit_android_version($version);
367             case 'Camino':
368                 // OSX browser using Gecke engine.
369                 return self::check_camino_version($version);
370         }
371         // Who knows?! doesn't pass anyway.
372         return false;
373     }
375     /**
376      * Checks the user agent is camino based and that the version is equal to or greater than that specified.
377      *
378      * Camino browser is at the end of its life, its no longer being developed or supported, just don't worry about it.
379      *
380      * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
381      * @return bool
382      */
383     protected static function check_camino_version($version = null) {
384         // OSX browser using Gecko engine.
385         $useragent = self::get_user_agent_string();
386         if ($useragent === false) {
387             return false;
388         }
389         if (strpos($useragent, 'Camino') === false) {
390             return false;
391         }
392         if (empty($version)) {
393             return true; // No version specified.
394         }
395         if (preg_match("/Camino\/([0-9\.]+)/i", $useragent, $match)) {
396             if (version_compare($match[1], $version) >= 0) {
397                 return true;
398             }
399         }
400         return false;
401     }
403     /**
404      * Checks the user agent is Firefox (of any version).
405      *
406      * @return bool true if firefox
407      */
408     public static function is_firefox() {
409         return self::check_firefox_version();
410     }
412     /**
413      * Checks the user agent is Firefox based and that the version is equal to or greater than that specified.
414      *
415      * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
416      * @return bool
417      */
418     public static function check_firefox_version($version = null) {
419         // Mozilla Firefox browsers.
420         $useragent = self::get_user_agent_string();
421         if ($useragent === false) {
422             return false;
423         }
424         if (strpos($useragent, 'Firefox') === false && strpos($useragent, 'Iceweasel') === false) {
425             return false;
426         }
427         if (empty($version)) {
428             return true; // No version specified..
429         }
430         if (preg_match("/(Iceweasel|Firefox)\/([0-9\.]+)/i", $useragent, $match)) {
431             if (version_compare($match[2], $version) >= 0) {
432                 return true;
433             }
434         }
435         return false;
436     }
438     /**
439      * Checks the user agent is Gecko based (of any version).
440      *
441      * @return bool true if Gecko based.
442      */
443     public static function is_gecko() {
444         return self::check_gecko_version();
445     }
447     /**
448      * Checks the user agent is Gecko based and that the version is equal to or greater than that specified.
449      *
450      * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
451      * @return bool
452      */
453     public static function check_gecko_version($version = null) {
454         // Gecko based browsers.
455         // Do not look for dates any more, we expect real Firefox version here.
456         $useragent = self::get_user_agent_string();
457         if ($useragent === false) {
458             return false;
459         }
460         if (empty($version)) {
461             $version = 1;
462         } else if ($version > 20000000) {
463             // This is just a guess, it is not supposed to be 100% accurate!
464             if (preg_match('/^201/', $version)) {
465                 $version = 3.6;
466             } else if (preg_match('/^200[7-9]/', $version)) {
467                 $version = 3;
468             } else if (preg_match('/^2006/', $version)) {
469                 $version = 2;
470             } else {
471                 $version = 1.5;
472             }
473         }
474         if (preg_match("/(Iceweasel|Firefox)\/([0-9\.]+)/i", $useragent, $match)) {
475             // Use real Firefox version if specified in user agent string.
476             if (version_compare($match[2], $version) >= 0) {
477                 return true;
478             }
479         } else if (preg_match("/Gecko\/([0-9\.]+)/i", $useragent, $match)) {
480             // Gecko might contain date or Firefox revision, let's just guess the Firefox version from the date.
481             $browserver = $match[1];
482             if ($browserver > 20000000) {
483                 // This is just a guess, it is not supposed to be 100% accurate!
484                 if (preg_match('/^201/', $browserver)) {
485                     $browserver = 3.6;
486                 } else if (preg_match('/^200[7-9]/', $browserver)) {
487                     $browserver = 3;
488                 } else if (preg_match('/^2006/', $version)) {
489                     $browserver = 2;
490                 } else {
491                     $browserver = 1.5;
492                 }
493             }
494             if (version_compare($browserver, $version) >= 0) {
495                 return true;
496             }
497         }
498         return false;
499     }
501     /**
502      * Checks the user agent is Edge (of any version).
503      *
504      * @return bool true if Edge
505      */
506     public static function is_edge() {
507         return self::check_edge_version();
508     }
510     /**
511      * Check the User Agent for the version of Edge.
512      *
513      * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
514      * @return bool
515      */
516     public static function check_edge_version($version = null) {
517         $useragent = self::get_user_agent_string();
519         if ($useragent === false) {
520             // No User Agent found.
521             return false;
522         }
524         if (strpos($useragent, 'Edge/') === false) {
525             // Edge was not found in the UA - this is not Edge.
526             return false;
527         }
529         if (empty($version)) {
530             // No version to check.
531             return true;
532         }
534         // Find the version.
535         // Edge versions are always in the format:
536         //      Edge/<version>.<OS build number>
537         preg_match('%Edge/([\d]+)\.(.*)$%', $useragent, $matches);
539         // Just to be safe, round the version being tested.
540         // Edge only uses integer versions - the second component is the OS build number.
541         $version = round($version);
543         // Check whether the version specified is >= the version found.
544         return version_compare($matches[1], $version, '>=');
545     }
547     /**
548      * Checks the user agent is IE (of any version).
549      *
550      * @return bool true if internet exporeer
551      */
552     public static function is_ie() {
553         return self::check_ie_version();
554     }
556     /**
557      * Checks the user agent is IE and returns its main properties:
558      * - browser version;
559      * - whether running in compatibility view.
560      *
561      * @return bool|array False if not IE, otherwise an associative array of properties.
562      */
563     public static function check_ie_properties() {
564         // Internet Explorer.
565         $useragent = self::get_user_agent_string();
566         if ($useragent === false) {
567             return false;
568         }
569         if (strpos($useragent, 'Opera') !== false) {
570             // Reject Opera.
571             return false;
572         }
573         // See: http://www.useragentstring.com/pages/Internet%20Explorer/.
574         if (preg_match("/MSIE ([0-9\.]+)/", $useragent, $match)) {
575             $browser = $match[1];
576         // See: http://msdn.microsoft.com/en-us/library/ie/bg182625%28v=vs.85%29.aspx for IE11+ useragent details.
577         } else if (preg_match("/Trident\/[0-9\.]+/", $useragent) && preg_match("/rv:([0-9\.]+)/", $useragent, $match)) {
578             $browser = $match[1];
579         } else {
580             return false;
581         }
583         $compatview = false;
584         // IE8 and later versions may pretend to be IE7 for intranet sites, use Trident version instead,
585         // the Trident should always describe the capabilities of IE in any emulation mode.
586         if ($browser === '7.0' and preg_match("/Trident\/([0-9\.]+)/", $useragent, $match)) {
587             $compatview = true;
588             $browser = $match[1] + 4; // NOTE: Hopefully this will work also for future IE versions.
589         }
590         $browser = round($browser, 1);
591         return array(
592             'version'    => $browser,
593             'compatview' => $compatview
594         );
595     }
597     /**
598      * Checks the user agent is IE and that the version is equal to or greater than that specified.
599      *
600      * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
601      * @return bool
602      */
603     public static function check_ie_version($version = null) {
604         // Internet Explorer.
605         $properties = self::check_ie_properties();
606         if (!is_array($properties)) {
607             return false;
608         }
609         // In case of IE we have to deal with BC of the version parameter.
610         if (is_null($version)) {
611             $version = 5.5; // Anything older is not considered a browser at all!
612         }
613         // IE uses simple versions, let's cast it to float to simplify the logic here.
614         $version = round($version, 1);
615         return ($properties['version'] >= $version);
616     }
618     /**
619      * Checks the user agent is IE and that IE is running under Compatibility View setting.
620      *
621      * @return bool true if internet explorer runs in Compatibility View mode.
622      */
623     public static function check_ie_compatibility_view() {
624         // IE User Agent string when in Compatibility View:
625         // - IE  8: "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/4.0; ...)".
626         // - IE  9: "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/5.0; ...)".
627         // - IE 10: "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/6.0; ...)".
628         // - IE 11: "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.3; Trident/7.0; ...)".
629         // Refs:
630         // - http://blogs.msdn.com/b/ie/archive/2009/01/09/the-internet-explorer-8-user-agent-string-updated-edition.aspx.
631         // - http://blogs.msdn.com/b/ie/archive/2010/03/23/introducing-ie9-s-user-agent-string.aspx.
632         // - http://blogs.msdn.com/b/ie/archive/2011/04/15/the-ie10-user-agent-string.aspx.
633         // - http://msdn.microsoft.com/en-us/library/ie/hh869301%28v=vs.85%29.aspx.
634         $properties = self::check_ie_properties();
635         if (!is_array($properties)) {
636             return false;
637         }
638         return $properties['compatview'];
639     }
641     /**
642      * Checks the user agent is Opera (of any version).
643      *
644      * @return bool true if opera
645      */
646     public static function is_opera() {
647         return self::check_opera_version();
648     }
650     /**
651      * Checks the user agent is Opera and that the version is equal to or greater than that specified.
652      *
653      * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
654      * @return bool
655      */
656     public static function check_opera_version($version = null) {
657         // Opera.
658         $useragent = self::get_user_agent_string();
659         if ($useragent === false) {
660             return false;
661         }
662         if (strpos($useragent, 'Opera') === false) {
663             return false;
664         }
665         if (empty($version)) {
666             return true; // No version specified.
667         }
668         // Recent Opera useragents have Version/ with the actual version, e.g.:
669         // Opera/9.80 (Windows NT 6.1; WOW64; U; en) Presto/2.10.289 Version/12.01
670         // That's Opera 12.01, not 9.8.
671         if (preg_match("/Version\/([0-9\.]+)/i", $useragent, $match)) {
672             if (version_compare($match[1], $version) >= 0) {
673                 return true;
674             }
675         } else if (preg_match("/Opera\/([0-9\.]+)/i", $useragent, $match)) {
676             if (version_compare($match[1], $version) >= 0) {
677                 return true;
678             }
679         }
680         return false;
681     }
683     /**
684      * Checks the user agent is webkit based
685      *
686      * @return bool true if webkit
687      */
688     public static function is_webkit() {
689         return self::check_webkit_version();
690     }
692     /**
693      * Checks the user agent is Webkit based and that the version is equal to or greater than that specified.
694      *
695      * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
696      * @return bool
697      */
698     public static function check_webkit_version($version = null) {
699         // WebKit based browser - everything derived from it (Safari, Chrome, iOS, Android and other mobiles).
700         $useragent = self::get_user_agent_string();
701         if ($useragent === false) {
702             return false;
703         }
704         if (strpos($useragent, 'AppleWebKit') === false) {
705             return false;
706         }
707         if (empty($version)) {
708             return true; // No version specified.
709         }
710         if (preg_match("/AppleWebKit\/([0-9.]+)/i", $useragent, $match)) {
711             if (version_compare($match[1], $version) >= 0) {
712                 return true;
713             }
714         }
715         return false;
716     }
718     /**
719      * Checks the user agent is Safari
720      *
721      * @return bool true if safari
722      */
723     public static function is_safari() {
724         return self::check_safari_version();
725     }
727     /**
728      * Checks the user agent is Safari based and that the version is equal to or greater than that specified.
729      *
730      * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
731      * @return bool
732      */
733     public static function check_safari_version($version = null) {
734         // Desktop version of Apple Safari browser - no mobile or touch devices.
735         $useragent = self::get_user_agent_string();
736         if ($useragent === false) {
737             return false;
738         }
739         if (strpos($useragent, 'AppleWebKit') === false) {
740             return false;
741         }
742         // Look for AppleWebKit, excluding strings with OmniWeb, Shiira and SymbianOS and any other mobile devices.
743         if (strpos($useragent, 'OmniWeb')) {
744             // Reject OmniWeb.
745             return false;
746         }
747         if (strpos($useragent, 'Shiira')) {
748             // Reject Shiira.
749             return false;
750         }
751         if (strpos($useragent, 'SymbianOS')) {
752             // Reject SymbianOS.
753             return false;
754         }
755         if (strpos($useragent, 'Android')) {
756             // Reject Androids too.
757             return false;
758         }
759         if (strpos($useragent, 'iPhone') or strpos($useragent, 'iPad') or strpos($useragent, 'iPod')) {
760             // No Apple mobile devices here - editor does not work, course ajax is not touch compatible, etc.
761             return false;
762         }
763         if (strpos($useragent, 'Chrome')) {
764             // Reject chrome browsers - it needs to be tested explicitly.
765             // This will also reject Edge, which pretends to be both Chrome, and Safari.
766             return false;
767         }
769         if (empty($version)) {
770             return true; // No version specified.
771         }
772         if (preg_match("/AppleWebKit\/([0-9.]+)/i", $useragent, $match)) {
773             if (version_compare($match[1], $version) >= 0) {
774                 return true;
775             }
776         }
777         return false;
778     }
780     /**
781      * Checks the user agent is Chrome
782      *
783      * @return bool true if chrome
784      */
785     public static function is_chrome() {
786         return self::check_chrome_version();
787     }
789     /**
790      * Checks the user agent is Chrome based and that the version is equal to or greater than that specified.
791      *
792      * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
793      * @return bool
794      */
795     public static function check_chrome_version($version = null) {
796         // Chrome.
797         $useragent = self::get_user_agent_string();
798         if ($useragent === false) {
799             return false;
800         }
801         if (strpos($useragent, 'Chrome') === false) {
802             return false;
803         }
804         if (empty($version)) {
805             return true; // No version specified.
806         }
807         if (preg_match("/Chrome\/(.*)[ ]+/i", $useragent, $match)) {
808             if (version_compare($match[1], $version) >= 0) {
809                 return true;
810             }
811         }
812         return false;
813     }
815     /**
816      * Checks the user agent is webkit android based.
817      *
818      * @return bool true if webkit based and on Android
819      */
820     public static function is_webkit_android() {
821         return self::check_webkit_android_version();
822     }
824     /**
825      * Checks the user agent is Webkit based and on Android and that the version is equal to or greater than that specified.
826      *
827      * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
828      * @return bool
829      */
830     public static function check_webkit_android_version($version = null) {
831         // WebKit browser on Android.
832         $useragent = self::get_user_agent_string();
833         if ($useragent === false) {
834             return false;
835         }
836         if (strpos($useragent, 'Android') === false) {
837             return false;
838         }
839         if (empty($version)) {
840             return true; // No version specified.
841         }
842         if (preg_match("/AppleWebKit\/([0-9]+)/i", $useragent, $match)) {
843             if (version_compare($match[1], $version) >= 0) {
844                 return true;
845             }
846         }
847         return false;
848     }
850     /**
851      * Checks the user agent is Safari on iOS
852      *
853      * @return bool true if Safari on iOS
854      */
855     public static function is_safari_ios() {
856         return self::check_safari_ios_version();
857     }
859     /**
860      * Checks the user agent is Safari on iOS and that the version is equal to or greater than that specified.
861      *
862      * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
863      * @return bool
864      */
865     public static function check_safari_ios_version($version = null) {
866         // Safari on iPhone, iPad and iPod touch.
867         $useragent = self::get_user_agent_string();
868         if ($useragent === false) {
869             return false;
870         }
871         if (strpos($useragent, 'AppleWebKit') === false or strpos($useragent, 'Safari') === false) {
872             return false;
873         }
874         if (!strpos($useragent, 'iPhone') and !strpos($useragent, 'iPad') and !strpos($useragent, 'iPod')) {
875             return false;
876         }
877         if (empty($version)) {
878             return true; // No version specified.
879         }
880         if (preg_match("/AppleWebKit\/([0-9]+)/i", $useragent, $match)) {
881             if (version_compare($match[1], $version) >= 0) {
882                 return true;
883             }
884         }
885         return false;
886     }
888     /**
889      * Checks if the user agent is MS Word.
890      * Not perfect, as older versions of Word use standard IE6/7 user agents without any identifying traits.
891      *
892      * @return bool true if user agent could be identified as MS Word.
893      */
894     public static function is_msword() {
895         $useragent = self::get_user_agent_string();
896         if (!preg_match('/(\bWord\b|ms-office|MSOffice|Microsoft Office)/i', $useragent)) {
897             return false;
898         } else if (strpos($useragent, 'Outlook') !== false) {
899             return false;
900         } else if (strpos($useragent, 'Meridio') !== false) {
901             return false;
902         }
903         // It's Office, not Outlook and not Meridio - so it's probably Word, but we can't really be sure in most cases.
904         return true;
905     }
907     /**
908      * Check if the user agent matches a given brand.
909      *
910      * Known brand: 'Windows','Linux','Macintosh','SGI','SunOS','HP-UX'
911      *
912      * @param string $brand
913      * @return bool
914      */
915     public static function check_browser_operating_system($brand) {
916         $useragent = self::get_user_agent_string();
917         return ($useragent !== false && preg_match("/$brand/i", $useragent));
918     }
920     /**
921      * Gets an array of CSS classes to represent the user agent.
922      * @return array
923      */
924     public static function get_browser_version_classes() {
925         $classes = array();
926         if (self::is_edge()) {
927             $classes[] = 'edge';
928         } else if (self::is_ie()) {
929             $classes[] = 'ie';
930             for ($i = 12; $i >= 6; $i--) {
931                 if (self::check_ie_version($i)) {
932                     $classes[] = 'ie'.$i;
933                     break;
934                 }
935             }
936         } else if (self::is_firefox() || self::is_gecko() || self::check_camino_version()) {
937             $classes[] = 'gecko';
938             if (preg_match('/rv\:([1-2])\.([0-9])/', self::get_user_agent_string(), $matches)) {
939                 $classes[] = "gecko{$matches[1]}{$matches[2]}";
940             }
941         } else if (self::is_chrome()) {
942             $classes[] = 'chrome';
943             if (self::is_webkit_android()) {
944                 $classes[] = 'android';
945             }
946         } else if (self::is_webkit()) {
947             if (self::is_safari()) {
948                 $classes[] = 'safari';
949             }
950             if (self::is_safari_ios()) {
951                 $classes[] = 'ios';
952             } else if (self::is_webkit_android()) {
953                 $classes[] = 'android'; // Old pre-Chrome android browsers.
954             }
955         } else if (self::is_opera()) {
956             $classes[] = 'opera';
957         }
958         return $classes;
959     }
961     /**
962      * Returns true if the user agent supports the display of SVG images.
963      *
964      * @return bool
965      */
966     public static function supports_svg() {
967         // IE 5 - 8 don't support SVG at all.
968         $instance = self::instance();
969         if ($instance->supportssvg === null) {
970             if ($instance->useragent === false) {
971                 // Can't be sure, just say no.
972                 $instance->supportssvg = false;
973             } else if (self::check_ie_version('0') and !self::check_ie_version('9')) {
974                 // IE < 9 doesn't support SVG. Say no.
975                 $instance->supportssvg = false;
976             } else if (self::is_ie() and !self::check_ie_version('10') and self::check_ie_compatibility_view()) {
977                 // IE 9 Compatibility View doesn't support SVG. Say no.
978                 $instance->supportssvg = false;
979             } else if (preg_match('#Android +[0-2]\.#', $instance->useragent)) {
980                 // Android < 3 doesn't support SVG. Say no.
981                 $instance->supportssvg = false;
982             } else if (self::is_opera()) {
983                 // Opera 12 still does not support SVG well enough. Say no.
984                 $instance->supportssvg = false;
985             } else {
986                 // Presumed fine.
987                 $instance->supportssvg = true;
988             }
989         }
990         return $instance->supportssvg;
991     }
993     /**
994      * Returns true if the user agent supports the MIME media type for JSON text, as defined in RFC 4627.
995      *
996      * @return bool
997      */
998     public static function supports_json_contenttype() {
999         // Modern browsers other than IE correctly supports 'application/json' media type.
1000         if (!self::check_ie_version('0')) {
1001             return true;
1002         }
1004         // IE8+ supports 'application/json' media type, when NOT in Compatibility View mode.
1005         // Refs:
1006         // - http://blogs.msdn.com/b/ie/archive/2008/09/10/native-json-in-ie8.aspx;
1007         // - MDL-39810: issues when using 'text/plain' in Compatibility View for the body of an HTTP POST response.
1008         if (self::check_ie_version(8) && !self::check_ie_compatibility_view()) {
1009             return true;
1010         }
1012         // This browser does not support json.
1013         return false;
1014     }
1016     /**
1017      * Returns true if the client appears to be some kind of web crawler.
1018      * This may include other types of crawler.
1019      *
1020      * @return bool
1021      */
1022     public static function is_web_crawler() {
1023         $instance = self::instance();
1024         return (bool) $instance->is_useragent_web_crawler();
1025     }
1027     /**
1028      * Returns true if the client appears to be a device using iOS (iPhone, iPad, iPod).
1029      *
1030      * @param scalar $version The version if we need to find out if it is equal to or greater than that specified.
1031      * @return bool true if the client is using iOS
1032      * @since Moodle 3.2
1033      */
1034     public static function is_ios($version = null) {
1035         $useragent = self::get_user_agent_string();
1036         if ($useragent === false) {
1037             return false;
1038         }
1039         if (strpos($useragent, 'AppleWebKit') === false) {
1040             return false;
1041         }
1042         if (strpos($useragent, 'Windows')) {
1043             // Reject Windows Safari.
1044             return false;
1045         }
1046         if (strpos($useragent, 'Macintosh')) {
1047             // Reject MacOS Safari.
1048             return false;
1049         }
1050         // Look for AppleWebKit, excluding strings with OmniWeb, Shiira and SymbianOS and any other mobile devices.
1051         if (strpos($useragent, 'OmniWeb')) {
1052             // Reject OmniWeb.
1053             return false;
1054         }
1055         if (strpos($useragent, 'Shiira')) {
1056             // Reject Shiira.
1057             return false;
1058         }
1059         if (strpos($useragent, 'SymbianOS')) {
1060             // Reject SymbianOS.
1061             return false;
1062         }
1063         if (strpos($useragent, 'Android')) {
1064             // Reject Androids too.
1065             return false;
1066         }
1067         if (strpos($useragent, 'Chrome')) {
1068             // Reject chrome browsers - it needs to be tested explicitly.
1069             // This will also reject Edge, which pretends to be both Chrome, and Safari.
1070             return false;
1071         }
1073         if (empty($version)) {
1074             return true; // No version specified.
1075         }
1076         if (preg_match("/AppleWebKit\/([0-9.]+)/i", $useragent, $match)) {
1077             if (version_compare($match[1], $version) >= 0) {
1078                 return true;
1079             }
1080         }
1081         return false;
1082     }
1084     /**
1085      * Returns true if the client appears to be the Moodle app (or an app based on the Moodle app code).
1086      *
1087      * @return bool true if the client is the Moodle app
1088      * @since Moodle 3.7
1089      */
1090     public static function is_moodle_app() {
1091         $useragent = self::get_user_agent_string();
1093         // Make it case insensitive, things can change in the app or desktop app depending on the platform frameworks.
1094         if (stripos($useragent, 'MoodleMobile') !== false) {
1095             return true;
1096         }
1098         return false;
1099     }
1101     /**
1102      * Checks if current browser supports files with give extension as <video> or <audio> source
1103      *
1104      * Note, the check here is not 100% accurate!
1105      *
1106      * First, we do not know which codec is used in .mp4 or .webm files. Not all browsers support
1107      * all codecs.
1108      *
1109      * Also we assume that users of Firefox/Chrome/Safari do not use the ancient versions of browsers.
1110      *
1111      * We check the exact version for IE/Edge though. We know that there are still users of very old
1112      * versions that are afraid to upgrade or have slow IT department.
1113      *
1114      * Resources:
1115      * https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
1116      * https://en.wikipedia.org/wiki/HTML5_video
1117      * https://en.wikipedia.org/wiki/HTML5_Audio
1118      *
1119      * @param string $extension extension without leading .
1120      * @return bool
1121      */
1122     public static function supports_html5($extension) {
1123         $extension = strtolower($extension);
1125         $supportedvideo = array('m4v', 'webm', 'ogv', 'mp4', 'mov');
1126         $supportedaudio = array('ogg', 'oga', 'aac', 'm4a', 'mp3', 'wav');
1127         // TODO MDL-56549 Flac will be supported in Firefox 51 in January 2017.
1129         // Basic extension support.
1130         if (!in_array($extension, $supportedvideo) && !in_array($extension, $supportedaudio)) {
1131             return false;
1132         }
1134         // MS IE support - version 9.0 or later.
1135         if (self::is_ie() && !self::check_ie_version('9.0')) {
1136             return false;
1137         }
1139         // MS Edge support - version 12.0 for desktop and 13.0 for mobile.
1140         if (self::is_edge()) {
1141             if (!self::check_edge_version('12.0')) {
1142                 return false;
1143             }
1144             if (self::instance()->is_useragent_mobile() && !self::check_edge_version('13.0')) {
1145                 return false;
1146             }
1147         }
1149         // Different exceptions.
1151         // Webm is not supported in IE, Edge and in Safari.
1152         if ($extension === 'webm' &&
1153                 (self::is_ie() || self::is_edge() || self::is_safari() || self::is_safari_ios())) {
1154             return false;
1155         }
1156         // Ogg is not supported in IE, Edge and Safari.
1157         $isogg = in_array($extension, ['ogg', 'oga', 'ogv']);
1158         if ($isogg && (self::is_ie() || self::is_edge() || self::is_safari() || self::is_safari_ios())) {
1159             return false;
1160         }
1161         // Wave is not supported in IE.
1162         if ($extension === 'wav' && self::is_ie()) {
1163             return false;
1164         }
1165         // Aac is not supported in IE below 11.0.
1166         if ($extension === 'aac' && (self::is_ie() && !self::check_ie_version('11.0'))) {
1167             return false;
1168         }
1169         // Mpeg is not supported in IE below 10.0.
1170         $ismpeg = in_array($extension, ['m4a', 'mp3', 'm4v', 'mp4']);
1171         if ($ismpeg && (self::is_ie() && !self::check_ie_version('10.0'))) {
1172             return false;
1173         }
1174         // Mov is not supported in IE.
1175         if ($extension === 'mov' && self::is_ie()) {
1176             return false;
1177         }
1179         return true;
1180     }
1182     /**
1183      * Checks if current browser supports the HLS and MPEG-DASH media
1184      * streaming formats. Most browsers get this from Media Source Extensions.
1185      * Safari on iOS, doesn't support MPEG-DASH at all.
1186      *
1187      * Note, the check here is not 100% accurate!
1188      *
1189      * Also we assume that users of Firefox/Chrome/Safari do not use the ancient versions of browsers.
1190      * We check the exact version for IE/Edge though. We know that there are still users of very old
1191      * versions that are afraid to upgrade or have slow IT department.
1192      *
1193      * Resources:
1194      * https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API
1195      * https://caniuse.com/#search=mpeg-dash
1196      * https://caniuse.com/#search=hls
1197      *
1198      * @param string $extension
1199      * @return bool
1200      */
1201     public static function supports_media_source_extensions(string $extension) : bool {
1202         // Not supported in IE below 11.0.
1203         if (self::is_ie() && !self::check_ie_version('11.0')) {
1204             return false;
1205         }
1207         if ($extension == '.mpd') {
1208             // Not supported in Safari on iOS.
1209             if (self::is_safari_ios()) {
1210                 return false;
1211             }
1212         }
1214         return true;
1215     }