diff --git a/config/bootstrap.php b/config/bootstrap.php index e05ec9fe2..3732ec7ee 100644 --- a/config/bootstrap.php +++ b/config/bootstrap.php @@ -202,16 +202,39 @@ * If you don't use these checks you can safely remove this code * and the mobiledetect package from composer.json. */ -ServerRequest::addDetector('mobile', function ($request) { - $detector = new MobileDetect(); - - return $detector->isMobile(); -}); -ServerRequest::addDetector('tablet', function ($request) { - $detector = new MobileDetect(); +$deviceDetector = static function (ServerRequest $request): MobileDetect { + // Read the headers from the request instead of the global $_SERVER. + $detector = new MobileDetect(config: ['autoInitOfHttpHeaders' => false]); + $knownHeaders = array_merge( + $detector->getUaHttpHeaders(), + array_keys($detector->getMobileHeaders()), + $detector->getCloudFrontHttpHeaders(), + ); + $serverParams = $request->getServerParams(); + $headers = []; + foreach ($knownHeaders as $name) { + if (isset($serverParams[$name])) { + // MobileDetect expects strings, but withHeader() can store arrays. + $headers[$name] = implode(', ', (array)$serverParams[$name]); + } + } + $detector->setHttpHeaders($headers); + // MobileDetect throws when no User-Agent is set at all. + if (!$detector->hasUserAgent()) { + $detector->setUserAgent(''); + } - return $detector->isTablet(); -}); + return $detector; +}; +ServerRequest::addDetector( + 'mobile', + static fn(ServerRequest $request): bool => $deviceDetector($request)->isMobile(), +); +ServerRequest::addDetector( + 'tablet', + static fn(ServerRequest $request): bool => $deviceDetector($request)->isTablet(), +); +unset($deviceDetector); /* * You can enable default locale format parsing by adding calls diff --git a/tests/TestCase/RequestDetectorTest.php b/tests/TestCase/RequestDetectorTest.php new file mode 100644 index 000000000..9a5882c69 --- /dev/null +++ b/tests/TestCase/RequestDetectorTest.php @@ -0,0 +1,104 @@ + ['HTTP_USER_AGENT' => self::IPHONE_USER_AGENT]]); + $this->assertTrue($phone->is('mobile')); + $this->assertFalse($phone->is('tablet')); + + $tablet = new ServerRequest(['environment' => ['HTTP_USER_AGENT' => self::IPAD_USER_AGENT]]); + $this->assertTrue($tablet->is('mobile')); + $this->assertTrue($tablet->is('tablet')); + + $desktop = new ServerRequest(['environment' => ['HTTP_USER_AGENT' => self::DESKTOP_USER_AGENT]]); + $this->assertFalse($desktop->is('mobile')); + $this->assertFalse($desktop->is('tablet')); + } + + /** + * Test the detectors follow a User-Agent header changed after construction. + * + * @return void + */ + public function testDetectorsUseChangedHeader(): void + { + $request = new ServerRequest(['environment' => ['HTTP_USER_AGENT' => self::DESKTOP_USER_AGENT]]); + $request = $request->withHeader('User-Agent', self::IPHONE_USER_AGENT); + + $this->assertTrue($request->is('mobile')); + } + + /** + * Test the detectors handle array header values and non-string server params. + * + * @return void + */ + public function testDetectorsWithArrayValues(): void + { + $request = new ServerRequest(['environment' => ['argv' => ['bin/cake'], 'argc' => 1]]); + $request = $request->withHeader('User-Agent', [self::IPHONE_USER_AGENT]) + ->withHeader('Accept', ['text/html']); + + // Fail on "Array to string conversion" warnings instead of only reporting them. + set_error_handler(function (int $severity, string $message): never { + throw new ErrorException($message, 0, $severity); + }); + try { + $this->assertTrue($request->is('mobile')); + $this->assertFalse($request->is('tablet')); + } finally { + restore_error_handler(); + } + } + + /** + * Test the detectors do not fail without a User-Agent. + * + * @return void + */ + public function testDetectorsWithoutUserAgent(): void + { + $request = new ServerRequest(); + + $this->assertFalse($request->is('mobile')); + $this->assertFalse($request->is('tablet')); + } +}