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 @@ -18,6 +18,7 @@ All notable changes to `phalcon/debugbar` are documented here. The format is bas
- Documentation and tests for the View and Cache panels in ADR applications. [#46](https://github.com/phalcon/debugbar/issues/46)
- `Logger\ExceptionAdapter` feeds the Exceptions panel from the logger in ADR applications. [#47](https://github.com/phalcon/debugbar/issues/47)
- `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)

### Fixed

Expand Down
90 changes: 12 additions & 78 deletions src/DebugBar/Controllers/HistoryController.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,106 +14,40 @@
namespace Phalcon\DebugBar\Controllers;

use Phalcon\DebugBar\Contracts\History;
use Phalcon\DebugBar\History\HistoryHandler;
use Phalcon\DebugBar\Security\AccessGate;
use Phalcon\Http\RequestInterface;
use Phalcon\Http\ResponseInterface;
use Phalcon\Mvc\ControllerInterface;

use function is_string;
use function json_encode;
use function strtoupper;

use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;

/**
* Internal MVC adapter for /_debugbar/open. GET returns the current browser's
* request list or one stored entry; DELETE clears that browser's history.
* Internal MVC adapter for /_debugbar/open. `HistoryHandler` does the work.
*/
final class HistoryController implements ControllerInterface
{
private HistoryHandler $handler;

public function __construct(
private readonly History $history,
private readonly AccessGate $accessGate,
private readonly RequestInterface $historyRequest,
private readonly ResponseInterface $historyResponse
History $history,
AccessGate $accessGate,
RequestInterface $historyRequest,
ResponseInterface $historyResponse
) {
$this->handler = new HistoryHandler($history, $accessGate, $historyRequest, $historyResponse);
}

public function clearAction(): ResponseInterface
{
return $this->handle(
'DELETE',
fn (): ResponseInterface => $this->json(
$this->historyResponse,
['cleared' => $this->history->clear()]
)
);
return $this->handler->clear();
}

public function indexAction(): ResponseInterface
{
return $this->handle(
'GET',
function (): ResponseInterface {
$id = $this->historyRequest->getQuery('id');
if (null === $id) {
return $this->json($this->historyResponse, ['requests' => $this->history->find()]);
}
if (!is_string($id)) {
return $this->json($this->historyResponse, ['error' => 'Request not found.'], 404);
}

$entry = $this->history->get($id);
if (null === $entry) {
return $this->json($this->historyResponse, ['error' => 'Request not found.'], 404);
}

return $this->json($this->historyResponse, ['request' => $entry]);
}
);
return $this->handler->index();
}

public function openAction(): ResponseInterface
{
return 'DELETE' === $this->transportMethod() ? $this->clearAction() : $this->indexAction();
}

/**
* @param callable(): ResponseInterface $action
*/
private function handle(string $expectedMethod, callable $action): ResponseInterface
{
$clientIp = $this->historyRequest->getClientAddress();
if (!$this->accessGate->allows(is_string($clientIp) ? $clientIp : null)) {
return $this->json($this->historyResponse, ['error' => 'Not found.'], 404);
}

if ($expectedMethod !== $this->transportMethod()) {
return $this->json($this->historyResponse, ['error' => 'Method not allowed.'], 405);
}

return $action();
}

/**
* @param array<string, mixed> $body
*/
private function json(ResponseInterface $response, array $body, int $status = 200): ResponseInterface
{
$json = json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);

$response->setStatusCode($status);
$response->setContentType('application/json', 'UTF-8');
$response->setHeader('Cache-Control', 'no-store, private');
$response->setHeader('X-Content-Type-Options', 'nosniff');
$response->setContent(false === $json ? '{}' : $json);

return $response;
}

private function transportMethod(): string
{
return strtoupper($this->historyRequest->getServer('REQUEST_METHOD') ?? '');
return $this->handler->open();
}
}
116 changes: 116 additions & 0 deletions src/DebugBar/History/HistoryHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?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\History;

use Phalcon\DebugBar\Contracts\History;
use Phalcon\DebugBar\Security\AccessGate;
use Phalcon\Http\RequestInterface;
use Phalcon\Http\ResponseInterface;

use function is_string;
use function json_encode;
use function strtoupper;

use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;

/**
* Serves the history endpoint with no framework application. GET returns the
* request list of the current browser or one stored entry. DELETE clears the
* history of that browser.
*/
final class HistoryHandler
{
public function __construct(
private readonly History $history,
private readonly AccessGate $accessGate,
private readonly RequestInterface $request,
private readonly ResponseInterface $response
) {
}

public function clear(): ResponseInterface
{
return $this->handle(
'DELETE',
fn (): ResponseInterface => $this->json(['cleared' => $this->history->clear()])
);
}

public function index(): ResponseInterface
{
return $this->handle(
'GET',
function (): ResponseInterface {
$id = $this->request->getQuery('id');
if (null === $id) {
return $this->json(['requests' => $this->history->find()]);
}
if (!is_string($id)) {
return $this->json(['error' => 'Request not found.'], 404);
}

$entry = $this->history->get($id);
if (null === $entry) {
return $this->json(['error' => 'Request not found.'], 404);
}

return $this->json(['request' => $entry]);
}
);
}

public function open(): ResponseInterface
{
return 'DELETE' === $this->transportMethod() ? $this->clear() : $this->index();
}

/**
* @param callable(): ResponseInterface $action
*/
private function handle(string $expectedMethod, callable $action): ResponseInterface
{
$clientIp = $this->request->getClientAddress();
if (!$this->accessGate->allows(is_string($clientIp) ? $clientIp : null)) {
return $this->json(['error' => 'Not found.'], 404);
}

if ($expectedMethod !== $this->transportMethod()) {
return $this->json(['error' => 'Method not allowed.'], 405);
}

return $action();
}

/**
* @param array<string, mixed> $body
*/
private function json(array $body, int $status = 200): ResponseInterface
{
$json = json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);

$this->response->setStatusCode($status);
$this->response->setContentType('application/json', 'UTF-8');
$this->response->setHeader('Cache-Control', 'no-store, private');
$this->response->setHeader('X-Content-Type-Options', 'nosniff');
$this->response->setContent(false === $json ? '{}' : $json);

return $this->response;
}

private function transportMethod(): string
{
return strtoupper($this->request->getServer('REQUEST_METHOD') ?? '');
}
}
Loading
Loading