Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 and 403]

   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/>.
  16  
  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   */
  24  
  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 {
  41  
  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';
  59  
  60      /**
  61       * An instance of this class.
  62       * @var core_useragent
  63       */
  64      protected static $instance = null;
  65  
  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      );
  76  
  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;
  82  
  83      /**
  84       * The users device type, one of self::DEVICETYPE_*.
  85       * @var string null until initialised
  86       */
  87      protected $devicetype = null;
  88  
  89      /**
  90       * True if the user agent supports the display of svg images. False if not.
  91       * @var bool|null Null until initialised, then true or false.
  92       */
  93      protected $supportssvg = null;
  94  
  95      /**
  96       * Get an instance of the user agent object.
  97       *
  98       * @param bool $reload If set to true the user agent will be reset and all ascertations remade.
  99       * @param string $forceuseragent The string to force as the user agent, don't use unless absolutely unavoidable.
 100       * @return core_useragent
 101       */
 102      public static function instance($reload = false, $forceuseragent = null) {
 103          if (!self::$instance || $reload) {
 104              self::$instance = new core_useragent($forceuseragent);
 105          }
 106          return self::$instance;
 107      }
 108  
 109      /**
 110       * Constructs a new user agent object. Publically you must use the instance method above.
 111       *
 112       * @param string|null $forceuseragent Optional a user agent to force.
 113       */
 114      protected function __construct($forceuseragent = null) {
 115          if ($forceuseragent !== null) {
 116              $this->useragent = $forceuseragent;
 117          } else if (!empty($_SERVER['HTTP_USER_AGENT'])) {
 118              $this->useragent = $_SERVER['HTTP_USER_AGENT'];
 119          } else {
 120              $this->useragent = false;
 121              $this->devicetype = self::DEVICETYPE_DEFAULT;
 122          }
 123      }
 124  
 125      /**
 126       * Get the MoodleBot UserAgent for this site.
 127       *
 128       * @return string UserAgent
 129       */
 130      public static function get_moodlebot_useragent() {
 131          global $CFG;
 132  
 133          $version = moodle_major_version(); // Only major version for security.
 134          return "MoodleBot/$version (+{$CFG->wwwroot})";
 135      }
 136  
 137      /**
 138       * Returns the user agent string.
 139       * @return bool|string The user agent string or false if one isn't available.
 140       */
 141      public static function get_user_agent_string() {
 142          $instance = self::instance();
 143          return $instance->useragent;
 144      }
 145  
 146      /**
 147       * Returns the device type we believe is being used.
 148       * @return string
 149       */
 150      public static function get_device_type() {
 151          $instance = self::instance();
 152          if ($instance->devicetype === null) {
 153              return $instance->guess_device_type();
 154          }
 155          return $instance->devicetype;
 156      }
 157  
 158      /**
 159       * Guesses the device type the user agent is running on.
 160       *
 161       * @return string
 162       */
 163      protected function guess_device_type() {
 164          if ($this->is_useragent_mobile()) {
 165              $this->devicetype = self::DEVICETYPE_MOBILE;
 166          } else if ($this->is_useragent_tablet()) {
 167              $this->devicetype = self::DEVICETYPE_TABLET;
 168          } else if (self::check_ie_version('0') && !self::check_ie_version('7.0')) {
 169              // IE 6 and before are considered legacy.
 170              $this->devicetype = self::DEVICETYPE_LEGACY;
 171          } else {
 172              $this->devicetype = self::DEVICETYPE_DEFAULT;
 173          }
 174          return $this->devicetype;
 175      }
 176  
 177      /**
 178       * Returns true if the user appears to be on a mobile device.
 179       * @return bool
 180       */
 181      protected function is_useragent_mobile() {
 182          // Mobile detection PHP direct copy from open source detectmobilebrowser.com.
 183          $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';
 184          $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';
 185          return (preg_match($phonesregex, $this->useragent) || preg_match($modelsregex, substr($this->useragent, 0, 4)));
 186      }
 187  
 188      /**
 189       * Returns true if the user appears to be on a tablet.
 190       *
 191       * @return int
 192       */
 193      protected function is_useragent_tablet() {
 194          $tabletregex = '/Tablet browser|android|iPad|iProd|GT-P1000|GT-I9000|SHW-M180S|SGH-T849|SCH-I800|Build\/ERE27|sholest/i';
 195          return (preg_match($tabletregex, $this->useragent));
 196      }
 197  
 198      /**
 199       * Whether the user agent relates to a web crawler.
 200       * This includes all types of web crawler.
 201       * @return bool
 202       */
 203      protected function is_useragent_web_crawler() {
 204          $regex = '/MoodleBot|Googlebot|google\.com|Yahoo! Slurp|\[ZSEBOT\]|msnbot|bingbot|BingPreview|Yandex|AltaVista'
 205                  .'|Baiduspider|Teoma/i';
 206          return (preg_match($regex, $this->useragent));
 207      }
 208  
 209      /**
 210       * Gets a list of known device types.
 211       *
 212       * @deprecated Moodle 4.3 MDL-78468 - No longer used. Please use core_useragent::devicetypes instead.
 213       * @todo Final deprecation on Moodle 4.7 MDL-79052
 214       * @param bool $includecustomtypes If set to true we'll include types that have been added by the admin.
 215       * @return array
 216       */
 217      public static function get_device_type_list($includecustomtypes = true) {
 218          debugging(
 219              __FUNCTION__ . '() is deprecated.' .
 220                  'All functions associated with devicedetectregex theme setting are being removed.
 221                  Please use core_useragent::devicetypes instead',
 222              DEBUG_DEVELOPER
 223          );
 224          $types = self::$devicetypes;
 225          if ($includecustomtypes) {
 226              $instance = self::instance();
 227              $types = array_merge($types, array_keys($instance->devicetypecustoms));
 228          }
 229          return $types;
 230      }
 231  
 232      /**
 233       * Returns the theme to use for the given device type.
 234       *
 235       * This used to be get_selected_theme_for_device_type.
 236       * @param null|string $devicetype The device type to find out for. Defaults to the device the user is using,
 237       * @deprecated since 4.3.
 238       * @return bool
 239       */
 240      public static function get_device_type_theme($devicetype = null) {
 241          debugging(
 242              __FUNCTION__ . '() is deprecated.' .
 243                  'All functions associated with device specific themes are being removed.',
 244              DEBUG_DEVELOPER
 245          );
 246          global $CFG;
 247          if ($devicetype === null) {
 248              $devicetype = self::get_device_type();
 249          }
 250          $themevarname = self::get_device_type_cfg_var_name($devicetype);
 251          if (empty($CFG->$themevarname)) {
 252              return false;
 253          }
 254          return $CFG->$themevarname;
 255      }
 256  
 257      /**
 258       * Returns the CFG var used to find the theme to use for the given device.
 259       *
 260       * Used to be get_device_cfg_var_name.
 261       *
 262       * @param null|string $devicetype The device type to find out for. Defaults to the device the user is using,
 263       * @deprecated since 4.3.
 264       * @return string
 265       */
 266      public static function get_device_type_cfg_var_name($devicetype = null) {
 267          debugging(
 268              __FUNCTION__ . '() is deprecated.' .
 269                  'All functions associated with device specific themes are being removed.',
 270              DEBUG_DEVELOPER
 271          );
 272          if ($devicetype == self::DEVICETYPE_DEFAULT || empty($devicetype)) {
 273              return 'theme';
 274          }
 275          return 'theme' . $devicetype;
 276      }
 277  
 278      /**
 279       * Gets the device type the user is currently using.
 280       * @return string
 281       */
 282      public static function get_user_device_type() {
 283          $device = self::get_device_type();
 284          $switched = get_user_preferences('switchdevice'.$device, false);
 285          if ($switched != false) {
 286              return $switched;
 287          }
 288          return $device;
 289      }
 290  
 291      /**
 292       * Switches the device type we think the user is using to what ever was given.
 293       * @param string $newdevice
 294       * @return bool
 295       * @throws coding_exception
 296       */
 297      public static function set_user_device_type($newdevice) {
 298          $devicetype = self::get_device_type();
 299          if ($newdevice == $devicetype) {
 300              unset_user_preference('switchdevice'.$devicetype);
 301              return true;
 302          } else {
 303              $devicetypes = self::$devicetypes;
 304              if (in_array($newdevice, $devicetypes)) {
 305                  set_user_preference('switchdevice'.$devicetype, $newdevice);
 306                  return true;
 307              }
 308          }
 309          throw new coding_exception('Invalid device type provided to set_user_device_type');
 310      }
 311  
 312      /**
 313       * Returns true if the user agent matches the given brand and the version is equal to or greater than that specified.
 314       *
 315       * @param string $brand The branch to check for.
 316       * @param scalar $version The version if we need to find out if it is equal to or greater than that specified.
 317       * @return bool
 318       */
 319      public static function check_browser_version($brand, $version = null) {
 320          switch ($brand) {
 321  
 322              case 'MSIE':
 323                  // Internet Explorer.
 324                  return self::check_ie_version($version);
 325  
 326              case 'Edge':
 327                  // Microsoft Edge.
 328                  return self::check_edge_version($version);
 329  
 330              case 'Firefox':
 331                  // Mozilla Firefox browsers.
 332                  return self::check_firefox_version($version);
 333  
 334              case 'Chrome':
 335                  return self::check_chrome_version($version);
 336  
 337              case 'Opera':
 338                  // Opera.
 339                  return self::check_opera_version($version);
 340  
 341              case 'Safari':
 342                  // Desktop version of Apple Safari browser - no mobile or touch devices.
 343                  return self::check_safari_version($version);
 344  
 345              case 'Safari iOS':
 346                  // Safari on iPhone, iPad and iPod touch.
 347                  return self::check_safari_ios_version($version);
 348  
 349              case 'WebKit':
 350                  // WebKit based browser - everything derived from it (Safari, Chrome, iOS, Android and other mobiles).
 351                  return self::check_webkit_version($version);
 352  
 353              case 'Gecko':
 354                  // Gecko based browsers.
 355                  return self::check_gecko_version($version);
 356  
 357              case 'WebKit Android':
 358                  // WebKit browser on Android.
 359                  return self::check_webkit_android_version($version);
 360  
 361              case 'Camino':
 362                  // OSX browser using Gecke engine.
 363                  return self::check_camino_version($version);
 364          }
 365          // Who knows?! doesn't pass anyway.
 366          return false;
 367      }
 368  
 369      /**
 370       * Checks the user agent is camino based and that the version is equal to or greater than that specified.
 371       *
 372       * Camino browser is at the end of its life, its no longer being developed or supported, just don't worry about it.
 373       *
 374       * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
 375       * @return bool
 376       */
 377      protected static function check_camino_version($version = null) {
 378          // OSX browser using Gecko engine.
 379          $useragent = self::get_user_agent_string();
 380          if ($useragent === false) {
 381              return false;
 382          }
 383          if (strpos($useragent, 'Camino') === false) {
 384              return false;
 385          }
 386          if (empty($version)) {
 387              return true; // No version specified.
 388          }
 389          if (preg_match("/Camino\/([0-9\.]+)/i", $useragent, $match)) {
 390              if (version_compare($match[1], $version) >= 0) {
 391                  return true;
 392              }
 393          }
 394          return false;
 395      }
 396  
 397      /**
 398       * Checks the user agent is Firefox (of any version).
 399       *
 400       * @return bool true if firefox
 401       */
 402      public static function is_firefox() {
 403          return self::check_firefox_version();
 404      }
 405  
 406      /**
 407       * Checks the user agent is Firefox based and that the version is equal to or greater than that specified.
 408       *
 409       * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
 410       * @return bool
 411       */
 412      public static function check_firefox_version($version = null) {
 413          // Mozilla Firefox browsers.
 414          $useragent = self::get_user_agent_string();
 415          if ($useragent === false) {
 416              return false;
 417          }
 418          if (strpos($useragent, 'Firefox') === false && strpos($useragent, 'Iceweasel') === false) {
 419              return false;
 420          }
 421          if (empty($version)) {
 422              return true; // No version specified..
 423          }
 424          if (preg_match("/(Iceweasel|Firefox)\/([0-9\.]+)/i", $useragent, $match)) {
 425              if (version_compare($match[2], $version) >= 0) {
 426                  return true;
 427              }
 428          }
 429          return false;
 430      }
 431  
 432      /**
 433       * Checks the user agent is Gecko based (of any version).
 434       *
 435       * @return bool true if Gecko based.
 436       */
 437      public static function is_gecko() {
 438          return self::check_gecko_version();
 439      }
 440  
 441      /**
 442       * Checks the user agent is Gecko based and that the version is equal to or greater than that specified.
 443       *
 444       * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
 445       * @return bool
 446       */
 447      public static function check_gecko_version($version = null) {
 448          // Gecko based browsers.
 449          // Do not look for dates any more, we expect real Firefox version here.
 450          $useragent = self::get_user_agent_string();
 451          if ($useragent === false) {
 452              return false;
 453          }
 454          if (empty($version)) {
 455              $version = 1;
 456          } else if ($version > 20000000) {
 457              // This is just a guess, it is not supposed to be 100% accurate!
 458              if (preg_match('/^201/', $version)) {
 459                  $version = 3.6;
 460              } else if (preg_match('/^200[7-9]/', $version)) {
 461                  $version = 3;
 462              } else if (preg_match('/^2006/', $version)) {
 463                  $version = 2;
 464              } else {
 465                  $version = 1.5;
 466              }
 467          }
 468          if (preg_match("/(Iceweasel|Firefox)\/([0-9\.]+)/i", $useragent, $match)) {
 469              // Use real Firefox version if specified in user agent string.
 470              if (version_compare($match[2], $version) >= 0) {
 471                  return true;
 472              }
 473          } else if (preg_match("/Gecko\/([0-9\.]+)/i", $useragent, $match)) {
 474              // Gecko might contain date or Firefox revision, let's just guess the Firefox version from the date.
 475              $browserver = $match[1];
 476              if ($browserver > 20000000) {
 477                  // This is just a guess, it is not supposed to be 100% accurate!
 478                  if (preg_match('/^201/', $browserver)) {
 479                      $browserver = 3.6;
 480                  } else if (preg_match('/^200[7-9]/', $browserver)) {
 481                      $browserver = 3;
 482                  } else if (preg_match('/^2006/', $version)) {
 483                      $browserver = 2;
 484                  } else {
 485                      $browserver = 1.5;
 486                  }
 487              }
 488              if (version_compare($browserver, $version) >= 0) {
 489                  return true;
 490              }
 491          }
 492          return false;
 493      }
 494  
 495      /**
 496       * Checks the user agent is Edge (of any version).
 497       *
 498       * @return bool true if Edge
 499       */
 500      public static function is_edge() {
 501          return self::check_edge_version();
 502      }
 503  
 504      /**
 505       * Check the User Agent for the version of Edge.
 506       *
 507       * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
 508       * @return bool
 509       */
 510      public static function check_edge_version($version = null) {
 511          $useragent = self::get_user_agent_string();
 512  
 513          if ($useragent === false) {
 514              // No User Agent found.
 515              return false;
 516          }
 517  
 518          if (strpos($useragent, 'Edge/') === false) {
 519              // Edge was not found in the UA - this is not Edge.
 520              return false;
 521          }
 522  
 523          if (empty($version)) {
 524              // No version to check.
 525              return true;
 526          }
 527  
 528          // Find the version.
 529          // Edge versions are always in the format:
 530          //      Edge/<version>.<OS build number>
 531          preg_match('%Edge/([\d]+)\.(.*)$%', $useragent, $matches);
 532  
 533          // Just to be safe, round the version being tested.
 534          // Edge only uses integer versions - the second component is the OS build number.
 535          $version = round($version);
 536  
 537          // Check whether the version specified is >= the version found.
 538          return version_compare($matches[1], $version, '>=');
 539      }
 540  
 541      /**
 542       * Checks the user agent is IE (of any version).
 543       *
 544       * @return bool true if internet exporeer
 545       */
 546      public static function is_ie() {
 547          return self::check_ie_version();
 548      }
 549  
 550      /**
 551       * Checks the user agent is IE and returns its main properties:
 552       * - browser version;
 553       * - whether running in compatibility view.
 554       *
 555       * @return bool|array False if not IE, otherwise an associative array of properties.
 556       */
 557      public static function check_ie_properties() {
 558          // Internet Explorer.
 559          $useragent = self::get_user_agent_string();
 560          if ($useragent === false) {
 561              return false;
 562          }
 563          if (strpos($useragent, 'Opera') !== false) {
 564              // Reject Opera.
 565              return false;
 566          }
 567          // See: http://www.useragentstring.com/pages/Internet%20Explorer/.
 568          if (preg_match("/MSIE ([0-9\.]+)/", $useragent, $match)) {
 569              $browser = $match[1];
 570          // See: http://msdn.microsoft.com/en-us/library/ie/bg182625%28v=vs.85%29.aspx for IE11+ useragent details.
 571          } else if (preg_match("/Trident\/[0-9\.]+/", $useragent) && preg_match("/rv:([0-9\.]+)/", $useragent, $match)) {
 572              $browser = $match[1];
 573          } else {
 574              return false;
 575          }
 576  
 577          $compatview = false;
 578          // IE8 and later versions may pretend to be IE7 for intranet sites, use Trident version instead,
 579          // the Trident should always describe the capabilities of IE in any emulation mode.
 580          if ($browser === '7.0' and preg_match("/Trident\/([0-9\.]+)/", $useragent, $match)) {
 581              $compatview = true;
 582              $browser = $match[1] + 4; // NOTE: Hopefully this will work also for future IE versions.
 583          }
 584          $browser = round($browser, 1);
 585          return array(
 586              'version'    => $browser,
 587              'compatview' => $compatview
 588          );
 589      }
 590  
 591      /**
 592       * Checks the user agent is IE and that the version is equal to or greater than that specified.
 593       *
 594       * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
 595       * @return bool
 596       */
 597      public static function check_ie_version($version = null) {
 598          // Internet Explorer.
 599          $properties = self::check_ie_properties();
 600          if (!is_array($properties)) {
 601              return false;
 602          }
 603          // In case of IE we have to deal with BC of the version parameter.
 604          if (is_null($version)) {
 605              $version = 5.5; // Anything older is not considered a browser at all!
 606          }
 607          // IE uses simple versions, let's cast it to float to simplify the logic here.
 608          $version = round($version, 1);
 609          return ($properties['version'] >= $version);
 610      }
 611  
 612      /**
 613       * Checks the user agent is IE and that IE is running under Compatibility View setting.
 614       *
 615       * @return bool true if internet explorer runs in Compatibility View mode.
 616       */
 617      public static function check_ie_compatibility_view() {
 618          // IE User Agent string when in Compatibility View:
 619          // - IE  8: "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/4.0; ...)".
 620          // - IE  9: "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/5.0; ...)".
 621          // - IE 10: "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/6.0; ...)".
 622          // - IE 11: "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.3; Trident/7.0; ...)".
 623          // Refs:
 624          // - http://blogs.msdn.com/b/ie/archive/2009/01/09/the-internet-explorer-8-user-agent-string-updated-edition.aspx.
 625          // - http://blogs.msdn.com/b/ie/archive/2010/03/23/introducing-ie9-s-user-agent-string.aspx.
 626          // - http://blogs.msdn.com/b/ie/archive/2011/04/15/the-ie10-user-agent-string.aspx.
 627          // - http://msdn.microsoft.com/en-us/library/ie/hh869301%28v=vs.85%29.aspx.
 628          $properties = self::check_ie_properties();
 629          if (!is_array($properties)) {
 630              return false;
 631          }
 632          return $properties['compatview'];
 633      }
 634  
 635      /**
 636       * Checks the user agent is Opera (of any version).
 637       *
 638       * @return bool true if opera
 639       */
 640      public static function is_opera() {
 641          return self::check_opera_version();
 642      }
 643  
 644      /**
 645       * Checks the user agent is Opera and that the version is equal to or greater than that specified.
 646       *
 647       * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
 648       * @return bool
 649       */
 650      public static function check_opera_version($version = null) {
 651          // Opera.
 652          $useragent = self::get_user_agent_string();
 653          if ($useragent === false) {
 654              return false;
 655          }
 656          if (strpos($useragent, 'Opera') === false) {
 657              return false;
 658          }
 659          if (empty($version)) {
 660              return true; // No version specified.
 661          }
 662          // Recent Opera useragents have Version/ with the actual version, e.g.:
 663          // Opera/9.80 (Windows NT 6.1; WOW64; U; en) Presto/2.10.289 Version/12.01
 664          // That's Opera 12.01, not 9.8.
 665          if (preg_match("/Version\/([0-9\.]+)/i", $useragent, $match)) {
 666              if (version_compare($match[1], $version) >= 0) {
 667                  return true;
 668              }
 669          } else if (preg_match("/Opera\/([0-9\.]+)/i", $useragent, $match)) {
 670              if (version_compare($match[1], $version) >= 0) {
 671                  return true;
 672              }
 673          }
 674          return false;
 675      }
 676  
 677      /**
 678       * Checks the user agent is webkit based
 679       *
 680       * @return bool true if webkit
 681       */
 682      public static function is_webkit() {
 683          return self::check_webkit_version();
 684      }
 685  
 686      /**
 687       * Checks the user agent is Webkit based and that the version is equal to or greater than that specified.
 688       *
 689       * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
 690       * @return bool
 691       */
 692      public static function check_webkit_version($version = null) {
 693          // WebKit based browser - everything derived from it (Safari, Chrome, iOS, Android and other mobiles).
 694          $useragent = self::get_user_agent_string();
 695          if ($useragent === false) {
 696              return false;
 697          }
 698          if (strpos($useragent, 'AppleWebKit') === false) {
 699              return false;
 700          }
 701          if (empty($version)) {
 702              return true; // No version specified.
 703          }
 704          if (preg_match("/AppleWebKit\/([0-9.]+)/i", $useragent, $match)) {
 705              if (version_compare($match[1], $version) >= 0) {
 706                  return true;
 707              }
 708          }
 709          return false;
 710      }
 711  
 712      /**
 713       * Checks the user agent is Safari
 714       *
 715       * @return bool true if safari
 716       */
 717      public static function is_safari() {
 718          return self::check_safari_version();
 719      }
 720  
 721      /**
 722       * Checks the user agent is Safari based and that the version is equal to or greater than that specified.
 723       *
 724       * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
 725       * @return bool
 726       */
 727      public static function check_safari_version($version = null) {
 728          // Desktop version of Apple Safari browser - no mobile or touch devices.
 729          $useragent = self::get_user_agent_string();
 730          if ($useragent === false) {
 731              return false;
 732          }
 733          if (strpos($useragent, 'AppleWebKit') === false) {
 734              return false;
 735          }
 736          // Look for AppleWebKit, excluding strings with OmniWeb, Shiira and SymbianOS and any other mobile devices.
 737          if (strpos($useragent, 'OmniWeb')) {
 738              // Reject OmniWeb.
 739              return false;
 740          }
 741          if (strpos($useragent, 'Shiira')) {
 742              // Reject Shiira.
 743              return false;
 744          }
 745          if (strpos($useragent, 'SymbianOS')) {
 746              // Reject SymbianOS.
 747              return false;
 748          }
 749          if (strpos($useragent, 'Android')) {
 750              // Reject Androids too.
 751              return false;
 752          }
 753          if (strpos($useragent, 'iPhone') or strpos($useragent, 'iPad') or strpos($useragent, 'iPod')) {
 754              // No Apple mobile devices here - editor does not work, course ajax is not touch compatible, etc.
 755              return false;
 756          }
 757          if (strpos($useragent, 'Chrome')) {
 758              // Reject chrome browsers - it needs to be tested explicitly.
 759              // This will also reject Edge, which pretends to be both Chrome, and Safari.
 760              return false;
 761          }
 762  
 763          if (empty($version)) {
 764              return true; // No version specified.
 765          }
 766          if (preg_match("/AppleWebKit\/([0-9.]+)/i", $useragent, $match)) {
 767              if (version_compare($match[1], $version) >= 0) {
 768                  return true;
 769              }
 770          }
 771          return false;
 772      }
 773  
 774      /**
 775       * Checks the user agent is Chrome
 776       *
 777       * @return bool true if chrome
 778       */
 779      public static function is_chrome() {
 780          return self::check_chrome_version();
 781      }
 782  
 783      /**
 784       * Checks the user agent is Chrome based and that the version is equal to or greater than that specified.
 785       *
 786       * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
 787       * @return bool
 788       */
 789      public static function check_chrome_version($version = null) {
 790          // Chrome.
 791          $useragent = self::get_user_agent_string();
 792          if ($useragent === false) {
 793              return false;
 794          }
 795          if (strpos($useragent, 'Chrome') === false) {
 796              return false;
 797          }
 798          if (empty($version)) {
 799              return true; // No version specified.
 800          }
 801          if (preg_match("/Chrome\/(.*)[ ]+/i", $useragent, $match)) {
 802              if (version_compare($match[1], $version) >= 0) {
 803                  return true;
 804              }
 805          }
 806          return false;
 807      }
 808  
 809      /**
 810       * Checks the user agent is webkit android based.
 811       *
 812       * @return bool true if webkit based and on Android
 813       */
 814      public static function is_webkit_android() {
 815          return self::check_webkit_android_version();
 816      }
 817  
 818      /**
 819       * Checks the user agent is Webkit based and on Android and that the version is equal to or greater than that specified.
 820       *
 821       * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
 822       * @return bool
 823       */
 824      public static function check_webkit_android_version($version = null) {
 825          // WebKit browser on Android.
 826          $useragent = self::get_user_agent_string();
 827          if ($useragent === false) {
 828              return false;
 829          }
 830          if (strpos($useragent, 'Android') === false) {
 831              return false;
 832          }
 833          if (empty($version)) {
 834              return true; // No version specified.
 835          }
 836          if (preg_match("/AppleWebKit\/([0-9]+)/i", $useragent, $match)) {
 837              if (version_compare($match[1], $version) >= 0) {
 838                  return true;
 839              }
 840          }
 841          return false;
 842      }
 843  
 844      /**
 845       * Checks the user agent is Safari on iOS
 846       *
 847       * @return bool true if Safari on iOS
 848       */
 849      public static function is_safari_ios() {
 850          return self::check_safari_ios_version();
 851      }
 852  
 853      /**
 854       * Checks the user agent is Safari on iOS and that the version is equal to or greater than that specified.
 855       *
 856       * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
 857       * @return bool
 858       */
 859      public static function check_safari_ios_version($version = null) {
 860          // Safari on iPhone, iPad and iPod touch.
 861          $useragent = self::get_user_agent_string();
 862          if ($useragent === false) {
 863              return false;
 864          }
 865          if (strpos($useragent, 'AppleWebKit') === false or strpos($useragent, 'Safari') === false) {
 866              return false;
 867          }
 868          if (!strpos($useragent, 'iPhone') and !strpos($useragent, 'iPad') and !strpos($useragent, 'iPod')) {
 869              return false;
 870          }
 871          if (empty($version)) {
 872              return true; // No version specified.
 873          }
 874          if (preg_match("/AppleWebKit\/([0-9]+)/i", $useragent, $match)) {
 875              if (version_compare($match[1], $version) >= 0) {
 876                  return true;
 877              }
 878          }
 879          return false;
 880      }
 881  
 882      /**
 883       * Checks if the user agent is MS Word.
 884       * Not perfect, as older versions of Word use standard IE6/7 user agents without any identifying traits.
 885       *
 886       * @return bool true if user agent could be identified as MS Word.
 887       */
 888      public static function is_msword() {
 889          $useragent = self::get_user_agent_string();
 890          if (!preg_match('/(\bWord\b|ms-office|MSOffice|Microsoft Office)/i', $useragent)) {
 891              return false;
 892          } else if (strpos($useragent, 'Outlook') !== false) {
 893              return false;
 894          } else if (strpos($useragent, 'Meridio') !== false) {
 895              return false;
 896          }
 897          // It's Office, not Outlook and not Meridio - so it's probably Word, but we can't really be sure in most cases.
 898          return true;
 899      }
 900  
 901      /**
 902       * Check if the user agent matches a given brand.
 903       *
 904       * Known brand: 'Windows','Linux','Macintosh','SGI','SunOS','HP-UX'
 905       *
 906       * @param string $brand
 907       * @return bool
 908       */
 909      public static function check_browser_operating_system($brand) {
 910          $useragent = self::get_user_agent_string();
 911          return ($useragent !== false && preg_match("/$brand/i", $useragent));
 912      }
 913  
 914      /**
 915       * Gets an array of CSS classes to represent the user agent.
 916       * @return array
 917       */
 918      public static function get_browser_version_classes() {
 919          $classes = array();
 920          if (self::is_edge()) {
 921              $classes[] = 'edge';
 922          } else if (self::is_ie()) {
 923              $classes[] = 'ie';
 924              for ($i = 12; $i >= 6; $i--) {
 925                  if (self::check_ie_version($i)) {
 926                      $classes[] = 'ie'.$i;
 927                      break;
 928                  }
 929              }
 930          } else if (self::is_firefox() || self::is_gecko() || self::check_camino_version()) {
 931              $classes[] = 'gecko';
 932              if (preg_match('/rv\:([1-2])\.([0-9])/', self::get_user_agent_string(), $matches)) {
 933                  $classes[] = "gecko{$matches[1]}{$matches[2]}";
 934              }
 935          } else if (self::is_chrome()) {
 936              $classes[] = 'chrome';
 937              if (self::is_webkit_android()) {
 938                  $classes[] = 'android';
 939              }
 940          } else if (self::is_webkit()) {
 941              if (self::is_safari()) {
 942                  $classes[] = 'safari';
 943              }
 944              if (self::is_safari_ios()) {
 945                  $classes[] = 'ios';
 946              } else if (self::is_webkit_android()) {
 947                  $classes[] = 'android'; // Old pre-Chrome android browsers.
 948              }
 949          } else if (self::is_opera()) {
 950              $classes[] = 'opera';
 951          }
 952          return $classes;
 953      }
 954  
 955      /**
 956       * Returns true if the user agent supports the display of SVG images.
 957       *
 958       * @return bool
 959       */
 960      public static function supports_svg() {
 961          $instance = self::instance();
 962          if ($instance->supportssvg === null) {
 963              if (preg_match('#Android +[0-2]\.#', $instance->useragent)) {
 964                  // Android < 3 doesn't support SVG.
 965                  $instance->supportssvg = false;
 966              } else {
 967                  // With widespread SVG support in modern browsers, default to returning true (even when useragent is false).
 968                  $instance->supportssvg = true;
 969              }
 970          }
 971          return $instance->supportssvg;
 972      }
 973  
 974      /**
 975       * Returns true if the user agent supports the MIME media type for JSON text, as defined in RFC 4627.
 976       *
 977       * @return bool
 978       */
 979      public static function supports_json_contenttype() {
 980          // Modern browsers other than IE correctly supports 'application/json' media type.
 981          if (!self::check_ie_version('0')) {
 982              return true;
 983          }
 984  
 985          // IE8+ supports 'application/json' media type, when NOT in Compatibility View mode.
 986          // Refs:
 987          // - http://blogs.msdn.com/b/ie/archive/2008/09/10/native-json-in-ie8.aspx;
 988          // - MDL-39810: issues when using 'text/plain' in Compatibility View for the body of an HTTP POST response.
 989          if (self::check_ie_version(8) && !self::check_ie_compatibility_view()) {
 990              return true;
 991          }
 992  
 993          // This browser does not support json.
 994          return false;
 995      }
 996  
 997      /**
 998       * Returns true if the client appears to be some kind of web crawler.
 999       * This may include other types of crawler.
1000       *
1001       * @return bool
1002       */
1003      public static function is_web_crawler() {
1004          $instance = self::instance();
1005          return (bool) $instance->is_useragent_web_crawler();
1006      }
1007  
1008      /**
1009       * Returns true if the client appears to be a device using iOS (iPhone, iPad, iPod).
1010       *
1011       * @param scalar $version The version if we need to find out if it is equal to or greater than that specified.
1012       * @return bool true if the client is using iOS
1013       * @since Moodle 3.2
1014       */
1015      public static function is_ios($version = null) {
1016          $useragent = self::get_user_agent_string();
1017          if ($useragent === false) {
1018              return false;
1019          }
1020          if (strpos($useragent, 'AppleWebKit') === false) {
1021              return false;
1022          }
1023          if (strpos($useragent, 'Windows')) {
1024              // Reject Windows Safari.
1025              return false;
1026          }
1027          if (strpos($useragent, 'Macintosh')) {
1028              // Reject MacOS Safari.
1029              return false;
1030          }
1031          // Look for AppleWebKit, excluding strings with OmniWeb, Shiira and SymbianOS and any other mobile devices.
1032          if (strpos($useragent, 'OmniWeb')) {
1033              // Reject OmniWeb.
1034              return false;
1035          }
1036          if (strpos($useragent, 'Shiira')) {
1037              // Reject Shiira.
1038              return false;
1039          }
1040          if (strpos($useragent, 'SymbianOS')) {
1041              // Reject SymbianOS.
1042              return false;
1043          }
1044          if (strpos($useragent, 'Android')) {
1045              // Reject Androids too.
1046              return false;
1047          }
1048          if (strpos($useragent, 'Chrome')) {
1049              // Reject chrome browsers - it needs to be tested explicitly.
1050              // This will also reject Edge, which pretends to be both Chrome, and Safari.
1051              return false;
1052          }
1053  
1054          if (empty($version)) {
1055              return true; // No version specified.
1056          }
1057          if (preg_match("/AppleWebKit\/([0-9.]+)/i", $useragent, $match)) {
1058              if (version_compare($match[1], $version) >= 0) {
1059                  return true;
1060              }
1061          }
1062          return false;
1063      }
1064  
1065      /**
1066       * Returns true if the client appears to be the Moodle app (or an app based on the Moodle app code).
1067       *
1068       * @return bool true if the client is the Moodle app
1069       * @since Moodle 3.7
1070       */
1071      public static function is_moodle_app() {
1072          $useragent = self::get_user_agent_string();
1073  
1074          // Make it case insensitive, things can change in the app or desktop app depending on the platform frameworks.
1075          if (stripos($useragent, 'MoodleMobile') !== false) {
1076              return true;
1077          }
1078  
1079          return false;
1080      }
1081  
1082      /**
1083       * Checks if current browser supports files with give extension as <video> or <audio> source
1084       *
1085       * Note, the check here is not 100% accurate!
1086       *
1087       * First, we do not know which codec is used in .mp4 or .webm files. Not all browsers support
1088       * all codecs.
1089       *
1090       * Also we assume that users of Firefox/Chrome/Safari do not use the ancient versions of browsers.
1091       *
1092       * We check the exact version for IE/Edge though. We know that there are still users of very old
1093       * versions that are afraid to upgrade or have slow IT department.
1094       *
1095       * Resources:
1096       * https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
1097       * https://en.wikipedia.org/wiki/HTML5_video
1098       * https://en.wikipedia.org/wiki/HTML5_Audio
1099       *
1100       * @param string $extension extension without leading .
1101       * @return bool
1102       */
1103      public static function supports_html5($extension) {
1104          $extension = strtolower($extension);
1105  
1106          $supportedvideo = array('m4v', 'webm', 'ogv', 'mp4', 'mov', 'fmp4');
1107          $supportedaudio = array('ogg', 'oga', 'aac', 'm4a', 'mp3', 'wav', 'flac');
1108  
1109          // Basic extension support.
1110          if (!in_array($extension, $supportedvideo) && !in_array($extension, $supportedaudio)) {
1111              return false;
1112          }
1113  
1114          // MS IE support - version 9.0 or later.
1115          if (self::is_ie() && !self::check_ie_version('9.0')) {
1116              return false;
1117          }
1118  
1119          // MS Edge support - version 12.0 for desktop and 13.0 for mobile.
1120          if (self::is_edge()) {
1121              if (!self::check_edge_version('12.0')) {
1122                  return false;
1123              }
1124              if (self::instance()->is_useragent_mobile() && !self::check_edge_version('13.0')) {
1125                  return false;
1126              }
1127          }
1128  
1129          // Different exceptions.
1130  
1131          // Webm is not supported in IE, Edge and in Safari.
1132          if ($extension === 'webm' &&
1133                  (self::is_ie() || self::is_edge() || self::is_safari() || self::is_safari_ios())) {
1134              return false;
1135          }
1136          // Ogg is not supported in IE, Edge and Safari.
1137          $isogg = in_array($extension, ['ogg', 'oga', 'ogv']);
1138          if ($isogg && (self::is_ie() || self::is_edge() || self::is_safari() || self::is_safari_ios())) {
1139              return false;
1140          }
1141          // FLAC is not supported in IE and Edge (below 16.0).
1142          if ($extension === 'flac' &&
1143                  (self::is_ie() || (self::is_edge() && !self::check_edge_version('16.0')))) {
1144              return false;
1145          }
1146          // Wave is not supported in IE.
1147          if ($extension === 'wav' && self::is_ie()) {
1148              return false;
1149          }
1150          // Aac is not supported in IE below 11.0.
1151          if ($extension === 'aac' && (self::is_ie() && !self::check_ie_version('11.0'))) {
1152              return false;
1153          }
1154          // Mpeg is not supported in IE below 10.0.
1155          $ismpeg = in_array($extension, ['m4a', 'mp3', 'm4v', 'mp4', 'fmp4']);
1156          if ($ismpeg && (self::is_ie() && !self::check_ie_version('10.0'))) {
1157              return false;
1158          }
1159          // Mov is not supported in IE.
1160          if ($extension === 'mov' && self::is_ie()) {
1161              return false;
1162          }
1163  
1164          return true;
1165      }
1166  
1167      /**
1168       * Checks if current browser supports the HLS and MPEG-DASH media
1169       * streaming formats. Most browsers get this from Media Source Extensions.
1170       * Safari on iOS, doesn't support MPEG-DASH at all.
1171       *
1172       * Note, the check here is not 100% accurate!
1173       *
1174       * Also we assume that users of Firefox/Chrome/Safari do not use the ancient versions of browsers.
1175       * We check the exact version for IE/Edge though. We know that there are still users of very old
1176       * versions that are afraid to upgrade or have slow IT department.
1177       *
1178       * Resources:
1179       * https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API
1180       * https://caniuse.com/#search=mpeg-dash
1181       * https://caniuse.com/#search=hls
1182       *
1183       * @param string $extension
1184       * @return bool
1185       */
1186      public static function supports_media_source_extensions(string $extension) : bool {
1187          // Not supported in IE below 11.0.
1188          if (self::is_ie() && !self::check_ie_version('11.0')) {
1189              return false;
1190          }
1191  
1192          if ($extension == '.mpd') {
1193              // Not supported in Safari on iOS.
1194              if (self::is_safari_ios()) {
1195                  return false;
1196              }
1197          }
1198  
1199          return true;
1200      }
1201  }