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
523 changes: 315 additions & 208 deletions composer.lock

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion config.default.ini
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ error_page_dir=page/_error
router_file=router.php
router_class=AppRouter
redirect_response_code=307
default_content_type=text/html
default_content_type=application/json

[view]
component_directory=page/_component
Expand Down Expand Up @@ -66,6 +66,8 @@ query_directory=query
migration_path=_migration
migration_table=_migration
query_path=query
charset=utf8mb4
collation=utf8mb4_general_ci

[security]
;default_headers="X-Content-Type-Options: nosniff; X-Frame-Options: deny; Content-Security-Policy: default-src 'none'"
Expand Down
251 changes: 203 additions & 48 deletions src/Dispatch/Dispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ public function __construct(
);
$this->viewStreamer = $viewStreamer ?? new ViewStreamer();
$this->headerManager = $headerManager ?? new HeaderManager();
$this->setupJsonDocumentErrorCallback();
}
// phpcs:enable Generic.Metrics.CyclomaticComplexity.TooHigh

Expand Down Expand Up @@ -290,48 +291,153 @@ public function generateBasicErrorResponse(
Throwable $actualThrowable,
Throwable $innerThrowable,
):Response {
// TODO: Handle innerThrowable for if there's an error thrown in WebEngine itself.
$errorStatusCode = $this->getBasicErrorStatusCode($actualThrowable);
$errorType = get_class($actualThrowable);
$errorMessage = $this->getBasicErrorMessage(
$actualThrowable,
$errorStatusCode,
);
$detail = $this->getBasicErrorDetail(
$actualThrowable,
$innerThrowable,
$errorStatusCode,
);

if($this->view instanceof JSONView || $this->viewModel instanceof JSONDocument) {
return $this->createBasicJsonErrorResponse(
$errorStatusCode,
$errorType,
$errorMessage,
$detail,
);
}

return $this->createBasicHtmlErrorResponse(
$errorStatusCode,
$errorType,
$errorMessage,
$detail,
);
}

