diff --git a/VERSION b/VERSION index e003a14..01bd5ce 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2403.282033 +0.2403.282229 diff --git a/src/Http/ContentHandling/BencodeContentHandler.php b/src/Http/ContentHandling/BencodeContentHandler.php new file mode 100644 index 0000000..720b4fa --- /dev/null +++ b/src/Http/ContentHandling/BencodeContentHandler.php @@ -0,0 +1,23 @@ +hasContentType()) + $response->setTypePlain(); + + $response->setContent(new BencodedContent($content)); + } +} diff --git a/src/Http/ContentHandling/IContentHandler.php b/src/Http/ContentHandling/IContentHandler.php new file mode 100644 index 0000000..7dd9705 --- /dev/null +++ b/src/Http/ContentHandling/IContentHandler.php @@ -0,0 +1,12 @@ +hasContentType()) + $response->setTypeJson(); + + $response->setContent(new JsonContent($content)); + } +} diff --git a/src/Http/ContentHandling/StreamContentHandler.php b/src/Http/ContentHandling/StreamContentHandler.php new file mode 100644 index 0000000..c58e9c3 --- /dev/null +++ b/src/Http/ContentHandling/StreamContentHandler.php @@ -0,0 +1,23 @@ +hasContentType()) + $response->setTypeStream(); + + $response->setContent(new StreamContent($content)); + } +} diff --git a/src/Http/ErrorHandling/HtmlErrorHandler.php b/src/Http/ErrorHandling/HtmlErrorHandler.php new file mode 100644 index 0000000..0c3b5b4 --- /dev/null +++ b/src/Http/ErrorHandling/HtmlErrorHandler.php @@ -0,0 +1,33 @@ + + + + + :code :message + + +

:code :message

