Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions app/Config/Toolbar.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,28 @@ 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.
*
* @var list<string>
*/
public array $disableOnHeaders = [
'HX-Request', // HTMX partial requests
'X-Up-Version', // Unpoly partial requests
];
}
5 changes: 5 additions & 0 deletions rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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',
Expand Down
7 changes: 7 additions & 0 deletions system/Boot.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,9 @@ public static function bootTest(Paths $paths): void
static::loadDotEnv($paths);
static::loadEnvironmentBootstrap($paths, false);

static::loadCommonFunctionsMock();
static::loadCommonFunctions();

static::loadAutoloader();
static::setExceptionHandler();
static::initializeKint();
Expand Down Expand Up @@ -260,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
Expand Down
27 changes: 19 additions & 8 deletions system/Debug/Toolbar.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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
Expand All @@ -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'],
Expand All @@ -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}"));

Expand Down Expand Up @@ -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;
Expand Down
102 changes: 102 additions & 0 deletions tests/system/Debug/ToolbarTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

declare(strict_types=1);

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <[email protected]>
*
* 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();

is_cli(false);

$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);
}

protected function tearDown(): void
{
// Restore is_cli state
is_cli(true);

parent::tearDown();
}

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('<html><body>Content</body></html>');
$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('<html><body>Content</body></html>');
$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());
}
}
3 changes: 3 additions & 0 deletions user_guide_src/source/changelogs/v4.7.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 <https://github.com/codeigniter4/CodeIgniter4/blob/develop/CHANGELOG.md>`_
Expand Down
Loading