Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Added support Sec-CH-UA-Form-Factors header for ClientHints #7807

Merged
merged 11 commits into from
Sep 10, 2024
35 changes: 33 additions & 2 deletions ClientHints.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ class ClientHints
*/
protected $app = '';

/**
* Represents `Sec-CH-UA-Form-Factors` header field: form factor device type name
*
* @var array
*/
protected $formFactors = [];

/**
* Constructor
*
Expand All @@ -88,8 +95,9 @@ class ClientHints
* @param string $architecture `Sec-CH-UA-Arch` header field
* @param string $bitness `Sec-CH-UA-Bitness`
* @param string $app `HTTP_X-REQUESTED-WITH`
* @param array $formFactors `Sec-CH-UA-Form-Factors` header field
*/
public function __construct(string $model = '', string $platform = '', string $platformVersion = '', string $uaFullVersion = '', array $fullVersionList = [], bool $mobile = false, string $architecture = '', string $bitness = '', string $app = '') // phpcs:ignore Generic.Files.LineLength
public function __construct(string $model = '', string $platform = '', string $platformVersion = '', string $uaFullVersion = '', array $fullVersionList = [], bool $mobile = false, string $architecture = '', string $bitness = '', string $app = '', array $formFactors = []) // phpcs:ignore Generic.Files.LineLength
{
$this->model = $model;
$this->platform = $platform;
Expand All @@ -100,6 +108,7 @@ public function __construct(string $model = '', string $platform = '', string $p
$this->architecture = $architecture;
$this->bitness = $bitness;
$this->app = $app;
$this->formFactors = $formFactors;
}

/**
Expand Down Expand Up @@ -224,6 +233,16 @@ public function getApp(): string
return $this->app;
}

/**
* Returns the formFactor device type name
*
* @return array
*/
public function getFormFactors(): array
{
return $this->formFactors;
}

/**
* Factory method to easily instantiate this class using an array containing all available (client hint) headers
*
Expand All @@ -237,6 +256,7 @@ public static function factory(array $headers): ClientHints
$app = '';
$mobile = false;
$fullVersionList = [];
$formFactors = [];

foreach ($headers as $name => $value) {
switch (\str_replace('_', '-', \strtolower((string) $name))) {
Expand Down Expand Up @@ -319,6 +339,16 @@ public static function factory(array $headers): ClientHints
$app = $value;
}

break;
case 'formfactors':
case 'http-sec-ch-ua-form-factors':
case 'sec-ch-ua-form-factors':
if (\is_array($value)) {
$formFactors = \array_map('\strtolower', $value);
} elseif (\preg_match_all('~"([a-z]+)"~i', \strtolower($value), $matches)) {
$formFactors = $matches[1];
}

break;
}
}
Expand All @@ -332,7 +362,8 @@ public static function factory(array $headers): ClientHints
$mobile,
$architecture,
$bitness,
$app
$app,
$formFactors
);
}
}
34 changes: 32 additions & 2 deletions Parser/Device/AbstractDeviceParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -2013,6 +2013,21 @@ abstract class AbstractDeviceParser extends AbstractParser
'XX' => 'Unknown',
];

/**
* Mapping formFactor types to parser device types
* (not sort array the array priority result device type)
* @var array
*/
protected static $clientHintFormFactorsMapping = [
'automotive' => self::DEVICE_TYPE_CAR_BROWSER,
'xr' => self::DEVICE_TYPE_WEARABLE,
'eink' => self::DEVICE_TYPE_TABLET,
'watch' => self::DEVICE_TYPE_WEARABLE,
'mobile' => self::DEVICE_TYPE_SMARTPHONE,
'tablet' => self::DEVICE_TYPE_TABLET,
'desktop' => self::DEVICE_TYPE_DESKTOP,
];

/**
* Returns the device type represented by one of the DEVICE_TYPE_* constants
*
Expand Down Expand Up @@ -2239,9 +2254,24 @@ protected function buildModel(string $model, array $matches): string
*/
protected function parseClientHints(): ?array
{
if ($this->clientHints && $this->clientHints->getModel()) {
if ($this->clientHints) {
$deviceType = null;
$formFactors = $this->clientHints->getFormFactors();

if (\count($formFactors) > 0) {
foreach (self::$clientHintFormFactorsMapping as $formFactor) {
$deviceType = $formFactors[$formFactor] ?? null;

if (null !== $deviceType) {
$deviceType = self::getDeviceName($deviceType);

break;
}
}
}

return [
'deviceType' => null,
'deviceType' => $deviceType,
'model' => $this->clientHints->getModel(),
'brand' => '',
];
Expand Down
4 changes: 4 additions & 0 deletions Tests/ClientHintsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public function testHeadersHttp(): void
'HTTP_SEC_CH_UA_PLATFORM' => 'Ubuntu',
'HTTP_SEC_CH_UA_PLATFORM_VERSION' => '3.7',
'HTTP_SEC_CH_UA_FULL_VERSION' => '98.0.14335.105',
'HTTP_SEC-CH-UA-FORM-FACTORS' => '"Desktop"',
];

$ch = ClientHints::factory($headers);
Expand All @@ -59,6 +60,7 @@ public function testHeadersHttp(): void
'Opera' => '98.0.4758.82',
], $ch->getBrandList());
self::assertSame('DN2103', $ch->getModel());
self::assertEquals(['desktop'], $ch->getFormFactors());
}

public function testHeadersJavascript(): void
Expand All @@ -69,6 +71,7 @@ public function testHeadersJavascript(): void
['brand' => 'Chromium', 'version' => '99.0.4844.51'],
['brand' => 'Google Chrome', 'version' => '99.0.4844.51'],
],
'formFactors' => ['Desktop'],
'mobile' => false,
'model' => '',
'platform' => 'Windows',
Expand All @@ -85,6 +88,7 @@ public function testHeadersJavascript(): void
'Google Chrome' => '99.0.4844.51',
], $ch->getBrandList());
self::assertSame('', $ch->getModel());
self::assertEquals(['desktop'], $ch->formFactors);
}

public function testIncorrectVersionListIsDiscarded(): void
Expand Down
6 changes: 4 additions & 2 deletions Tests/DeviceDetectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,8 @@ public function testNotSkipDetectDeviceForClientHints(): void
true,
'',
'',
''
'',
[]
));

$this->assertEquals($dd->parse(), [
Expand All @@ -471,7 +472,8 @@ public function testVersionTruncationForClientHints(): void
true,
'',
'',
''
'',
[]
));
$dd->parse();
$this->assertEquals('8.0', $dd->getOs('version'));
Expand Down
Loading