private function getBasicErrorStatusCode(Throwable $throwable):int {
$errorStatusCode = $this->response->getStatusCode();
if(!$errorStatusCode) {
$errorStatusCode = $actualThrowable instanceof ResponseStatusException
? $actualThrowable->getHttpCode()
: StatusCode::INTERNAL_SERVER_ERROR;
if($errorStatusCode) {
return $errorStatusCode;
}
$errorType = get_class($actualThrowable);
// var_dump($errorType);die();
$errorMessage = $actualThrowable->getMessage();
$detail = "";

$errorPageDir = $this->config->getString("app.error_page_dir");
return $throwable instanceof ResponseStatusException
? $throwable->getHttpCode()
: StatusCode::INTERNAL_SERVER_ERROR;
}

if(!$errorMessage) {
if($actualThrowable instanceof HttpNotFound) {
$errorMessage = "The server could not find the requested resource.";
private function getBasicErrorMessage(
Throwable $throwable,
int $errorStatusCode,
):string {
$errorMessage = $throwable->getMessage();
if($errorMessage) {
return $errorMessage;
}

if(!$this->config->getBool("app.production")) {
$detail .= " Additionally, there was no error page found in your "
. "application at <strong>$errorPageDir/$errorStatusCode.html</strong>";
}
}
if($throwable instanceof HttpNotFound) {
return "The server could not find the requested resource.";
}

return StatusCode::REASON_PHRASE[$errorStatusCode] ?? "Error";
}

private function getBasicErrorDetail(
Throwable $throwable,
Throwable $innerThrowable,
int $errorStatusCode,
):string {
$detail = $this->getBasicNotFoundErrorDetail($throwable, $errorStatusCode);
if($errorStatusCode >= 500 && !$this->config->getBool("app.production")) {
$detail .= implode(":", [
$actualThrowable->getFile(),
$actualThrowable->getLine(),
]) . "\n\n";
foreach($actualThrowable->getTrace() as $i => $t) {
if(isset($t["file"]) && str_ends_with($t["file"], "/vendor/phpgt/servicecontainer/src/Injector.php")) {
break;
}
$detail .= "#$i\n";
$detail .= $this->getBasicDebugErrorDetail($throwable);
}
if(!$this->config->getBool("app.production")) {
$detail .= $this->getBasicInnerThrowableDetail($innerThrowable);
}

foreach($t as $key => $value) {
$detail .= "$key:\t$value\n";
}
return $detail;
}

private function getBasicNotFoundErrorDetail(
Throwable $throwable,
int $errorStatusCode,
):string {
if(!($throwable instanceof HttpNotFound)
|| $throwable->getMessage()
|| $this->config->getBool("app.production")) {
return "";
}

$errorPageDir = $this->config->getString("app.error_page_dir");
return " Additionally, there was no error page found in your "
. "application at <strong>$errorPageDir/$errorStatusCode.html</strong>";
}

private function getBasicDebugErrorDetail(Throwable $throwable):string {
$detail = implode(":", [
$throwable->getFile(),
$throwable->getLine(),
]) . "\n\n";
foreach($throwable->getTrace() as $i => $t) {
if(isset($t["file"]) && str_ends_with($t["file"], "/vendor/phpgt/servicecontainer/src/Injector.php")) {
break;
}
$detail .= "#$i\n";

foreach($t as $key => $value) {
$detail .= "$key:\t$value\n";
}
}

return $detail;
}

private function getBasicInnerThrowableDetail(Throwable $throwable):string {
return "\n\nFailed to render framework error response: "
. get_class($throwable)
. ": "
. $throwable->getMessage()
. "\n"
. $throwable->getFile()
. ":"
. $throwable->getLine()
. "\n";
}

private function createBasicJsonErrorResponse(
int $errorStatusCode,
string $errorType,
string $errorMessage,
string $detail,
):Response {
$error = [
"error" => $errorMessage,
"status" => $errorStatusCode,
"type" => $errorType,
];
if($detail !== "") {
$error["detail"] = $detail;
}

$body = new Stream();
$body->write((json_encode($error) ?: "{}") . "\n");
$response = new Response(null, null, $this->request);
$response = $response->withHeader("Content-Type", "application/json");
$response = $response->withBody($body);
return $response->withStatus($errorStatusCode);
}

private function createBasicHtmlErrorResponse(
int $errorStatusCode,
string $errorType,
string $errorMessage,
string $detail,
):Response {
// TODO: Load this HTML from a file in the root of WebEngine!
$html = <<<HTML
<!doctype html>
Expand Down Expand Up @@ -359,6 +465,34 @@ private function setupResponse():Response {
return $response;
}

private function setupJsonDocumentErrorCallback():void {
if(!($this->view instanceof JSONView) || !($this->viewModel instanceof JSONDocument)) {
return;
}

$this->viewModel->setErrorCallback(function(
string $message,
int $statusCode,
):void {
if(!$this->response->getStatusCode()) {
$this->response = $this->response->withStatus($statusCode);
}
if(!$this->response->hasHeader("Content-Type")) {
$this->response = $this->response->withHeader(
"Content-Type",
"application/json",
);
}

$this->viewStreamer->stream($this->view, $this->viewModel);
($this->finishCallback)($this->response);

if($this->interruptResponse) {
throw new ResponseInterrupt();
}
});
}

private function handleLogicExecution(
Assembly $logicAssembly,
Input $input,
Expand Down Expand Up @@ -498,24 +632,8 @@ public function processResponse(
}
}

if($this->logicExecutionOrder) {
$this->response = $this->response->withHeader(
self::LOGIC_EXECUTION_HEADER,
implode(";", $this->logicExecutionOrder),
);
}

if($responseWithHeader = $this->headerManager->applyWithHeader(
$this->response->getResponseHeaders(),
$this->response->withHeader(...)
)) {
$this->response = $responseWithHeader;
}

$documentBinder = $this->serviceContainer->get(Binder::class);
assert($documentBinder instanceof DocumentBinder);
$documentBinder->cleanupDocument();
$this->protectHtmlDocumentFromCsrf();
$this->applyResponseHeaders();
$this->prepareViewModelForStreaming();

$this->viewStreamer->stream($this->view, $this->viewModel);
$this->consumeCsrfToken($csrfToken);
Expand All @@ -530,6 +648,39 @@ public function processResponse(
}
// phpcs:enable Generic.Metrics.CyclomaticComplexity.TooHigh

private function applyResponseHeaders():void {
if($this->logicExecutionOrder) {
$this->response = $this->response->withHeader(
self::LOGIC_EXECUTION_HEADER,
implode(";", $this->logicExecutionOrder),
);
}

if($responseWithHeader = $this->headerManager->applyWithHeader(
$this->response->getResponseHeaders(),
$this->response->withHeader(...)
)) {
$this->response = $responseWithHeader;
}
}

private function prepareViewModelForStreaming():void {
if($this->viewModel instanceof HTMLDocument) {
$documentBinder = $this->serviceContainer->get(Binder::class);
assert($documentBinder instanceof DocumentBinder);
$documentBinder->cleanupDocument();
$this->protectHtmlDocumentFromCsrf();
return;
}

if($this->view instanceof JSONView && !$this->response->hasHeader("Content-Type")) {
$this->response = $this->response->withHeader(
"Content-Type",
"application/json",
);
}
}

private function recordLogicExecution(string $functionReference):void {
$refWithoutAttributes = explode("#", $functionReference, 2)[0];
$separatorPosition = strrpos($refWithoutAttributes, "::");
Expand Down Expand Up @@ -565,6 +716,10 @@ private function normaliseLogicExecutionFile(string $file):string {
}

private function verifyCsrfRequest(string $method, InputData $inputData):?string {
if(!($this->viewModel instanceof HTMLDocument)) {
return null;
}

if($method !== "POST" || $this->isIgnoredCsrfPath()) {
return null;
}
Expand Down
3 changes: 2 additions & 1 deletion src/View/BaseView.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
namespace GT\WebEngine\View;

use GT\Dom\HTMLDocument;
use GT\Json\Schema\JSONDocument;
use Psr\Http\Message\StreamInterface;

abstract class BaseView {
Expand All @@ -20,7 +21,7 @@ public function addViewFile(string $fileName):void {
array_push($this->viewFileArray, $fileName);
}

public function stream(HTMLDocument $viewModel):void {
public function stream(HTMLDocument|JSONDocument $viewModel):void {
$this->outputStream->write((string)$viewModel);
}
}
5 changes: 5 additions & 0 deletions src/View/JSONView.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php
namespace GT\WebEngine\View;

use GT\Dom\HTMLDocument;
use GT\Json\Schema\JSONDocument;

/**
Expand All @@ -11,4 +12,8 @@ class JSONView extends BaseView {
public function createViewModel():JSONDocument {
return new JSONDocument();
}

public function stream(HTMLDocument|JSONDocument $viewModel):void {
$this->outputStream->write((string)$viewModel . "\n");
}
}
3 changes: 2 additions & 1 deletion src/View/ViewStreamer.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
namespace GT\WebEngine\View;

use GT\Dom\HTMLDocument;
use GT\Json\Schema\JSONDocument;

class ViewStreamer {
public function stream(
HTMLView|JSONView|NullView $view,
HTMLDocument $viewModel,
HTMLDocument|JSONDocument $viewModel,
):void {
$view->stream($viewModel);
}
Expand Down
Loading
Loading