Skip to content

Commit f8ebda8

Browse files
committed
Fixed bug versions::load() where if the cache folder already existed by the cache file didn't, the cache file wasn't written.
Changed default setting to not specify where the versions cache file is stored, thus version information will not be retrieved unless it is explicitly set. Updated `versions::get()` to handle when the cache file has not been specified. Fixed issue in `versions::get()` where the `currentdate` parameter was not passed to `versions::latest()`. Flattened version settings so they are not in their own sub array. Added `agentzero::getHints()` to retrieve the client hints sent by the browser. Updated `README.md` with information on how to parse client hints and add versioning information.
1 parent daa201b commit f8ebda8

File tree

8 files changed

+142
-45
lines changed

8 files changed

+142
-45
lines changed

README.md

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ The returned value will be something like:
6060
public readonly ?string 'browser' => string 'Chrome';
6161
public readonly ?string 'browserversion' => string '116.0.0.0';
6262
public readonly ?string 'browserstatus' => 'previous';
63-
public readonly ?string 'browserreleased' => '2023-09-15';
64-
public readonly ?string 'browserlatest' => '133.0.6943.54';
63+
public readonly ?string 'browserreleased' => '2023-09-15';
64+
public readonly ?string 'browserlatest' => '133.0.6943.54';
6565
public readonly ?string 'language' => string 'en-GB';
6666

6767
// app
@@ -87,6 +87,43 @@ The returned value will be something like:
8787

8888
You can read the [full list of properties here](docs/api.md).
8989

