aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorwinter2024-12-06 20:43:26 +0000
committerwinter2024-12-06 20:43:26 +0000
commite1ad307b531c27ec6bfaf8b4d018991b0d4a78f3 (patch)
tree4ef08af2b4051d433bb929d5162474ac6777cab1
initial commit
hardcoded functionality mostly!
-rw-r--r--WpfTest/GlobalConfig.php25
-rw-r--r--WpfTest/HttpReturnStatus.php7
-rw-r--r--WpfTest/HttpReturnStatus/BadRequest.php18
-rw-r--r--WpfTest/HttpReturnStatus/InternalServerError.php16
-rw-r--r--WpfTest/HttpReturnStatus/NoHandlerFound.php18
-rw-r--r--WpfTest/HttpReturnStatus/NotFound.php18
-rw-r--r--WpfTest/Router.php101
-rw-r--r--index.php17
-rw-r--r--misc/json_response.php11
-rw-r--r--misc/path_to_uri.php16
-rw-r--r--routes/actor.php39
-rw-r--r--routes/homepage.php12
-rw-r--r--routes/nodeinfo.php42
-rw-r--r--routes/note.php20
-rw-r--r--routes/webfinger.php34
15 files changed, 394 insertions, 0 deletions
diff --git a/WpfTest/GlobalConfig.php b/WpfTest/GlobalConfig.php
new file mode 100644
index 0000000..4bbfe68
--- /dev/null
+++ b/WpfTest/GlobalConfig.php
@@ -0,0 +1,25 @@
+<?php
+namespace WpfTest;
+
+class GlobalConfig {
+ private static GlobalConfig $instance;
+
+ private function __construct() {
+
+ }
+
+ public static function getInstance() {
+ if (!isset(self::$instance)) {
+ self::$instance = new self();
+ }
+ return self::$instance;
+ }
+
+ public function getCanonicalHost(): string {
+ return $_SERVER['HTTP_HOST']; // for now this is fine
+ }
+
+ public function getBasePath(): string {
+ return 'http://' . $this->getCanonicalHost(); // fix this?
+ }
+} \ No newline at end of file
diff --git a/WpfTest/HttpReturnStatus.php b/WpfTest/HttpReturnStatus.php
new file mode 100644
index 0000000..60622bb
--- /dev/null
+++ b/WpfTest/HttpReturnStatus.php
@@ -0,0 +1,7 @@
+<?php
+namespace WpfTest;
+
+interface HttpReturnStatus extends \Throwable {
+ public function httpCode(): int;
+ public function httpReason(): string;
+} \ No newline at end of file
diff --git a/WpfTest/HttpReturnStatus/BadRequest.php b/WpfTest/HttpReturnStatus/BadRequest.php
new file mode 100644
index 0000000..83b5011
--- /dev/null
+++ b/WpfTest/HttpReturnStatus/BadRequest.php
@@ -0,0 +1,18 @@
+<?php
+namespace WpfTest\HttpReturnStatus;
+
+use WpfTest\HttpReturnStatus;
+
+class BadRequest extends \Exception implements HttpReturnStatus {
+ public function __construct(?string $reason) {
+ $this->message = $reason ?? 'Request does not conform to expectations';
+ }
+
+ public function httpCode(): int {
+ return 400;
+ }
+
+ public function httpReason(): string {
+ return "Bad Request";
+ }
+} \ No newline at end of file
diff --git a/WpfTest/HttpReturnStatus/InternalServerError.php b/WpfTest/HttpReturnStatus/InternalServerError.php
new file mode 100644
index 0000000..0141e26
--- /dev/null
+++ b/WpfTest/HttpReturnStatus/InternalServerError.php
@@ -0,0 +1,16 @@
+<?php
+namespace WpfTest\HttpReturnStatus;
+
+use WpfTest\HttpReturnStatus;
+
+class InternalServerError extends \Exception implements HttpReturnStatus {
+ public function __construct(\Exception $reason) {
+ $this->message = $reason->getMessage();
+ }
+ public function httpCode(): int {
+ return 500;
+ }
+ public function httpReason(): string {
+ return "Internal Server Error";
+ }
+} \ No newline at end of file
diff --git a/WpfTest/HttpReturnStatus/NoHandlerFound.php b/WpfTest/HttpReturnStatus/NoHandlerFound.php
new file mode 100644
index 0000000..d914ca2
--- /dev/null
+++ b/WpfTest/HttpReturnStatus/NoHandlerFound.php
@@ -0,0 +1,18 @@
+<?php
+namespace WpfTest\HttpReturnStatus;
+
+use WpfTest\HttpReturnStatus;
+
+class NoHandlerFound extends \Exception implements HttpReturnStatus {
+ public function __construct(string $path) {
+ $this->message = "No handler found for path $path";
+ }
+
+ public function httpCode(): int {
+ return 404;
+ }
+
+ public function httpReason(): string {
+ return "Not Found";
+ }
+} \ No newline at end of file
diff --git a/WpfTest/HttpReturnStatus/NotFound.php b/WpfTest/HttpReturnStatus/NotFound.php
new file mode 100644
index 0000000..eef23b3
--- /dev/null
+++ b/WpfTest/HttpReturnStatus/NotFound.php
@@ -0,0 +1,18 @@
+<?php
+namespace WpfTest\HttpReturnStatus;
+
+use WpfTest\HttpReturnStatus;
+
+class NotFound extends \Exception implements HttpReturnStatus {
+ public function __construct(?string $reason) {
+ $this->message = $reason ?? 'Request was handled but no appropriate resource found';
+ }
+
+ public function httpCode(): int {
+ return 404;
+ }
+
+ public function httpReason(): string {
+ return "Not Found";
+ }
+} \ No newline at end of file
diff --git a/WpfTest/Router.php b/WpfTest/Router.php
new file mode 100644
index 0000000..ea71657
--- /dev/null
+++ b/WpfTest/Router.php
@@ -0,0 +1,101 @@
+<?php
+namespace WpfTest;
+
+use WpfTest\HttpReturnStatus\InternalServerError;
+use WpfTest\HttpReturnStatus\NoHandlerFound;
+
+class Router {
+ private static Router $instance;
+
+ private readonly string $requestPath;
+ private array $routes;
+
+ private function __construct() {
+ $this->requestPath = $_GET['requestPath'] ?? $_SERVER['PATH_INFO'] ?? explode('?', $_SERVER['REQUEST_URI'], 2)[0] ?? '/';
+ $this->routes = [];
+ }
+
+ /**
+ * @return Router the singleton instance
+ */
+ public static function getInstance() {
+ if (!isset(self::$instance)) {
+ self::$instance = new self();
+ }
+ return self::$instance;
+ }
+
+ public function processRequest() {
+ try {
+ $this->processRequestInner();
+ } catch (HttpReturnStatus $e) {
+ http_response_code($e->httpCode());
+ echo '<h1>' . $e->httpReason() . '</h1><pre>' . $e->getMessage() . '</pre>';
+ }
+ }
+
+ private function processRequestInner() {
+ try {
+ $route = $this->matchPath($this->requestPath);
+ if ($route == null) {
+ throw new NoHandlerFound($this->requestPath);
+ }
+ $route[1]($route[2]); // call handler with args
+ } catch (\Exception $e) {
+ if (is_a($e, HttpReturnStatus::class)) {
+ throw $e;
+ }
+ throw new InternalServerError($e);
+ }
+ }
+
+ public function mount(string $pathSpec, callable $handler) {
+ $this->routes[] = [$this->explodePathPieces($pathSpec), $handler];
+ }
+
+ private function explodePathPieces(string $pathSpec): array {
+ $pieces = explode('/', $pathSpec);
+ if ($pieces[0] == '')
+ array_shift($pieces);
+ return $pieces;
+ }
+
+ private function matchPath(string $requestPath): ?array {
+ // find matching candidates
+ $pieces = $this->explodePathPieces($requestPath);
+ $candidates = [];
+ foreach ($this->routes as $route) {
+ $routePieces = $route[0];
+ if (count($routePieces) != count($pieces))
+ continue;
+ $args = [];
+ $matches = true;
+ for ($i = 0; $i < count($pieces); $i++) {
+ if (str_starts_with($routePieces[$i], ':')) { // this is a parameter
+ $args[substr($routePieces[$i], 1)] = $pieces[$i];
+ } elseif ($routePieces[$i] != $pieces[$i]) {
+ $matches = false;
+ break;
+ }
+ }
+ if ($matches) {
+ $candidates[] = [$route[0], $route[1], $args];
+ }
+ }
+
+ // select the best matching one (has the longest path, for now)
+ $bestScore = 0;
+ $bestItem = null;
+ foreach ($candidates as $c) {
+ $score = count($c[0]) - count(array_filter($c[0], function ($item) {
+ return str_starts_with($item, ':');
+ })) * 0.1;
+ if ($score > $bestScore) {
+ $bestItem = $c;
+ $bestScore = $score;
+ }
+ }
+ return $bestItem;
+ }
+
+}
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..54f1043
--- /dev/null
+++ b/index.php
@@ -0,0 +1,17 @@
+<?php
+
+spl_autoload_register(function ($class) {
+ $file = str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php';
+ require $file;
+});
+
+function autoload_glob(string $pattern) {
+ foreach (glob($pattern) as $filename) {
+ require $filename;
+ }
+}
+
+autoload_glob('routes/*.php');
+autoload_glob('misc/*.php');
+
+WpfTest\Router::getInstance()->processRequest();
diff --git a/misc/json_response.php b/misc/json_response.php
new file mode 100644
index 0000000..4244b64
--- /dev/null
+++ b/misc/json_response.php
@@ -0,0 +1,11 @@
+<?php
+
+function json_response(
+ mixed $value,
+ string $contentType = 'application/json',
+ int $flags = 0,
+ int $depth = 512
+) {
+ header("Content-Type: $contentType");
+ echo json_encode($value, $flags, $depth);
+} \ No newline at end of file
diff --git a/misc/path_to_uri.php b/misc/path_to_uri.php
new file mode 100644
index 0000000..756640e
--- /dev/null
+++ b/misc/path_to_uri.php
@@ -0,0 +1,16 @@
+<?php
+
+use WpfTest\GlobalConfig;
+
+
+/**
+ * Converts an absolute path into an absolute URI based on the instance's base path
+ * @param string $path absolute path (i.e. must start with a `/`)
+ * @return string absolute URI
+ */
+function path_to_uri(string $path): string {
+ if (!str_starts_with($path, '/')) {
+ throw new InvalidArgumentException('path must be absolute (start with a `/`');
+ }
+ return GlobalConfig::getInstance()->getBasePath() . $path;
+} \ No newline at end of file
diff --git a/routes/actor.php b/routes/actor.php
new file mode 100644
index 0000000..230f342
--- /dev/null
+++ b/routes/actor.php
@@ -0,0 +1,39 @@
+<?php
+
+use WpfTest\HttpReturnStatus\NotFound;
+use WpfTest\Router;
+
+Router::getInstance()->mount('/user/:username', function (array $args) {
+ if ($args['username'] != 'winter') {
+ throw new NotFound('i only know about winter');
+ }
+ json_response([ // hardcoded much for testing
+ 'type' => 'actor',
+ 'self' => path_to_uri('/user/winter'),
+ 'created' => '2024-12-06T19:40:00+00:00',
+ 'homepage' => path_to_uri('/winter'),
+ 'handle' => 'winter',
+ 'displayName' => 'winter!',
+ 'bio' => 'winter on php',
+ 'pronouns' => 'it/she',
+ 'automated' => false,
+ 'endpoints' => [
+ 'basicFeed' => path_to_uri('/user/winter/basicFeed')
+ ]
+ ]);
+});
+
+Router::getInstance()->mount('/user/:username/basicFeed', function (array $args) {
+ if ($args['username'] != 'winter') {
+ throw new NotFound('i only know about winter');
+ }
+ json_response([
+ 'page' => 1,
+ 'totalPages' => 1,
+ 'nextPage' => null,
+ 'previousPage' => null,
+ 'items' => [
+ path_to_uri('/post/1')
+ ]
+ ]);
+}); \ No newline at end of file
diff --git a/routes/homepage.php b/routes/homepage.php
new file mode 100644
index 0000000..ea5d31b
--- /dev/null
+++ b/routes/homepage.php
@@ -0,0 +1,12 @@
+<?php
+
+use WpfTest\Router;
+
+Router::getInstance()->mount('/', function (array $args) {
+ echo '<h1>home</h1>';
+});
+
+Router::getInstance()->mount('/:username', function (array $args) { ?>
+ <h1><?= $args['username'] ?></h1>
+ <p>todo!</p>
+<?php }); \ No newline at end of file
diff --git a/routes/nodeinfo.php b/routes/nodeinfo.php
new file mode 100644
index 0000000..b507849
--- /dev/null
+++ b/routes/nodeinfo.php
@@ -0,0 +1,42 @@
+<?php
+
+use WpfTest\Router;
+
+Router::getInstance()->mount('/.well-known/nodeinfo', function (array $args) {
+ json_response([
+ 'links' => [
+ [
+ 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.1',
+ 'href' => path_to_uri('/nodeinfo/2.1')
+ ]
+ ]
+ ], 'application/jrd+json');
+});
+
+Router::getInstance()->mount('/nodeinfo/2.1', function (array $args) {
+ json_response([
+ 'version' => '2.1',
+ 'software' => [
+ 'name' => 'WPF test server',
+ 'version' => '0.1.0'
+ ],
+ 'protocols' => ['wpf'],
+ 'services' => [
+ 'inbound' => [],
+ 'outbound' => []
+ ],
+ 'openRegistrations' => false,
+ 'usage' => [
+ 'users' => [
+ 'total' => 1
+ ]
+ ],
+ 'metadata' => [
+ 'nodeName' => "winter's pawsome test server",
+ 'nodeDescription' => 'written in php i dunno',
+ 'wpfExtensions' => [
+ //
+ ]
+ ]
+ ], 'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.1#"');
+}); \ No newline at end of file
diff --git a/routes/note.php b/routes/note.php
new file mode 100644
index 0000000..9c5e507
--- /dev/null
+++ b/routes/note.php
@@ -0,0 +1,20 @@
+<?php
+
+use WpfTest\Router;
+
+Router::getInstance()->mount('/post/:id', function (array $args) {
+ json_response([
+ 'type' => 'note',
+ 'self' => path_to_uri('/post/' . $args['id']),
+ 'created' => '2024-12-06T20:14:00+00:00',
+ 'author' => path_to_uri('/user/winter'),
+ 'plainContent' => 'meow :3',
+ 'language' => 'eng',
+ 'inReplyTo' => null,
+ 'threadApex' => path_to_uri('/post/' . $args['id']),
+ 'privacy' => [
+ 'scope' => 'public',
+ 'indexable' => true
+ ]
+ ]);
+});
diff --git a/routes/webfinger.php b/routes/webfinger.php
new file mode 100644
index 0000000..5994529
--- /dev/null
+++ b/routes/webfinger.php
@@ -0,0 +1,34 @@
+<?php
+
+use WpfTest\GlobalConfig;
+use WpfTest\HttpReturnStatus\BadRequest;
+use WpfTest\HttpReturnStatus\NotFound;
+use WpfTest\Router;
+
+Router::getInstance()->mount('/.well-known/webfinger', function (array $args) {
+ if (!isset($_GET['resource'])) {
+ throw new BadRequest('parameter `resource` is required');
+ }
+ $resource = $_GET['resource'];
+ if (!str_starts_with($resource, 'acct:')) {
+ throw new BadRequest('only acct: URIs are supported');
+ }
+ $handle = substr($resource, 5);
+ [$localPart, $remotePart] = explode('@', $handle, 2);
+ if ($remotePart != GlobalConfig::getInstance()->getCanonicalHost()) {
+ throw new NotFound('i only know about local accounts');
+ }
+ if ($localPart != 'winter') {
+ throw new NotFound('i only know about winter!!!');
+ }
+ json_response([
+ 'subject' => "acct:$localPart@$remotePart",
+ 'links' => [
+ [
+ 'rel' => 'urn:example:wpf:actor',
+ 'href' => path_to_uri('/user/winter'),
+ 'type' => 'application/json'
+ ]
+ ]
+ ]);
+}); \ No newline at end of file