+
+
Index
+ + + HTML; + + public function handle(HttpResponseBuilder $response, HttpRequest $request, int $code, string $message): void { + $response->setTypeHTML(); + $response->setContent(strtr(self::TEMPLATE, [ + ':charset' => strtolower(mb_preferred_mime_name(mb_internal_encoding())), + ':code' => sprintf('%03d', $code), + ':message' => $message, + ])); + } +} diff --git a/src/Http/ErrorHandling/IErrorHandler.php b/src/Http/ErrorHandling/IErrorHandler.php new file mode 100644 index 0000000..d8d1842 --- /dev/null +++ b/src/Http/ErrorHandling/IErrorHandler.php @@ -0,0 +1,13 @@ +setTypePlain(); + $response->setContent(sprintf('HTTP %03d %s', $code, $message)); + } +} diff --git a/src/Http/HttpMessageBuilder.php b/src/Http/HttpMessageBuilder.php index 3a58dba..26a32f1 100644 --- a/src/Http/HttpMessageBuilder.php +++ b/src/Http/HttpMessageBuilder.php @@ -1,7 +1,7 @@ content; } + /** @phpstan-impure */ public function hasContent(): bool { return $this->content !== null; } diff --git a/src/Http/Routing/HttpRouter.php b/src/Http/Routing/HttpRouter.php index 30baf1f..9e28945 100644 --- a/src/Http/Routing/HttpRouter.php +++ b/src/Http/Routing/HttpRouter.php @@ -7,7 +7,10 @@ namespace Index\Http\Routing; use stdClass; use InvalidArgumentException; -use RuntimeException; +use Index\Http\{HttpResponse,HttpResponseBuilder,HttpRequest}; +use Index\Http\Content\StringContent; +use Index\Http\ContentHandling\{BencodeContentHandler,IContentHandler,JsonContentHandler,StreamContentHandler}; +use Index\Http\ErrorHandling\{HtmlErrorHandler,IErrorHandler,PlainErrorHandler}; class HttpRouter implements IRouter { use RouterTrait; @@ -17,6 +20,52 @@ class HttpRouter implements IRouter { private array $staticRoutes = []; private array $dynamicRoutes = []; + private array $contentHandlers = []; + + private IErrorHandler $errorHandler; + + public function __construct( + IErrorHandler|string $errorHandler = 'html', + bool $registerDefaultContentHandlers = true, + ) { + $this->setErrorHandler($errorHandler); + + if($registerDefaultContentHandlers) + $this->registerDefaultContentHandlers(); + } + + public function getErrorHandler(): IErrorHandler { + return $this->errorHandler; + } + + public function setErrorHandler(IErrorHandler|string $handler): void { + if($handler instanceof IErrorHandler) + $this->errorHandler = $handler; + elseif($handler === 'html') + $this->setHTMLErrorHandler(); + else // plain + $this->setPlainErrorHandler(); + } + + public function setHTMLErrorHandler(): void { + $this->errorHandler = new HtmlErrorHandler; + } + + public function setPlainErrorHandler(): void { + $this->errorHandler = new PlainErrorHandler; + } + + public function registerContentHandler(IContentHandler $contentHandler): void { + if(!in_array($contentHandler, $this->contentHandlers)) + $this->contentHandlers[] = $contentHandler; + } + + public function registerDefaultContentHandlers(): void { + $this->registerContentHandler(new StreamContentHandler); + $this->registerContentHandler(new JsonContentHandler); + $this->registerContentHandler(new BencodeContentHandler); + } + public function scopeTo(string $prefix): IRouter { return new ScopedRouter($this, $prefix); } @@ -104,4 +153,84 @@ class HttpRouter implements IRouter { return new ResolvedRouteInfo($middlewares, array_keys($methods), $handler, $args); } + + public function dispatch(?HttpRequest $request = null, array $args = []): void { + $request ??= HttpRequest::fromRequest(); + $response = new HttpResponseBuilder; + $args = array_merge([$response, $request], $args); + + $routeInfo = $this->resolve($request->getMethod(), $request->getPath()); + + // always run middleware regardless of 404 or 405 + $result = $routeInfo->runMiddleware($args); + if($result === null) { + if(!$routeInfo->hasHandler()) { + if($routeInfo->hasOtherMethods()) { + $result = 405; + $response->setHeader('Allow', implode(', ', $routeInfo->getSupportedMethods())); + } else + $result = 404; + } else + $result = $routeInfo->dispatch($args); + } + + if(is_int($result)) { + if(!$response->hasStatusCode() && $result >= 100 && $result < 600) + $this->writeErrorPage($response, $request, $result); + else + $response->setContent(new StringContent((string)$result)); + } elseif(!$response->hasContent()) { + foreach($this->contentHandlers as $contentHandler) + if($contentHandler->match($result)) { + $contentHandler->handle($response, $result); + break; + } + + if(!$response->hasContent() && $result !== null) { + $result = (string)$result; + $response->setContent(new StringContent($result)); + + if(!$response->hasContentType()) { + $charset = strtolower(mb_preferred_mime_name(mb_detect_encoding($result))); + + if(strtolower(substr($result, 0, 14)) === 'setTypeHTML($charset); + elseif(strtolower(substr($result, 0, 5)) === 'setTypeXML($charset); + else + $response->setTypePlain($charset); + } + } + } + + self::output($response->toResponse()); + } + + public function writeErrorPage(HttpResponseBuilder $response, HttpRequest $request, int $statusCode): void { + $response->setStatusCode($statusCode); + $this->errorHandler->handle($response, $request, $response->getStatusCode(), $response->getStatusText()); + } + + public static function output(HttpResponse $response): void { + $version = $response->getHttpVersion(); + header(sprintf( + 'HTTP/%d.%d %03d %s', + $version->getMajor(), + $version->getMinor(), + $response->getStatusCode(), + $response->getStatusText() + )); + + $headers = $response->getHeaders(); + foreach($headers as $header) { + $name = (string)$header->getName(); + $lines = $header->getLines(); + + foreach($lines as $line) + header(sprintf('%s: %s', $name, (string)$line)); + } + + if($response->hasContent()) + echo (string)$response->getContent(); + } }