PageRenderTime 51ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/inc/BrowserInfo.php

http://github.com/jquery/testswarm
PHP | 369 lines | 213 code | 41 blank | 115 comment | 24 complexity | cbd2d855d9e07ca5c3413cff33b8a019 MD5 | raw file
  1. <?php
  2. /**
  3. * Extract information from a user agent string.
  4. *
  5. * @author Timo Tijhof
  6. * @since 1.0.0
  7. * @package TestSwarm
  8. */
  9. class BrowserInfo {
  10. /**
  11. * @var TestSwarmContext
  12. */
  13. private $context;
  14. /**
  15. * @var string
  16. */
  17. protected $rawUserAgent = '';
  18. /**
  19. * @var stdClass: Object returned by UA::parse.
  20. */
  21. protected $uaData;
  22. /**
  23. * @var stdClass
  24. */
  25. protected $swarmUaItem;
  26. /**
  27. * @var stdClass: Cache for getBrowserIndex()
  28. */
  29. protected static $browserIndex;
  30. /** @return object */
  31. public static function getBrowserIndex() {
  32. // Lazy-init and cache
  33. if ( self::$browserIndex === null ) {
  34. global $swarmInstallDir, $swarmContext;
  35. // Convert from array with string values
  36. // to an object with boolean values
  37. $browserIndex = new stdClass();
  38. $userAgents = $swarmContext->getConf()->userAgents;
  39. foreach ( $userAgents as $uaID => $uaData ) {
  40. $keys = array_keys(get_object_vars(
  41. $swarmContext->getBrowserInfo()->getUaData()
  42. ));
  43. $data = new stdClass();
  44. // Filter out unwanted properties, and set missing properties.
  45. foreach ( $keys as $key ) {
  46. $data->$key = isset( $uaData->$key ) ? $uaData->$key : '';
  47. }
  48. $data->displayInfo = self::getDisplayInfo( $data );
  49. $browserIndex->$uaID = $data;
  50. }
  51. self::$browserIndex = $browserIndex;
  52. }
  53. return self::$browserIndex;
  54. }
  55. /**
  56. * @param TestSwarmContext $context
  57. * @param string $userAgent
  58. * @return BrowserInfo
  59. */
  60. public static function newFromContext( TestSwarmContext $context, $userAgent ) {
  61. $bi = new self();
  62. $bi->context = $context;
  63. $bi->parseUserAgent( $userAgent );
  64. return $bi;
  65. }
  66. /**
  67. * Utility to build a blank uaData object. Used when dealing with outdated
  68. * or malformed uaIDs.
  69. */
  70. public static function makeGenericUaData( $id = '-' ) {
  71. $uaData = new stdClass();
  72. $uaData->browserFamily =
  73. $uaData->browserMajor =
  74. $uaData->browserMinor =
  75. $uaData->browserPatch =
  76. $uaData->osFamily =
  77. $uaData->osMajor =
  78. $uaData->osMinor =
  79. $uaData->osPatch =
  80. $uaData->deviceFamily =
  81. $uaData->deviceMajor =
  82. $uaData->deviceMinor = '';
  83. $uaData->displayInfo = self::getDisplayInfo( $uaData );
  84. $uaData->displayInfo['title'] = "[ $id ]";
  85. return $uaData;
  86. }
  87. /**
  88. * Callback for `uasort()`.
  89. *
  90. * @param Array|stdClass $a UA data, as returned by #getUaData and #makeGenericUaData.
  91. * @param Array|stdClass $b UA data.
  92. * @return int Like other PHP comparison functions,
  93. * returns -1 if A is less than B, +1 if A is greater than B, 0 if they are equal.
  94. */
  95. public static function sortUaData( $a, $b ) {
  96. $a = is_array( $a ) ? (object)$a : $a;
  97. $b = is_array( $b ) ? (object)$b : $b;
  98. return strnatcasecmp( $a->displayInfo['title'], $b->displayInfo['title'] );
  99. }
  100. public function sortUaId( $a, $b ) {
  101. $browserIndex = $this->getBrowserIndex();
  102. return self::sortUaData(
  103. isset( $browserIndex->$a ) ? $browserIndex->$a : self::makeGenericUaData( $a ),
  104. isset( $browserIndex->$b ) ? $browserIndex->$b : self::makeGenericUaData( $a )
  105. );
  106. }
  107. /**
  108. * Create a new BrowserInfo object for the given user agent string.
  109. * Instances may not be created directly, use the static newFromContext method instead.
  110. *
  111. * @param string $userAgent
  112. */
  113. protected function parseUserAgent( $userAgent ) {
  114. /**
  115. * A ua-parser object looks like this (simplified version of the actual object)
  116. * @source https://github.com/tobie/ua-parser
  117. *
  118. * ua->family: Chrome
  119. * ua->major: 24
  120. * ua->minor: 0
  121. * ua->patch: 1312
  122. * os->family: Mac OS X
  123. * os->major: 10
  124. * os->minor: 8
  125. * os->patch: 2
  126. * device->family:
  127. * toFullString: Chrome 24.0.1312/Mac OS X 10.8.2
  128. */
  129. $parser = UAParser\Parser::create();
  130. $parsed = $parser->parse( $userAgent );
  131. $uaData = new stdClass();
  132. $uaData->browserFamily = $parsed->ua->family;
  133. $uaData->browserMajor = $parsed->ua->major;
  134. $uaData->browserMinor = $parsed->ua->minor;
  135. $uaData->browserPatch = $parsed->ua->patch;
  136. $uaData->osFamily = $parsed->os->family;
  137. $uaData->osMajor = $parsed->os->major;
  138. $uaData->osMinor = $parsed->os->minor;
  139. $uaData->osPatch = $parsed->os->patch;
  140. $uaData->deviceFamily = $parsed->device->family;
  141. $uaData->deviceMajor = null; // deprecated
  142. $uaData->deviceMinor = null; // deprecated
  143. $uaData->displayInfo = self::getDisplayInfo( $uaData );
  144. $this->rawUserAgent = $userAgent;
  145. $this->uaData = $uaData;
  146. return $this;
  147. }
  148. /**
  149. * @param array|object $uaData
  150. * @param string $prefix: Prefix for CSS classes.
  151. * @return array
  152. */
  153. protected static function getDisplayInfo( $uaData, $prefix = 'swarm-' ) {
  154. $uaData = (object) $uaData;
  155. $classes = array();
  156. $classes[] = $prefix . 'icon';
  157. if ( $uaData->browserFamily ) {
  158. $classes[] = $prefix . 'browser';
  159. $browserFamily = strtolower( str_replace( ' ', '_', $uaData->browserFamily ) );
  160. $classes[] = $prefix . 'browser-' . $browserFamily;
  161. if ( $uaData->browserMajor ) {
  162. $classes[] = $prefix . 'browser-' . $browserFamily . '-' . intval( $uaData->browserMajor );
  163. }
  164. }
  165. if ( $uaData->osFamily ) {
  166. $classes[] = $prefix . 'os';
  167. $osFamily = strtolower( str_replace( ' ', '_', $uaData->osFamily ) );
  168. $classes[] = $prefix . 'os-' . $osFamily;
  169. if ( $uaData->osMajor ) {
  170. $classes[] = $prefix . 'os-' . $osFamily . '-' . intval( $uaData->osMajor );
  171. }
  172. }
  173. if ( $uaData->deviceFamily && $uaData->deviceFamily !== 'Other' ) {
  174. $classes[] = $prefix . 'device';
  175. $deviceFamily = strtolower( str_replace( ' ', '_', $uaData->deviceFamily ) );
  176. $classes[] = $prefix . 'device-' . $deviceFamily;
  177. if ( $uaData->deviceMajor ) {
  178. $classes[] = $prefix . 'device-' . $deviceFamily . '-' . intval( $uaData->deviceMajor );
  179. }
  180. }
  181. $title = array();
  182. // "Smart" way of concatenating the parts, and trimming off empty parts
  183. // (Trim trailing dots or spaces indicate two adjacent empty parts).
  184. // Also remove the wildcard from the interface label (only relevant to the backend)
  185. if ( $uaData->browserFamily ) {
  186. $title[] = rtrim("$uaData->browserFamily $uaData->browserMajor.$uaData->browserMinor.$uaData->browserPatch", '. *');
  187. }
  188. if ( $uaData->osFamily ) {
  189. $title[] = rtrim("$uaData->osFamily $uaData->osMajor.$uaData->osMinor.$uaData->osPatch", '. *');
  190. }
  191. if ( $uaData->deviceFamily && $uaData->deviceFamily !== 'Other' ) {
  192. $title[] = rtrim("$uaData->deviceFamily $uaData->deviceMajor.$uaData->deviceMinor", '. *');
  193. }
  194. return array(
  195. 'class' => implode( ' ', $classes ),
  196. 'title' => implode( '/', $title ),
  197. 'labelText' => implode( "\n", $title ),
  198. 'labelHtml' => implode( '<br/>', array_map( 'htmlspecialchars', $title ) ),
  199. );
  200. }
  201. /** @return string */
  202. public function getRawUA() {
  203. return $this->rawUserAgent;
  204. }
  205. /** @return object */
  206. public function getUaData() {
  207. return $this->uaData;
  208. }
  209. /** @return string: HTML */
  210. public function getIconHtml() {
  211. return self::buildIconHtml( $this->getUaData()->displayInfo );
  212. }
  213. /** @return string: HTML */
  214. public static function buildIconHtml( Array $displayInfo, Array $options = null ) {
  215. $classes = '';
  216. $afterHtml = '';
  217. if ( isset( $options['size'] ) && $options['size'] === 'small' ) {
  218. $classes .= ' swarm-icon-small';
  219. }
  220. $labelHtml = isset( $options['label'] ) ? $options['label'] : $displayInfo['labelHtml'];
  221. if ( $labelHtml ) {
  222. $afterHtml .= '<br>'
  223. . html_tag_open( 'span', array(
  224. 'class' => 'badge swarm-browsername',
  225. ) ) . $labelHtml . '</span>';
  226. }
  227. if ( isset( $options['wrap'] ) && !$options['wrap'] ) {
  228. return html_tag( 'div', array(
  229. 'class' => $displayInfo['class'] . $classes,
  230. 'title' => $displayInfo['title'],
  231. ) );
  232. }
  233. return ''
  234. . html_tag_open( 'div', array( 'class' => 'well well-swarm-icon' ) )
  235. . html_tag( 'div', array(
  236. 'class' => $displayInfo['class'] . $classes,
  237. 'title' => $displayInfo['title'],
  238. ) )
  239. . $afterHtml
  240. . '</div>';
  241. }
  242. /**
  243. * Process the wildcard syntax allowed at the end
  244. * of uaData property values.
  245. * This was originally created to handle the different
  246. * pseudo-patch releases from Opera. Opera 11.62 for instance
  247. * some people want to treat it like 11.6.2 because BrowserStack
  248. * has 11.60 and 11.62 mixed up under the id "11.6". So we can
  249. * use "browserMinor: 6*" in the userAgents configuration,
  250. * which will tolerate anything. Use carefully though,
  251. * theoretically this means it will match X.6, X.60 and X.600,
  252. * X.6foo, X.61-alpha etc.
  253. * NB: Wildcards are only allowed at the end of values. And because
  254. * it doesn't make sense to have more than one in that case, it
  255. * only looks for one.
  256. * NB: Pass the objects as copied arrays to this function, they will
  257. * be mutated otherwise.
  258. *
  259. * @param Array $uaData: browserSet configuration item
  260. * @param Array $myUaData: parsed ua-browser object
  261. * @return number|bool: If they match, how precise it is (higher is better),
  262. * or boolean false.
  263. */
  264. private function compareUaData( Array $uaData, Array $myUaData ) {
  265. unset( $uaData['displayInfo'], $myUaData['displayInfo'] );
  266. foreach ( $uaData as $key => $value ) {
  267. if ( preg_match( '/(Major|Minor|Patch)$/', $key ) && substr( $value, -1 ) === '*' ) {
  268. $uaData[$key] = substr( $value, 0, -1 );
  269. // Shorten myUaData's value to just before the
  270. // position of the wildcard in uaData's value.
  271. $myUaData[$key] = substr( $myUaData[$key], 0, strlen( $uaData[$key] ) );
  272. }
  273. // Android <= 4.3 uses "Android" browser. Android 4.4+ uses "Chrome Mobile".
  274. // Except on tablets, which omit "Mobile" from the User-Agent string, which confuses
  275. // ua-parser into parsing it as plain "Chrome". Adjust our search criteria to also
  276. // accept "Chrome" on "Android" when the requirement was "Chrome Mobile".
  277. // https://github.com/jquery/testswarm/issues/306
  278. if ( $key === 'browserFamily' &&
  279. $value === 'Chrome Mobile' &&
  280. $myUaData['osFamily'] === 'Android' &&
  281. $myUaData['browserFamily'] === 'Chrome'
  282. ) {
  283. $uaData[$key] = 'Chrome';
  284. }
  285. }
  286. $diff = array_diff_assoc( $uaData, $myUaData );
  287. $precision = count( $uaData ) - count( array_values( $diff ) );
  288. if ( implode( '', array_values( $diff ) ) === '' ) {
  289. return $precision;
  290. }
  291. return false;
  292. }
  293. /**
  294. * Find the uaID in browserIndex that best matches the current
  295. * user-agent and return the uaData from the browser index.
  296. * @return object: Object from browserindex (with additional 'id' property).
  297. */
  298. public function getSwarmUaItem() {
  299. // Lazy-init and cache
  300. if ( $this->swarmUaItem === null ) {
  301. $browserIndex = self::getBrowserIndex();
  302. $myUaData = $this->getUaData();
  303. $foundPrecision = 0;
  304. $found = false;
  305. foreach ( $browserIndex as $uaID => $uaData ) {
  306. $precision = $this->compareUaData( (array)$uaData, (array)$myUaData );
  307. if ( $precision !== false && $precision > $foundPrecision ) {
  308. $found = $uaData;
  309. $found->id = $uaID;
  310. $foundPrecision = $precision;
  311. }
  312. }
  313. $this->swarmUaItem = $found;
  314. }
  315. return $this->swarmUaItem;
  316. }
  317. /** @return bool */
  318. public function isInSwarmUaIndex() {
  319. return (bool)$this->getSwarmUaItem();
  320. }
  321. /** @return string|null */
  322. public function getSwarmUaID() {
  323. $uaData = $this->getSwarmUaItem();
  324. return $uaData ? $uaData->id : null;
  325. }
  326. /** Don't allow direct instantiations of this class, use newFromContext instead */
  327. private function __construct() {}
  328. }