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();
+ }
}