90+
### Client Hints
91+
92+
AgentZero now supports processing client hints for improved user-agent information. You must request the client hints to improve the information delivered through the user-agent string:
93+
94+
```php
95+
96+
// request client hints
97+
\header('Accept-CH: Width, ECT, Device-Memory, Sec-CH-UA-Platform-Version, Sec-CH-UA-Model, Sec-CH-UA-Full-Version-List');
98+
99+
// retrieve client hints
100+
$hints = \hexydec\agentzero\agentzero::getHints();
101+
102+
// parse
103+
$az = \hexydec\agentzero\agentzero::parse($_SERVER['HTTP_USER_AGENT'], $hints);
104+
```
105+
106+
Note that by using the `Accept-CH` header, you may receive client hints on subsequent requests, if you need the client hints on first call, use the `Critical-CH` header instead.
107+
108+
### Browser Versions
109+
110+
You can determine the date the browser was released, latest version, and status, by setting where the version file should be cached:
111+
112+
```php
113+
$config = [
114+
'versionscache' => __DIR__.'/cache/versions.json'
115+
];
116+
$az = \hexydec\agentzero\agentzero::parse($_SERVER['HTTP_USER_AGENT'], [], $config);
117+
var_dump(
118+
$ua->browserstatus, // either "canary", "beta", "latest", "previous", "legacy", legacy means released over 5 years ago
119+
$ua->browserreleased, // the date the browser was released
120+
$us->browserlatest // the latest version number of the browser
121+
);
122+
123+
```
124+
125+
The browser version information is sourced from [my browser versions project](https://github.com/hexydec/versions).
126+
90127
## Supported Features
91128

92129
AgentZero supports a wide range of architectures, browsers, rendering engines, platforms, devices, languages, and crawlers. [Access the full list on the Supported Features page](docs/support.md).

index.php

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
11
<?php
2+
3+
use hexydec\agentzero\agentzero;
4+
25
require(__DIR__.'/src/autoload.php');
36

47
// fetch UA string
58
$ua = $_POST['ua'] ?? $_SERVER['HTTP_USER_AGENT'] ?? '';
69

710
// client hints
8-
$hints = [
9-
'sec-ch-ua-mobile' => $_POST['mobile'] ?? $_SERVER['HTTP_SEC_CH_UA_MOBILE'] ?? '',
10-
'sec-ch-ua-full-version-list' => $_POST['browser'] ?? $_SERVER['HTTP_SEC_CH_UA_FULL_VERSION_LIST'] ?? '',
11-
'sec-ch-ua-platform' => $_POST['platform'] ?? $_SERVER['HTTP_SEC_CH_UA_PLATFORM'] ?? '',
12-
'sec-ch-ua-platform-version' => $_POST['platformversion'] ?? $_SERVER['HTTP_SEC_CH_UA_PLATFORM_VERSION'] ?? '',
13-
'sec-ch-ua-model' => $_POST['model'] ?? $_SERVER['HTTP_SEC_CH_UA_MODEL'] ?? '',
14-
'device-memory' => $_POST['memory'] ?? $_SERVER['HTTP_DEVICE_MEMORY'] ?? '',
15-
'width' => $_POST['width'] ?? $_SERVER['HTTP_WIDTH'] ?? '',
16-
'ect' => $_POST['ect'] ?? $_SERVER['HTTP_ECT'] ?? ''
11+
$hints = agentzero::getHints();
12+
$keys = [
13+
'sec-ch-ua-mobile',
14+
'sec-ch-ua-full-version-list',
15+
'sec-ch-ua-platform',
16+
'sec-ch-ua-platform-version',
17+
'sec-ch-ua-model',
18+
'device-memory',
19+
'width',
20+
'ect'
1721
];
22+
foreach ($keys AS $item) {
23+
if (!empty($_POST[$item])) {
24+
$hints[$item] = $_POST[$item];
25+
}
26+
}
1827
$memsizes = [
1928
'0.25' => '256Mb',
2029
'0.5' => '512Mb',
@@ -39,7 +48,7 @@
3948

4049
// timing variables
4150
$time = \microtime(true);
42-
$output = \hexydec\agentzero\agentzero::parse($ua, \array_filter($hints));
51+
$output = \hexydec\agentzero\agentzero::parse($ua, \array_filter($hints), ['versionscache' => __DIR__.'/cache/versions.json']);
4352
$total = \microtime(true) - $time;
4453
?>
4554
<!DOCTYPE html>
@@ -104,48 +113,48 @@
104113
<h3>Client Hints</h3>
105114
<div class="form__control">
106115
<label class="form__label">Mobile:</label>
107-
<select name="mobile">
116+
<select name="sec-ch-ua-mobile">
108117
<option value="">-- Select Mobile --</option>
109118
<?php foreach ($devices AS $key => $item) { ?>
110-
<option value="<?= \htmlspecialchars($key); ?>"<?= $hints['sec-ch-ua-mobile'] === $key ? ' selected="selected"' : ''; ?>><?= \htmlspecialchars($item); ?></option>
119+
<option value="<?= \htmlspecialchars($key); ?>"<?= ($hints['sec-ch-ua-mobile'] ?? null) === $key ? ' selected="selected"' : ''; ?>><?= \htmlspecialchars($item); ?></option>
111120
<?php } ?>
112121
</select>
113122
</div>
114123
<div class="form__control">
115124
<label class="form__label">Browser:</label>
116-
<input type="text" class="form__input" name="browser" value="<?= \htmlspecialchars($hints['sec-ch-ua-full-version-list']); ?>" />
125+
<input type="text" class="form__input" name="sec-ch-ua-full-version-list" value="<?= \htmlspecialchars($hints['sec-ch-ua-full-version-list'] ?? ''); ?>" />
117126
</div>
118127
<div class="form__control">
119128
<label class="form__label">Platform:</label>
120-
<input type="text" class="form__input--short" name="platform" value="<?= \htmlspecialchars($hints['sec-ch-ua-platform']); ?>" />
129+
<input type="text" class="form__input--short" name="sec-ch-ua-platform" value="<?= \htmlspecialchars($hints['sec-ch-ua-platform'] ?? ''); ?>" />
121130
</div>
122131
<div class="form__control">
123132
<label class="form__label">Platform Version:</label>
124-
<input type="text" class="form__input--short" name="platformversion" value="<?= \htmlspecialchars($hints['sec-ch-ua-platform-version']); ?>" />
133+
<input type="text" class="form__input--short" name="sec-ch-ua-platform-version" value="<?= \htmlspecialchars($hints['sec-ch-ua-platform-version'] ?? ''); ?>" />
125134
</div>
126135
<div class="form__control">
127136
<label class="form__label">Model:</label>
128-
<input type="text" class="form__input--short" name="model" value="<?= \htmlspecialchars($hints['sec-ch-ua-model']); ?>" />
137+
<input type="text" class="form__input--short" name="sec-ch-ua-model" value="<?= \htmlspecialchars($hints['sec-ch-ua-model'] ?? ''); ?>" />
129138
</div>
130139
<div class="form__control">
131140
<label class="form__label">Memory:</label>
132-
<select name="memory">
141+
<select name="device-memory">
133142
<option value="">-- Select Memory --</option>
134143
<?php foreach ($memsizes AS $key => $item) { ?>
135-
<option value="<?= \htmlspecialchars($key); ?>"<?= $hints['device-memory'] == $key ? ' selected="selected"' : ''; ?>><?= \htmlspecialchars($item); ?></option>
144+
<option value="<?= \htmlspecialchars($key); ?>"<?= ($hints['device-memory'] ?? null) == $key ? ' selected="selected"' : ''; ?>><?= \htmlspecialchars($item); ?></option>
136145
<?php } ?>
137146
</select>
138147
</div>
139148
<div class="form__control">
140149
<label class="form__label">Width:</label>
141-
<input type="number" class="form__input--short" name="width" value="<?= \htmlspecialchars($hints['width']); ?>" />
150+
<input type="number" class="form__input--short" name="width" value="<?= \htmlspecialchars($hints['width'] ?? ''); ?>" />
142151
</div>
143152
<div class="form__control">
144153
<label class="form__label">Connection:</label>
145154
<select name="ect">
146155
<option value="">-- Select Connection --</option>
147156
<?php foreach ($conns AS $key => $item) { ?>
148-
<option value="<?= \htmlspecialchars($key); ?>"<?= $hints['ect'] === $key ? ' selected="selected"' : ''; ?>><?= \htmlspecialchars($item); ?></option>
157+
<option value="<?= \htmlspecialchars($key); ?>"<?= ($hints['ect'] ?? null) === $key ? ' selected="selected"' : ''; ?>><?= \htmlspecialchars($item); ?></option>
149158
<?php } ?>
150159
</select>
151160
</div>

src/agentzero.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,32 @@ public function __get(string $key) : string|int|null {
145145
return $this->{$key} ?? null;
146146
}
147147

148+
/**
149+
* Fetch the client hints sent by the browser
150+
*
151+
* @return array<string,string> An array containing relevant client hints sent by the client
152+
*/
153+
public static function getHints() : array {
154+
$hints = [
155+
'sec-ch-ua-mobile',
156+
'sec-ch-ua-full-version-list',
157+
'sec-ch-ua-platform',
158+
'sec-ch-ua-platform-version',
159+
'sec-ch-ua-model',
160+
'device-memory',
161+
'width',
162+
'ect'
163+
];
164+
$data = [];
165+
foreach ($hints AS $item) {
166+
$upper = \strtoupper(\str_replace('-', '_', $item));
167+
if (!empty($_SERVER['HTTP_'.$upper])) {
168+
$data[$item] = $_SERVER['HTTP_'.$upper];
169+
}
170+
}
171+
return $data;
172+
}
173+
148174
/**
149175
* Extracts tokens from a UA string
150176
*

src/config.php

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,10 @@ public static function get(array $config = []) : ?array {
3030
apps::get(),
3131
frameworks::get()
3232
),
33-
'versions' => [
34-
'source' => 'https://raw.githubusercontent.com/hexydec/versions/refs/heads/main/dist/versions.json', // browser version data source
35-
'cache' => \dirname(__DIR__).'/cache/versions.json', // location of the cache file
36-
'cachelife' => 604800, // how long to cache for
37-
'currentdate' => null // the point in time to calculate the browser data from, may be in the past (DateTime object)
38-
]
33+
'versionssource' => 'https://raw.githubusercontent.com/hexydec/versions/refs/heads/main/dist/versions.json', // browser version data source
34+
'versionscache' => null, // location of the cache file, null to not fetch version data
35+
'versionscachelife' => 604800, // how long to cache for
36+
'currentdate' => null // the point in time to calculate the browser data from, may be in the past (DateTime object)
3937
];
4038
}
4139
return \array_replace_recursive($default, $config);

src/helpers/hints.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,20 @@
44

55
class hints {
66

7+
/**
8+
* Parses client hints to set agentzero properties
9+
*
10+
* @param string &$ua A reference to the User-Agent string, which may be used with brand names and versions
11+
* @param array $hints An array of client hints
12+
* @return stdClass A stdClass object containing parsed values for agentzero
13+
*/
714
public static function parse(string &$ua, array $hints) : \stdClass {
815
$map = [
916
'sec-ch-ua-mobile' => fn (\stdClass $obj, string $value) : string => $obj->category = $value === '?1' ? 'mobile' : 'desktop',
1017
'sec-ch-ua-platform' => fn (\stdClass $obj, string $value) : ?string => $obj->platform = \trim($value, '"') ?: null,
1118
'sec-ch-ua-platform-version' => function (\stdClass $obj, string $value) : void {
1219
$value = \trim($value, '"');
13-
if ($obj->platform === 'Windows') {
20+
if (($obj->platform ?? null) === 'Windows') {
1421
$map = [
1522
'8',
1623
'10.1507',

src/helpers/versions.php

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,20 @@
44

55
class versions {
66

7+
/**
8+
* @var array|false|null An array of browser version numbers
9+
*/
710
protected static array|false|null $versions = null;
811

9-
protected static function load(string $source, ?string $cache = null, ?int $life = 604800) : array|false {
12+
/**
13+
* Loads browser version information from an external source
14+
*
15+
* @param string $source The URL of the source JSON containing the version information
16+
* @param string $cache The absolute file address of the cache file
17+
* @param ?int $life The maximum life of the cache file in seconds
18+
* @return array<string,array<string,string>> An array of browser versioning information, or false if the data source not be retrieved
19+
*/
20+
protected static function load(string $source, string $cache, ?int $life = 604800) : array|false {
1021

1122
// cache for this session
1223
$data = self::$versions;
@@ -25,13 +36,11 @@ protected static function load(string $source, ?string $cache = null, ?int $life
2536
}
2637

2738
// update cache
28-
} elseif ($cache !== null) {
39+
} else {
2940

30-
// create directory
41+
// create directory and cache file
3142
$dir = \dirname($cache);
32-
if (!\is_dir($dir) && \mkdir($dir, 0755)) {
33-
34-
// cache file
43+
if (\is_dir($dir) || \mkdir($dir, 0755)) {
3544
\file_put_contents($cache, $json);
3645
}
3746
}
@@ -42,6 +51,11 @@ protected static function load(string $source, ?string $cache = null, ?int $life
4251
return $data ?? false;
4352
}
4453

54+
/**
55+
* Determines the latest version of a browser, optionally capped by the supplied date
56+
*
57+
* @param array<string,string> $versions An array of browser versions, where the key is the version number and the value is the release date (In Ymd format)
58+
*/
4559
protected static function latest(array $versions, ?\DateTime $now = null) : ?string {
4660

4761
// no date restriction
@@ -86,12 +100,15 @@ protected static function released(array $data, string $version) : ?string {
86100
}
87101

88102
public static function get(string $browser, string $version, array $config) : array {
89-
if (($versions = self::load($config['source'], $config['cache'])) !== false) {
103+
$source = $config['versionssource'];
104+
$cache = $config['versionscache'];
105+
$life = $config['versionscachelife'];
106+
if ($cache !== null && ($versions = self::load($source, $cache, $life)) !== false) {
90107
$data = [];
91108
if (isset($versions[$browser])) {
92109

93110
// get latest version of the browser
94-
$data['browserlatest'] = self::latest($versions[$browser]);
111+
$data['browserlatest'] = self::latest($versions[$browser], $config['currentdate']);
95112

96113
// check if version is greater than latest version
97114
$major = \intval($version);

src/mappings/browsers.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ protected static function getBrowser(string $value, int $i, array $tokens, strin
5656
$key = $map[$data['browser']] ?? \mb_strtolower($data['browser']);
5757
return \array_merge(
5858
$data,
59-
isset($data['browserversion']) ? versions::get($key, $data['browserversion'], $config['versions']) : []
59+
isset($data['browserversion']) ? versions::get($key, $data['browserversion'], $config) : []
6060
);
6161
}
6262
/**
@@ -103,7 +103,7 @@ public static function get() : array {
103103
'browserversion' => $version ?? null,
104104
'engine' => 'WebKit',
105105
'engineversion' => $parts[1] ?? null
106-
], $version !== null ? versions::get('safari', $version, $config['versions']) : []);
106+
], $version !== null ? versions::get('safari', $version, $config) : []);
107107
},
108108
];
109109
return [
@@ -118,7 +118,7 @@ public static function get() : array {
118118
'Maxthon ' => new props('start', fn (string $value, int $i, array $tokens, string $key, array $config = []) : array => \array_merge([
119119
'browser' => 'Maxthon',
120120
'browserversion' => \mb_substr($value, 8)
121-
], versions::get('maxathon', \mb_substr($value, 8), $config['versions']))),
121+
], versions::get('maxathon', \mb_substr($value, 8), $config))),
122122
'Konqueror/' => new props('start', $fn['browserslash']),
123123
'K-Meleon/' => new props('start', $fn['browserslash']),
124124
'UCBrowser/' => new props('start', $fn['browserslash']),
@@ -170,7 +170,7 @@ public static function get() : array {
170170
'browser' => 'Midori',
171171
'engine' => $major >= 11 ? 'Gecko' : ($major < 9 ? 'WebKit' : 'Blink'),
172172
'browserversion' => $version
173-
], $version !== null ? versions::get('midori', $version, $config['versions']) : []);
173+
], $version !== null ? versions::get('midori', $version, $config) : []);
174174
}),
175175
'PrivacyBrowser/' => new props('start', $fn['browserslash']),
176176
'Fennec/' => new props('start', $fn['gecko']),
@@ -220,19 +220,19 @@ public static function get() : array {
220220
'browser' => 'Internet Explorer',
221221
'browserversion' => \mb_substr($value, 5),
222222
'engine' => 'Trident'
223-
], versions::get('ie', \mb_substr($value, 5), $config['versions']))),
223+
], versions::get('ie', \mb_substr($value, 5), $config))),
224224
'BOIE9' => new props('start', fn (string $value, int $i, array $tokens, string $key, array $config = []) => \array_merge([
225225
'type' => 'human',
226226
'browser' => 'Internet Explorer',
227227
'browserversion' => '9.0',
228228
'engine' => 'Trident'
229-
], versions::get('ie', '9.0', $config['versions']))),
229+
], versions::get('ie', '9.0', $config))),
230230
'IEMobile/' => new props('start', fn (string $value, int $i, array $tokens, string $key, array $config = []) : array => \array_merge([
231231
'type' => 'human',
232232
'browser' => 'Internet Explorer',
233233
'browserversion' => \mb_substr($value, 9),
234234
'engine' => 'Trident'
235-
], versions::get('ie', \mb_substr($value, 9), $config['versions']))),
235+
], versions::get('ie', \mb_substr($value, 9), $config))),
236236
'Trident' => new props('start', [ // infill for missing browser name
237237
'browser' => 'Internet Explorer'
238238
]),

tests/lib.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
class lib {
66

77
public static function parse(string $ua, array $hints = []) : array {
8+
$config = [
9+
'versionscache' => \dirname(__DIR__).'/cache/versions.json'
10+
];
811
$arr = \array_filter(
9-
\array_diff_key((array) agentzero::parse($ua, $hints), ['browserstatus' => '', 'browserlatest' => '']),
12+
\array_diff_key((array) agentzero::parse($ua, $hints, $config), ['browserstatus' => '', 'browserlatest' => '']),
1013
fn (mixed $item) : mixed => $item !== null
1114
);
1215
return $arr;

0 commit comments

Comments
 (0)