From 6c41a51258fb8de9145f68136bdb211f626dcf57 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Sun, 21 Dec 2025 01:31:05 +0330 Subject: [PATCH 01/14] feat: skip HTML/JS injection for partial requests --- app/Config/Toolbar.php | 36 ++++++++++++++++++++++++++++++++++++ system/Debug/Toolbar.php | 27 +++++++++++++++++++-------- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/app/Config/Toolbar.php b/app/Config/Toolbar.php index 5a3e5045d1e2..f79b51aac64b 100644 --- a/app/Config/Toolbar.php +++ b/app/Config/Toolbar.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace Config; use CodeIgniter\Config\BaseConfig; @@ -119,4 +128,31 @@ class Toolbar extends BaseConfig public array $watchedExtensions = [ 'php', 'css', 'js', 'html', 'svg', 'json', 'env', ]; + + /** + * -------------------------------------------------------------------------- + * Ignored HTTP Headers + * -------------------------------------------------------------------------- + * + * CodeIgniter Debug Toolbar normally injects HTML and JavaScript into every + * HTML response. This is correct for full page loads, but it breaks requests + * that expect only a clean HTML fragment. + * + * Libraries like HTMX, Unpoly, and Hotwire (Turbo) update parts of the page or + * manage navigation on the client side. Injecting the Debug Toolbar into their + * responses can cause invalid HTML, duplicated scripts, or JavaScript errors + * (such as infinite loops or "Maximum call stack size exceeded"). + * + * Any request containing one of the following headers is treated as a + * client-managed or partial request, and the Debug Toolbar injection is skipped.s + * + * @var list + */ + public array $disableOnHeaders = [ + 'HX-Request', // HTMX partial requests + 'HX-Boosted', // HTMX boosted navigation + 'X-Unpoly-Request', // Unpoly partial requests + 'Turbo-Frame', // Turbo Frames + 'Turbo-Visit', // Turbo Drive navigation + ]; } diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index 7900c7c780c5..fac06e42ec70 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -42,6 +42,12 @@ class Toolbar */ protected $config; + /** + * Indicates if the current request is a custom AJAX-like request + * (HTMX, Unpoly, Turbo, etc.) that expects clean HTML fragments. + */ + protected bool $isCustomAjax = false; + /** * Collectors to be used and displayed. * @@ -365,10 +371,8 @@ protected function roundTo(float $number, int $increments = 5): float /** * Prepare for debugging. - * - * @return void */ - public function prepare(?RequestInterface $request = null, ?ResponseInterface $response = null) + public function prepare(?RequestInterface $request = null, ?ResponseInterface $response = null): void { /** * @var IncomingRequest|null $request @@ -385,7 +389,9 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r return; } - $toolbar = service('toolbar', config(ToolbarConfig::class)); + $config = config(ToolbarConfig::class); + + $toolbar = service('toolbar', $config); $stats = $app->getPerformanceStats(); $data = $toolbar->run( $stats['startTime'], @@ -407,10 +413,17 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r $format = $response->getHeaderLine('content-type'); + foreach ($config->disableOnHeaders as $header) { + if ($request->hasHeader($header)) { + $this->isCustomAjax = true; + break; + } + } + // Non-HTML formats should not include the debugbar // then we send headers saying where to find the debug data // for this response - if ($request->isAJAX() || ! str_contains($format, 'html')) { + if ($request->isAJAX() || ! str_contains($format, 'html') || $this->isCustomAjax) { $response->setHeader('Debugbar-Time', "{$time}") ->setHeader('Debugbar-Link', site_url("?debugbar_time={$time}")); @@ -454,10 +467,8 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r * Inject debug toolbar into the response. * * @codeCoverageIgnore - * - * @return void */ - public function respond() + public function respond(): void { if (ENVIRONMENT === 'testing') { return; From 18d0d49f32c41633f8d06b7de5377cb32f18b9f8 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Sun, 21 Dec 2025 01:33:19 +0330 Subject: [PATCH 02/14] tests: add test for skip html/js injection in development mode --- tests/system/Debug/ToolbarTest.php | 100 +++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 tests/system/Debug/ToolbarTest.php diff --git a/tests/system/Debug/ToolbarTest.php b/tests/system/Debug/ToolbarTest.php new file mode 100644 index 000000000000..8456bd934b3f --- /dev/null +++ b/tests/system/Debug/ToolbarTest.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Debug; + +use CodeIgniter\CodeIgniter; +use CodeIgniter\Config\Factories; +use CodeIgniter\Config\Services; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\ResponseInterface; +use CodeIgniter\Test\CIUnitTestCase; +use Config\Toolbar as ToolbarConfig; +use PHPUnit\Framework\Attributes\BackupGlobals; +use PHPUnit\Framework\Attributes\Group; + +/** + * @internal + */ +#[BackupGlobals(true)] +#[Group('Others')] +final class ToolbarTest extends CIUnitTestCase +{ + private ToolbarConfig $config; + private ?IncomingRequest $request = null; + private ?ResponseInterface $response = null; + + protected function setUp(): void + { + parent::setUp(); + Services::reset(); + + $this->config = new ToolbarConfig(); + + // Mock CodeIgniter core service to provide performance stats + $app = $this->createMock(CodeIgniter::class); + $app->method('getPerformanceStats')->willReturn([ + 'startTime' => microtime(true), + 'totalTime' => 0.05, + ]); + Services::injectMock('codeigniter', $app); + } + + public function testPrepareRespectsDisableOnHeaders(): void + { + // Set up the new configuration property + $this->config->disableOnHeaders = ['HX-Request']; + Factories::injectMock('config', 'Toolbar', $this->config); + + // Initialize Request with the custom header + $this->request = service('incomingrequest', null, false); + $this->request->setHeader('HX-Request', 'true'); + + // Initialize Response + $this->response = service('response', null, false); + $this->response->setBody('Content'); + $this->response->setHeader('Content-Type', 'text/html'); + + $toolbar = new Toolbar($this->config); + $toolbar->prepare($this->request, $this->response); + + // Assertions + $this->assertTrue($this->response->hasHeader('Debugbar-Time')); + $this->assertStringNotContainsString('id="debugbar_loader"', (string) $this->response->getBody()); + } + + public function testPrepareInjectsNormallyWithoutIgnoredHeader(): void + { + $this->config->disableOnHeaders = ['HX-Request']; + Factories::injectMock('config', 'Toolbar', $this->config); + + $this->request = service('incomingrequest', null, false); + $this->response = service('response', null, false); + $this->response->setBody('Content'); + $this->response->setHeader('Content-Type', 'text/html'); + + $toolbar = new Toolbar($this->config); + $toolbar->prepare($this->request, $this->response); + + // Assertions + $this->assertStringContainsString('id="debugbar_loader"', (string) $this->response->getBody()); + } +} + +/** + * Mock is_cli() to return false within this namespace. + */ +function is_cli(): bool +{ + return false; +} From 4473aff8defd68b8256bcccfcfdb282937c76f87 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Sun, 21 Dec 2025 01:37:19 +0330 Subject: [PATCH 03/14] docs: Add explanation for skipping Debug Toolbar injection on AJAX-like requests --- user_guide_src/source/changelogs/v4.7.0.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.7.0.rst b/user_guide_src/source/changelogs/v4.7.0.rst index 70b27b15a600..c8babddff294 100644 --- a/user_guide_src/source/changelogs/v4.7.0.rst +++ b/user_guide_src/source/changelogs/v4.7.0.rst @@ -178,6 +178,8 @@ Changes - **Cookie:** The ``CookieInterface::EXPIRES_FORMAT`` has been changed to ``D, d M Y H:i:s T`` to follow the recommended format in RFC 7231. - **Format:** Added support for configuring ``json_encode()`` maximum depth via ``Config\Format::$jsonEncodeDepth``. - **Paths:** Added support for changing the location of the ``.env`` file via the ``Paths::$envDirectory`` property. +- **Toolbar:** Added ``$disableOnHeaders`` property to **app/Config/Toolbar.php**. + ************ Deprecations @@ -193,6 +195,7 @@ Bugs Fixed - **Cookie:** The ``CookieInterface::SAMESITE_STRICT``, ``CookieInterface::SAMESITE_LAX``, and ``CookieInterface::SAMESITE_NONE`` constants are now written in ucfirst style to be consistent with usage in the rest of the framework. - **Cache:** Changed ``WincacheHandler::increment()`` and ``WincacheHandler::decrement()`` to return ``bool`` instead of ``mixed``. +- **Toolbar:** Fixed **Maximum call stack size exceeded”** crash when AJAX-like requests (HTMX, Turbo, Unpoly, etc.) were made on pages with Debug Toolbar enabled. See the repo's `CHANGELOG.md `_ From b5093c45723682ff3b50e2ab02a6174084b541db Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Sun, 21 Dec 2025 03:30:34 +0330 Subject: [PATCH 04/14] tests: try fix test --- system/Debug/Toolbar.php | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index fac06e42ec70..ed61f779eb76 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -390,6 +390,22 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r } $config = config(ToolbarConfig::class); + + try { + $stats = $app->getPerformanceStats(); + if (! isset($stats['startTime']) || ! isset($stats['totalTime'])) { + return; + } + } catch (\Throwable $e) { + return; + } + + foreach ($config->disableOnHeaders as $header) { + if ($request->hasHeader($header)) { + $this->isCustomAjax = true; + break; + } + } $toolbar = service('toolbar', $config); $stats = $app->getPerformanceStats(); @@ -413,13 +429,6 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r $format = $response->getHeaderLine('content-type'); - foreach ($config->disableOnHeaders as $header) { - if ($request->hasHeader($header)) { - $this->isCustomAjax = true; - break; - } - } - // Non-HTML formats should not include the debugbar // then we send headers saying where to find the debug data // for this response From 52919c59a7074964bfc5cb74cf7449433d9e678d Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Sun, 21 Dec 2025 03:40:46 +0330 Subject: [PATCH 05/14] style: fix code style --- system/Debug/Toolbar.php | 5 +++-- user_guide_src/source/changelogs/v4.7.0.rst | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index ed61f779eb76..55d4749cac35 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -27,6 +27,7 @@ use CodeIgniter\I18n\Time; use Config\Toolbar as ToolbarConfig; use Kint\Kint; +use Throwable; /** * Displays a toolbar with bits of stats to aid a developer in debugging. @@ -390,13 +391,13 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r } $config = config(ToolbarConfig::class); - + try { $stats = $app->getPerformanceStats(); if (! isset($stats['startTime']) || ! isset($stats['totalTime'])) { return; } - } catch (\Throwable $e) { + } catch (Throwable) { return; } diff --git a/user_guide_src/source/changelogs/v4.7.0.rst b/user_guide_src/source/changelogs/v4.7.0.rst index c8babddff294..e1364e10c1e6 100644 --- a/user_guide_src/source/changelogs/v4.7.0.rst +++ b/user_guide_src/source/changelogs/v4.7.0.rst @@ -195,7 +195,7 @@ Bugs Fixed - **Cookie:** The ``CookieInterface::SAMESITE_STRICT``, ``CookieInterface::SAMESITE_LAX``, and ``CookieInterface::SAMESITE_NONE`` constants are now written in ucfirst style to be consistent with usage in the rest of the framework. - **Cache:** Changed ``WincacheHandler::increment()`` and ``WincacheHandler::decrement()`` to return ``bool`` instead of ``mixed``. -- **Toolbar:** Fixed **Maximum call stack size exceeded”** crash when AJAX-like requests (HTMX, Turbo, Unpoly, etc.) were made on pages with Debug Toolbar enabled. +- **Toolbar:** Fixed **Maximum call stack size exceeded** crash when AJAX-like requests (HTMX, Turbo, Unpoly, etc.) were made on pages with Debug Toolbar enabled. See the repo's `CHANGELOG.md `_ From 1482cddfe7adc022e823e1d03ef90a6ce7591cb3 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Sun, 21 Dec 2025 14:49:07 +0330 Subject: [PATCH 06/14] refactor: remove unnecessary parts --- app/Config/Toolbar.php | 1 - system/Debug/Toolbar.php | 1 - 2 files changed, 2 deletions(-) diff --git a/app/Config/Toolbar.php b/app/Config/Toolbar.php index f79b51aac64b..1c7c1226df35 100644 --- a/app/Config/Toolbar.php +++ b/app/Config/Toolbar.php @@ -150,7 +150,6 @@ class Toolbar extends BaseConfig */ public array $disableOnHeaders = [ 'HX-Request', // HTMX partial requests - 'HX-Boosted', // HTMX boosted navigation 'X-Unpoly-Request', // Unpoly partial requests 'Turbo-Frame', // Turbo Frames 'Turbo-Visit', // Turbo Drive navigation diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index 55d4749cac35..d14304706cae 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -409,7 +409,6 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r } $toolbar = service('toolbar', $config); - $stats = $app->getPerformanceStats(); $data = $toolbar->run( $stats['startTime'], $stats['totalTime'], From cce24770ddf294b9e242eb5ae6bfdbc6e6a88e2a Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Tue, 23 Dec 2025 02:31:07 +0330 Subject: [PATCH 07/14] feat: add MockCommon for shared test helpers and mocks --- system/Boot.php | 3 +++ tests/_support/MockCommon.php | 47 +++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 tests/_support/MockCommon.php diff --git a/system/Boot.php b/system/Boot.php index 85b983c19d89..b06257bc2f2f 100644 --- a/system/Boot.php +++ b/system/Boot.php @@ -149,6 +149,9 @@ public static function bootTest(Paths $paths): void static::setExceptionHandler(); static::initializeKint(); static::autoloadHelpers(); + + // Global test helpers and mocks for all tests + require_once ROOTPATH . 'tests/_support/MockCommon.php'; } /** diff --git a/tests/_support/MockCommon.php b/tests/_support/MockCommon.php new file mode 100644 index 000000000000..fe1b47c4e274 --- /dev/null +++ b/tests/_support/MockCommon.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Tests\Support; + +/** + * MockCommon.php + * + * Shared mocks and helpers for all tests in CodeIgniter 4. + * Loaded automatically during `Boot::bootTest()`. + * + * Purpose: + * - Provide global mock functions like `is_cli()` + * - Keep tests isolated and maintainable + * + * How to extend: + * - Add new helper functions below + * - Keep mocks idempotent to avoid side effects + */ + +// --------------------------------------------------- +// Environment helpers +// --------------------------------------------------- +if (! function_exists('is_cli')) { + /** + * Force non-CLI environment for tests + */ + function is_cli(): bool + { + return false; + } +} + +// --------------------------------------------------- +// Placeholder for additional mocks/helpers +// --------------------------------------------------- +// Add any other test helpers here, e.g., logger mocks, cache mocks, etc. From 17cf6214a8cc5b1612391e4ca9a819dcf36d41c3 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Tue, 23 Dec 2025 02:33:15 +0330 Subject: [PATCH 08/14] tests: refactor tests to use MockCommon helpers --- system/Debug/Toolbar.php | 10 +--------- tests/system/Debug/ToolbarTest.php | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index d14304706cae..de0434871eea 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -392,15 +392,6 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r $config = config(ToolbarConfig::class); - try { - $stats = $app->getPerformanceStats(); - if (! isset($stats['startTime']) || ! isset($stats['totalTime'])) { - return; - } - } catch (Throwable) { - return; - } - foreach ($config->disableOnHeaders as $header) { if ($request->hasHeader($header)) { $this->isCustomAjax = true; @@ -409,6 +400,7 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r } $toolbar = service('toolbar', $config); + $stats = $app->getPerformanceStats(); $data = $toolbar->run( $stats['startTime'], $stats['totalTime'], diff --git a/tests/system/Debug/ToolbarTest.php b/tests/system/Debug/ToolbarTest.php index 8456bd934b3f..75ceb0599b51 100644 --- a/tests/system/Debug/ToolbarTest.php +++ b/tests/system/Debug/ToolbarTest.php @@ -33,12 +33,16 @@ final class ToolbarTest extends CIUnitTestCase private ToolbarConfig $config; private ?IncomingRequest $request = null; private ?ResponseInterface $response = null; + private bool $originalIsCli; protected function setUp(): void { parent::setUp(); Services::reset(); + $this->originalIsCli = is_cli(); + is_cli(false); + $this->config = new ToolbarConfig(); // Mock CodeIgniter core service to provide performance stats @@ -50,6 +54,14 @@ protected function setUp(): void Services::injectMock('codeigniter', $app); } + protected function tearDown(): void + { + // Restore original is_cli state + is_cli($this->originalIsCli); + + parent::tearDown(); + } + public function testPrepareRespectsDisableOnHeaders(): void { // Set up the new configuration property @@ -90,11 +102,3 @@ public function testPrepareInjectsNormallyWithoutIgnoredHeader(): void $this->assertStringContainsString('id="debugbar_loader"', (string) $this->response->getBody()); } } - -/** - * Mock is_cli() to return false within this namespace. - */ -function is_cli(): bool -{ - return false; -} From d744b4d84a82a912200d6b82193ede9aae8402c5 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Tue, 23 Dec 2025 02:46:02 +0330 Subject: [PATCH 09/14] fix: rector & cs --- system/Debug/Toolbar.php | 3 +-- tests/system/Debug/ToolbarTest.php | 7 ++----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index de0434871eea..0517c690326d 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -27,7 +27,6 @@ use CodeIgniter\I18n\Time; use Config\Toolbar as ToolbarConfig; use Kint\Kint; -use Throwable; /** * Displays a toolbar with bits of stats to aid a developer in debugging. @@ -400,7 +399,7 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r } $toolbar = service('toolbar', $config); - $stats = $app->getPerformanceStats(); + $stats = $app->getPerformanceStats(); $data = $toolbar->run( $stats['startTime'], $stats['totalTime'], diff --git a/tests/system/Debug/ToolbarTest.php b/tests/system/Debug/ToolbarTest.php index 75ceb0599b51..ac28d4247999 100644 --- a/tests/system/Debug/ToolbarTest.php +++ b/tests/system/Debug/ToolbarTest.php @@ -33,15 +33,12 @@ final class ToolbarTest extends CIUnitTestCase private ToolbarConfig $config; private ?IncomingRequest $request = null; private ?ResponseInterface $response = null; - private bool $originalIsCli; protected function setUp(): void { parent::setUp(); Services::reset(); - - $this->originalIsCli = is_cli(); - is_cli(false); + is_cli(); $this->config = new ToolbarConfig(); @@ -57,7 +54,7 @@ protected function setUp(): void protected function tearDown(): void { // Restore original is_cli state - is_cli($this->originalIsCli); + is_cli(); parent::tearDown(); } From 792e921c590a0ced380311eb01861e37b0210e3e Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Tue, 23 Dec 2025 18:34:49 +0330 Subject: [PATCH 10/14] refactor: update test and other --- app/Config/Toolbar.php | 4 +-- system/Boot.php | 13 +++++++-- system/Debug/Toolbar.php | 15 +++++----- tests/_support/MockCommon.php | 47 ------------------------------ tests/system/Debug/ToolbarTest.php | 7 +++-- 5 files changed, 24 insertions(+), 62 deletions(-) delete mode 100644 tests/_support/MockCommon.php diff --git a/app/Config/Toolbar.php b/app/Config/Toolbar.php index 1c7c1226df35..3b6ee4e7d085 100644 --- a/app/Config/Toolbar.php +++ b/app/Config/Toolbar.php @@ -150,8 +150,6 @@ class Toolbar extends BaseConfig */ public array $disableOnHeaders = [ 'HX-Request', // HTMX partial requests - 'X-Unpoly-Request', // Unpoly partial requests - 'Turbo-Frame', // Turbo Frames - 'Turbo-Visit', // Turbo Drive navigation + 'X-Up-Version', // Unpoly partial requests ]; } diff --git a/system/Boot.php b/system/Boot.php index b06257bc2f2f..7a748ba2e499 100644 --- a/system/Boot.php +++ b/system/Boot.php @@ -144,14 +144,13 @@ public static function bootTest(Paths $paths): void static::loadDotEnv($paths); static::loadEnvironmentBootstrap($paths, false); + static::loadCommonFunctionsMock(); + static::loadCommonFunctions(); static::loadAutoloader(); static::setExceptionHandler(); static::initializeKint(); static::autoloadHelpers(); - - // Global test helpers and mocks for all tests - require_once ROOTPATH . 'tests/_support/MockCommon.php'; } /** @@ -392,4 +391,12 @@ protected static function runCommand(Console $console): int return is_int($exit) ? $exit : EXIT_SUCCESS; } + + protected static function loadCommonFunctionsMock(): void + { + // Require system/Test/Mock/MockCommon.php + if (is_file(SYSTEMPATH . 'Test/Mock/MockCommon.php')) { + require_once SYSTEMPATH . 'Test/Mock/MockCommon.php'; + } + } } diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index 0517c690326d..bef080a29751 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -391,13 +391,6 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r $config = config(ToolbarConfig::class); - foreach ($config->disableOnHeaders as $header) { - if ($request->hasHeader($header)) { - $this->isCustomAjax = true; - break; - } - } - $toolbar = service('toolbar', $config); $stats = $app->getPerformanceStats(); $data = $toolbar->run( @@ -420,6 +413,14 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r $format = $response->getHeaderLine('content-type'); + foreach ($config->disableOnHeaders as $header) { + if ($request->hasHeader($header)) { + $this->isCustomAjax = true; + + continue; + } + } + // Non-HTML formats should not include the debugbar // then we send headers saying where to find the debug data // for this response diff --git a/tests/_support/MockCommon.php b/tests/_support/MockCommon.php deleted file mode 100644 index fe1b47c4e274..000000000000 --- a/tests/_support/MockCommon.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace Tests\Support; - -/** - * MockCommon.php - * - * Shared mocks and helpers for all tests in CodeIgniter 4. - * Loaded automatically during `Boot::bootTest()`. - * - * Purpose: - * - Provide global mock functions like `is_cli()` - * - Keep tests isolated and maintainable - * - * How to extend: - * - Add new helper functions below - * - Keep mocks idempotent to avoid side effects - */ - -// --------------------------------------------------- -// Environment helpers -// --------------------------------------------------- -if (! function_exists('is_cli')) { - /** - * Force non-CLI environment for tests - */ - function is_cli(): bool - { - return false; - } -} - -// --------------------------------------------------- -// Placeholder for additional mocks/helpers -// --------------------------------------------------- -// Add any other test helpers here, e.g., logger mocks, cache mocks, etc. diff --git a/tests/system/Debug/ToolbarTest.php b/tests/system/Debug/ToolbarTest.php index ac28d4247999..75ceb0599b51 100644 --- a/tests/system/Debug/ToolbarTest.php +++ b/tests/system/Debug/ToolbarTest.php @@ -33,12 +33,15 @@ final class ToolbarTest extends CIUnitTestCase private ToolbarConfig $config; private ?IncomingRequest $request = null; private ?ResponseInterface $response = null; + private bool $originalIsCli; protected function setUp(): void { parent::setUp(); Services::reset(); - is_cli(); + + $this->originalIsCli = is_cli(); + is_cli(false); $this->config = new ToolbarConfig(); @@ -54,7 +57,7 @@ protected function setUp(): void protected function tearDown(): void { // Restore original is_cli state - is_cli(); + is_cli($this->originalIsCli); parent::tearDown(); } From 1cc588e39eca63521d61bccd4323fbdfb16de7b0 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Wed, 24 Dec 2025 02:12:52 +0330 Subject: [PATCH 11/14] refactor: internal cleanup and tooling alignment --- app/Config/Toolbar.php | 15 +++------------ system/Boot.php | 15 ++++++--------- tests/system/Debug/ToolbarTest.php | 6 ++---- 3 files changed, 11 insertions(+), 25 deletions(-) diff --git a/app/Config/Toolbar.php b/app/Config/Toolbar.php index 3b6ee4e7d085..454349bd4427 100644 --- a/app/Config/Toolbar.php +++ b/app/Config/Toolbar.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - namespace Config; use CodeIgniter\Config\BaseConfig; @@ -144,12 +135,12 @@ class Toolbar extends BaseConfig * (such as infinite loops or "Maximum call stack size exceeded"). * * Any request containing one of the following headers is treated as a - * client-managed or partial request, and the Debug Toolbar injection is skipped.s + * client-managed or partial request, and the Debug Toolbar injection is skipped. * * @var list */ public array $disableOnHeaders = [ - 'HX-Request', // HTMX partial requests - 'X-Up-Version', // Unpoly partial requests + 'HX-Request', // HTMX partial requests + 'X-Up-Version', // Unpoly partial requests ]; } diff --git a/system/Boot.php b/system/Boot.php index 7a748ba2e499..76f9fee8966d 100644 --- a/system/Boot.php +++ b/system/Boot.php @@ -145,8 +145,8 @@ public static function bootTest(Paths $paths): void static::loadEnvironmentBootstrap($paths, false); static::loadCommonFunctionsMock(); - static::loadCommonFunctions(); + static::loadAutoloader(); static::setExceptionHandler(); static::initializeKint(); @@ -262,6 +262,11 @@ protected static function loadCommonFunctions(): void require_once SYSTEMPATH . 'Common.php'; } + protected static function loadCommonFunctionsMock(): void + { + require_once SYSTEMPATH . 'Test/Mock/MockCommon.php'; + } + /** * The autoloader allows all the pieces to work together in the framework. * We have to load it here, though, so that the config files can use the @@ -391,12 +396,4 @@ protected static function runCommand(Console $console): int return is_int($exit) ? $exit : EXIT_SUCCESS; } - - protected static function loadCommonFunctionsMock(): void - { - // Require system/Test/Mock/MockCommon.php - if (is_file(SYSTEMPATH . 'Test/Mock/MockCommon.php')) { - require_once SYSTEMPATH . 'Test/Mock/MockCommon.php'; - } - } } diff --git a/tests/system/Debug/ToolbarTest.php b/tests/system/Debug/ToolbarTest.php index 75ceb0599b51..a67e679830cd 100644 --- a/tests/system/Debug/ToolbarTest.php +++ b/tests/system/Debug/ToolbarTest.php @@ -33,14 +33,12 @@ final class ToolbarTest extends CIUnitTestCase private ToolbarConfig $config; private ?IncomingRequest $request = null; private ?ResponseInterface $response = null; - private bool $originalIsCli; protected function setUp(): void { parent::setUp(); Services::reset(); - $this->originalIsCli = is_cli(); is_cli(false); $this->config = new ToolbarConfig(); @@ -56,8 +54,8 @@ protected function setUp(): void protected function tearDown(): void { - // Restore original is_cli state - is_cli($this->originalIsCli); + // Restore is_cli state + is_cli(); parent::tearDown(); } From 7f65297b7089664a67340c697106578ff9c51e37 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Wed, 24 Dec 2025 03:03:37 +0330 Subject: [PATCH 12/14] tests: fix is_cli state --- tests/system/Debug/ToolbarTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/system/Debug/ToolbarTest.php b/tests/system/Debug/ToolbarTest.php index a67e679830cd..2203b5aa6865 100644 --- a/tests/system/Debug/ToolbarTest.php +++ b/tests/system/Debug/ToolbarTest.php @@ -55,7 +55,7 @@ protected function setUp(): void protected function tearDown(): void { // Restore is_cli state - is_cli(); + is_cli(true); parent::tearDown(); } From d3249e7cdac13805ea46dad627b547029bef7b55 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Wed, 24 Dec 2025 03:22:47 +0330 Subject: [PATCH 13/14] refactor: stop iterating headers after first match --- system/Debug/Toolbar.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index bef080a29751..fac06e42ec70 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -416,8 +416,7 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r foreach ($config->disableOnHeaders as $header) { if ($request->hasHeader($header)) { $this->isCustomAjax = true; - - continue; + break; } } From e2ef9a935622aa3892ab04231c203538c62e96a2 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Wed, 24 Dec 2025 04:56:44 +0330 Subject: [PATCH 14/14] fix: suppress RemoveExtraParametersRector false-positive for is_cli override --- rector.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rector.php b/rector.php index ef2e55a10116..55e8c6a66a03 100644 --- a/rector.php +++ b/rector.php @@ -31,6 +31,7 @@ use Rector\EarlyReturn\Rector\If_\RemoveAlwaysElseRector; use Rector\EarlyReturn\Rector\Return_\PreparedValueToEarlyReturnRector; use Rector\Php70\Rector\FuncCall\RandomFunctionRector; +use Rector\Php71\Rector\FuncCall\RemoveExtraParametersRector; use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector; use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\YieldDataProviderRector; @@ -107,6 +108,10 @@ __DIR__ . '/system/HTTP/Response.php', ], + RemoveExtraParametersRector::class => [ + __DIR__ . '/tests/system/Debug/ToolbarTest.php', + ], + // check on constant compare UnwrapFutureCompatibleIfPhpVersionRector::class => [ __DIR__ . '/system/Autoloader/Autoloader.php',