SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Helpers/UserAgent.php

  1. <?php
  2. /*
  3.  * Copyright (C) 2017 Karmabunny Pty Ltd.
  4.  *
  5.  * This file is a part of SproutCMS.
  6.  *
  7.  * SproutCMS is free software: you can redistribute it and/or modify it under the terms
  8.  * of the GNU General Public License as published by the Free Software Foundation, either
  9.  * version 2 of the License, or (at your option) any later version.
  10.  *
  11.  * For more information, visit <http://getsproutcms.com>.
  12.  */
  13.  
  14. namespace Sprout\Helpers;
  15.  
  16. use Exception;
  17.  
  18.  
  19. /**
  20.  * Implementation of parser for user-agents.json file
  21.  */
  22. class UserAgent {
  23. const MAX_AGE_HOURS = 6;
  24.  
  25. private static $ua;
  26. private static $info;
  27.  
  28. private $rules;
  29.  
  30.  
  31. public static function init()
  32. {
  33. if (self::$ua) return;
  34.  
  35. $rules_file = APPPATH . 'cache/user-agents.json';
  36.  
  37. $mtime = @filemtime($rules_file);
  38. $age = time() - $mtime;
  39.  
  40. if ($age > 3600 * self::MAX_AGE_HOURS) {
  41. try {
  42. $new_rules = HttpReq::get('https://raw.githubusercontent.com/Karmabunny/user-agents.json/master/data/user-agents.json');
  43. if ($new_rules and HttpReq::getLastreqStatus() == '200') {
  44. file_put_contents($rules_file, $new_rules);
  45. }
  46. } catch (Exception $ex) {
  47. // In case of e.g. DNS resolution failure, retain the old file
  48. }
  49. }
  50.  
  51. self::$ua = new UserAgent($rules_file);
  52. self::$info = self::$ua->getAgentInfo($_SERVER['HTTP_USER_AGENT']);
  53. }
  54.  
  55.  
  56. public static function getInfo() {
  57. self::init();
  58. return self::$info;
  59. }
  60.  
  61.  
  62. public static function getDeviceCategory() {
  63. self::init();
  64. return self::$info['device_category'];
  65. }
  66.  
  67.  
  68. public static function getBodyClasses() {
  69. self::init();
  70. return 'dc-' . self::getDeviceCategory();
  71. }
  72.  
  73.  
  74. /**
  75.   * Load the rules file
  76.   * @param string $filename Filename of the rules file to load
  77.   * @throws Exception if JSON file missing or invalid
  78.   **/
  79. public function __construct($filename)
  80. {
  81. $json = @file_get_contents($filename);
  82. if (!$json) throw new Exception('Rules file missing or empty');
  83.  
  84. $json = @json_decode($json);
  85. if (!$json) throw new Exception('Rules file not valid JSON');
  86.  
  87. $this->rules = $json;
  88. }
  89.  
  90.  
  91. /**
  92.   * Return info about a given user-agent
  93.   * @return array Keys will only exist if values determined. Possible keys are:
  94.   * 'os_name', 'os_version', 'os_title',
  95.   * 'browser_name', 'browser_version',
  96.   * 'device_category'
  97.   */
  98. public function getAgentInfo($ua)
  99. {
  100. // Extra space makes the regexes much simpler
  101. $res = $this->process(' ' . $ua . ' ', $this->rules);
  102.  
  103. // Convert to readable format
  104. $out = [];
  105. $mappings = [
  106. 'on' => 'os_name',
  107. 'ov' => 'os_version',
  108. 'ot' => 'os_title',
  109. 'bn' => 'browser_name',
  110. 'bv' => 'browser_version',
  111. 'dc' => 'device_category',
  112. ];
  113. foreach ($mappings as $old => $new) {
  114. if (isset($res[$old])) $out[$new] = $res[$old];
  115. }
  116. return $out;
  117. }
  118.  
  119.  
  120. /**
  121.   * Internal recursive processing method
  122.   * @param string $ua User agent
  123.   * @param array $rules Rules extracted from JSON file
  124.   * @return array Keys will only exist if values determined. Possible keys are:
  125.   * 'on', 'ov', 'ot', 'bn', 'bv', 'dc'
  126.   */
  127. private function process($ua, array $rules)
  128. {
  129. $out = [];
  130. foreach ($rules as $obj) {
  131. if (! preg_match($obj->regex, $ua, $matches)) continue;
  132.  
  133. if (isset($obj->on)) $out['on'] = $this->pregInject($obj->on, $matches);
  134. if (isset($obj->ov)) $out['ov'] = $this->pregInject($obj->ov, $matches);
  135. if (isset($obj->ot)) $out['ot'] = $this->pregInject($obj->ot, $matches);
  136. if (isset($obj->bn)) $out['bn'] = $this->pregInject($obj->bn, $matches);
  137. if (isset($obj->bv)) $out['bv'] = $this->pregInject($obj->bv, $matches);
  138. if (isset($obj->dc)) $out['dc'] = $this->pregInject($obj->dc, $matches);
  139.  
  140. if (isset($obj->rules)) {
  141. $sub = $this->process($ua, $obj->rules);
  142. foreach ($sub as $key => $val) {
  143. $out[$key] = $val;
  144. }
  145. }
  146. }
  147.  
  148. foreach ($out as $key => $val) {
  149. if (!$val) unset($out[$key]);
  150. }
  151.  
  152. return $out;
  153. }
  154.  
  155.  
  156. /**
  157.   * Replace values in a way similar to preg_replace
  158.   */
  159. function pregInject($text, $matches)
  160. {
  161. foreach ($matches as $idx => $str) {
  162. $text = str_replace('$' . $idx , $str, $text);
  163. }
  164. return $text;
  165. }
  166. }
  167.  
  168.  
  169.