Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ All notable changes to `phalcon/debugbar` are documented here. The format is bas
- `History\HistoryEndpoint` takes the request and the base URI with no DI container, and `Mvc\HistoryListener` selects the MVC controller. [#41](https://github.com/phalcon/debugbar/issues/41)
- `History\HistoryHandler` lists, reads and clears the request history with no MVC types. [#42](https://github.com/phalcon/debugbar/issues/42)
- Request history in ADR applications, with the `history.base_uri` option. [#43](https://github.com/phalcon/debugbar/issues/43)
- The `debug_page` option shows the debug page for a server error in ADR applications. [#48](https://github.com/phalcon/debugbar/issues/48)

### Fixed

Expand Down
220 changes: 110 additions & 110 deletions composer.lock

Large diffs are not rendered by default.

32 changes: 31 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ use Phalcon\Debug;
(new Debug())->listen();
```

`listen()` does not see the exceptions of an ADR application, because the application catches them first. For an ADR application, see [ADR](#adr).

The remainder of this document covers the debug bar.

## Registering the Debug Bar
Expand Down Expand Up @@ -213,6 +215,33 @@ The provider decorates the router (`Phalcon\Contracts\ADR\Router\Router`) with `

Do not resolve the router during registration. `extend()` on a resolved service throws `CannotExtendResolved`. In a blocked environment, the provider does not change the router.

The `debug_page` key shows the debug page (`Phalcon\Debug`) for a server error. It is off by default. The provider reads the exception from the bar, so add `ExceptionAdapter` to your logger.

```php
<?php

use Phalcon\DebugBar\ADR\Provider as DebugBarProvider;

(new DebugBarProvider(['debug_page' => true]))->provide($container);
```

The debug page replaces the content of the response when the status is 500 or higher, the response is an HTML page and the access gate allows the client. The status does not change. A JSON response, an AJAX request and a 404 response keep their content. The bar is not in the debug page. If the debug page fails, the response keeps the error page of the application.

!!! warning "NOTE"

The debug page shows `$_SERVER`, which can hold passwords and keys. To remove these values, give a configured `Phalcon\Debug` to the provider.

```php
<?php

use Phalcon\Debug;
use Phalcon\DebugBar\ADR\Provider as DebugBarProvider;

$debug = (new Debug())->setBlacklist(['server' => ['DB_PASSWORD', 'APP_CRYPT_SALT']]);

(new DebugBarProvider(['debug_page' => $debug]))->provide($container);
```

These limits apply in an ADR application:

- **One request for each process.** `Phalcon\ADR\Application::handle()` supports a reused request in a long-lived worker, but collectors have no reset method. In a worker, their data grows from request to request. `HistoryCookie::fromGlobals()` also reads only at boot.
Expand Down Expand Up @@ -252,14 +281,15 @@ if (null !== $listener) {

## Configuration

The second argument to `Provider` is a nested array. Every key is optional. `Phalcon\DebugBar\ADR\Provider` and `Phalcon\DebugBar\Core` take the same array as their only argument. `Core` does not read the `history` keys.
The second argument to `Provider` is a nested array. Every key is optional. `Phalcon\DebugBar\ADR\Provider` and `Phalcon\DebugBar\Core` take the same array as their only argument. `Core` does not read the `debug_page` and `history` keys.

| Key | Type | Default | Purpose |
|--------------------|---------------------------|--------------------------|------------------------------------------------------------------|
| `access.allow_ips` | `list<string>` | `[]` (any client) | Client IP allowlist. Empty allows any address. |
| `access.callback` | `(Closure(): bool)\|null` | `null` | Extra gate. When present, it must also return `true`. |
| `assets.nonce` | `string\|null` | `null` | CSP nonce stamped on the injected `<style>` and `<script>` tags. |
| `collectors` | `array<string, bool>` | all applicable enabled | Per-collector switch, keyed by collector name. |
| `debug_page` | `bool\|Phalcon\Debug` | `false` | Show the debug page for a server error. Only the ADR provider reads it. |
| `enabled` | `bool` | `true` | Master switch. When `false`, `boot()` returns after the gate. |
| `env.blocked` | `list<string>` | `['production', 'prod']` | Environment values that block the bar (case-insensitive). |
| `env.strict` | `bool` | `false` | When `true`, `boot()` throws in a blocked/undefined environment instead of returning silently. |
Expand Down
74 changes: 74 additions & 0 deletions src/DebugBar/ADR/DebugPageListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

/**
* This file is part of the Phalcon Framework.
*
* (c) Phalcon Team <team@phalcon.io>
*
* For the full copyright and license information, please view the LICENSE.txt
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Phalcon\DebugBar\ADR;

use Phalcon\Debug;
use Phalcon\DebugBar\DebugBar;
use Phalcon\DebugBar\Injector;
use Phalcon\DebugBar\ResponseListener;
use Phalcon\DebugBar\Security\AccessGate;
use Phalcon\Events\EventInterface;
use Phalcon\Http\RequestInterface;
use Phalcon\Http\ResponseInterface;
use Throwable;

use function is_string;

/**
* Shows the debug page for a server error in an ADR application. The application
* catches each exception before `Debug::listen()` can see it, so this class reads
* the exception that the logger gave to the bar. It runs the bar listener first,
* so that the debug page replaces a page with the bar in it. The event fires
* outside the `try` block of the application: a failure in the debug page keeps
* the error page of the application.
*/
final class DebugPageListener
{
public function __construct(
private readonly ResponseListener $listener,
private readonly DebugBar $bar,
private readonly Debug $debug,
private readonly AccessGate $accessGate,
private readonly ?RequestInterface $request
) {
}

public function __invoke(EventInterface $event, mixed $source, mixed $response): void
{
($this->listener)($event, $source, $response);

$exception = $this->bar->getLastException();
if (null === $exception || !$response instanceof ResponseInterface || !$this->applies($response)) {
return;
}

try {
$response->setContent($this->debug->renderHtml($exception));
} catch (Throwable) {
return;
}
}

/**
* A server error on an HTML page, for a client that the access gate allows.
*/
private function applies(ResponseInterface $response): bool
{
$clientIp = $this->request?->getClientAddress();

return ($response->getStatusCode() ?? 0) >= 500
&& $this->accessGate->allows(is_string($clientIp) ? $clientIp : null)
&& (new Injector())->shouldInject($response, $this->request?->isAjax() ?? false);
}
}
26 changes: 25 additions & 1 deletion src/DebugBar/ADR/Provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,19 @@
use Phalcon\Contracts\Container\Service\Provider as ProviderContract;
use Phalcon\Contracts\Events\Manager as ManagerContract;
use Phalcon\Contracts\Http\AttributeRequest;
use Phalcon\Debug as DebugPage;
use Phalcon\DebugBar\Collector\HistoryCollector;
use Phalcon\DebugBar\Contracts\History;
use Phalcon\DebugBar\Core;
use Phalcon\DebugBar\Debug;
use Phalcon\DebugBar\DebugBar;
use Phalcon\DebugBar\DebugBarTypes;
use Phalcon\DebugBar\Exceptions\CannotUseInProduction;
use Phalcon\DebugBar\History\FilesystemHistory;
use Phalcon\DebugBar\History\HistoryCookie;
use Phalcon\DebugBar\History\HistoryEndpoint;
use Phalcon\DebugBar\History\HistoryOptions;
use Phalcon\DebugBar\ResponseListener;
use Phalcon\Events\ManagerInterface;
use Phalcon\Http\RequestInterface;

Expand All @@ -46,6 +49,8 @@ final class Provider implements ProviderContract
{
private Core $core;

private bool | DebugPage $debugPage;

private ?HistoryCookie $historyCookie = null;

private ?HistoryEndpoint $historyEndpoint = null;
Expand All @@ -58,6 +63,7 @@ final class Provider implements ProviderContract
public function __construct(array $config = [])
{
$this->core = new Core($config);
$this->debugPage = $config['debug_page'] ?? false;
$this->historyOptions = HistoryOptions::fromConfig($config['history'] ?? []);
}

Expand Down Expand Up @@ -89,7 +95,7 @@ public function provide(Collection $services): void
return;
}

$eventsManager->attach('application:afterHandle', $listener);
$eventsManager->attach('application:afterHandle', $this->responseListener($listener, $bar, $request));
(new Bridge($bar))->subscribe($eventsManager);
}

Expand Down Expand Up @@ -143,4 +149,22 @@ private function resolve(Collection $services, string $name): mixed
{
return $services->has($name) ? $services->get($name) : null;
}

private function responseListener(
ResponseListener $listener,
DebugBar $bar,
?RequestInterface $request
): DebugPageListener | ResponseListener {
if (false === $this->debugPage) {
return $listener;
}

return new DebugPageListener(
$listener,
$bar,
true === $this->debugPage ? new DebugPage() : $this->debugPage,
$this->core->getAccessGate(),
$request
);
}
}
9 changes: 9 additions & 0 deletions src/DebugBar/DebugBar.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ class DebugBar
'meta' => [],
];

protected ?Throwable $lastException = null;

/**
* Registers (or replaces) a collector under its name.
*/
Expand All @@ -62,6 +64,8 @@ public function addCollector(Collector $collector): static

public function addException(Throwable $throwable): static
{
$this->lastException = $throwable;

$collector = $this->collectors['exceptions'] ?? null;
if ($collector instanceof ExceptionAware) {
$collector->addThrowable($throwable);
Expand Down Expand Up @@ -158,6 +162,11 @@ public function getData(): array
return $this->data;
}

public function getLastException(): ?Throwable
{
return $this->lastException;
}

public function hasCollector(string $name): bool
{
return isset($this->collectors[$name]);
Expand Down
1 change: 1 addition & 0 deletions src/DebugBar/DebugBarTypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
* assets?: array{nonce?: string|null},
* access?: array{allow_ips?: list<string>, callback?: (\Closure(): bool)|null},
* collectors?: array<string, bool>,
* debug_page?: bool|\Phalcon\Debug,
* headers?: bool,
* history?: history_config,
* redact?: array{mask?: list<string>, hidden?: list<string>}
Expand Down
Loading
Loading