From ef788c7b0ee777d966a494f290e665b79c3d81e3 Mon Sep 17 00:00:00 2001 From: Jaro Varga Date: Tue, 13 Oct 2026 08:46:50 +0200 Subject: [PATCH] Detect mobile and tablet devices from the request The detectors created MobileDetect without arguments, so it read the headers from the global $_SERVER instead of the request they were called on. Constructed requests in tests, sub-requests and headers changed by middleware were therefore ignored. Pass the request's known User-Agent, mobile and CloudFront headers to MobileDetect instead, flattening array values set via withHeader(), and fall back to an empty User-Agent so that MobileDetect does not throw. Fixes #1114 --- config/bootstrap.php | 41 +++++++--- tests/TestCase/RequestDetectorTest.php | 104 +++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 tests/TestCase/RequestDetectorTest.php 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')); + } +}