Add some routing, start working on actually searching webmentions

This commit is contained in:
Lewis Dale 2023-03-09 21:50:59 +00:00
parent 15d03799bf
commit 45024faa5f
13 changed files with 308 additions and 12 deletions

View File

@ -2,10 +2,20 @@
require_once __DIR__ . "/vendor/autoload.php"; require_once __DIR__ . "/vendor/autoload.php";
use Lewisdale\Webmentions\Router\Router;
use Lewisdale\Webmentions\Router\Response;
use Lewisdale\Webmentions\Webmention; use Lewisdale\Webmentions\Webmention;
$mentioner = new Webmention(); $mentioner = new Webmention();
$mentioner->sendForPage("https://lewisdale.dev/post/bringing-my-omg-lol-now-page-into-eleventy/"); // $mentioner->sendForPage("https://lewisdale.dev/post/bringing-my-omg-lol-now-page-into-eleventy/");
$router = new Router();
$router->get("/send", function($req, Response $response) {
return "<h1>Hello world</h1>";
});
$router->dispatch();
?> ?>

View File

@ -2,7 +2,11 @@
namespace Lewisdale\Webmentions; namespace Lewisdale\Webmentions;
use Lewisdale\Webmentions\Exceptions\InvalidTargetException;
use Lewisdale\Webmentions\Exceptions\InvalidUrlException;
use Lewisdale\Webmentions\Gateways\WebmentionGatewayInterface; use Lewisdale\Webmentions\Gateways\WebmentionGatewayInterface;
use Lewisdale\Webmentions\Models\Webmention;
use Symfony\Component\HttpClient\HttpClient;
class Endpoint { class Endpoint {
private readonly WebmentionGatewayInterface $gateway; private readonly WebmentionGatewayInterface $gateway;
@ -16,19 +20,35 @@ class Endpoint {
// Validate that both source and target are actual domains // Validate that both source and target are actual domains
if (!$this->validateUrl($source) || !$this->validateUrl($target)) if (!$this->validateUrl($source) || !$this->validateUrl($target))
{ {
return; throw new InvalidUrlException();
} }
// Validate that the webmention target is a domain I care about // Validate that the webmention target is a domain I care about
if (!str_contains($target, "https://lewisdale.dev")) { if (!str_contains($target, "https://lewisdale.dev")) {
// Should throw a Bad Request error throw new InvalidTargetException();
return;
} }
// Parse content from the source // Parse content from the source
$client = HttpClient::create();
$response = $client->request('GET', $source);
// Store the webmention if ($response->getStatusCode() === 200)
{
$content = $this->parseContent($response->getContent());
$author = $this->parseAuthor($response->getContent());
// Send the appropriate response $webmention = new Webmention(null, $target, $source, $content, $author);
$this->gateway->save($webmention);
}
}
private function parseContent(string $content) : ?string
{
return null;
}
private function parseAuthor(string $author) : ?string
{
return null;
} }
} }

View File

@ -0,0 +1,6 @@
<?php declare(strict_types=1);
namespace Lewisdale\Webmentions\Exceptions;
class InvalidTargetException extends \Exception
{}

View File

@ -0,0 +1,6 @@
<?php declare(strict_types=1);
namespace Lewisdale\Webmentions\Exceptions;
class InvalidUrlException extends \Exception
{}

View File

@ -97,5 +97,21 @@ class SqliteGateway extends WebmentionGatewayInterface {
$statement->execute(["id" => $webmention->id]); $statement->execute(["id" => $webmention->id]);
$statement->closeCursor(); $statement->closeCursor();
} }
public function find(array $values) : array
{
$keys = implode(" AND ", array_map(function($v) {
return "$v=:$v";
}, array_keys($values)));
$sql = <<<SQL
SELECT * FROM webmentions
WHERE {$keys}
SQL;
$statement = $this->connection->prepare($sql);
$statement->execute($values);
return $statement->fetchAll();
}
} }
?> ?>

View File

@ -10,6 +10,7 @@ abstract class WebmentionGatewayInterface {
abstract public function save(Webmention $webmention) : ?int; abstract public function save(Webmention $webmention) : ?int;
abstract public function delete(Webmention $webmention) : void; abstract public function delete(Webmention $webmention) : void;
abstract public function get(int $id) : ?Webmention; abstract public function get(int $id) : ?Webmention;
abstract public function find(array $values) : array;
} }
?> ?>

11
src/Router/Method.php Normal file
View File

@ -0,0 +1,11 @@
<?php declare(strict_types=1);
namespace Lewisdale\Webmentions\Router;
enum Method {
case GET;
case POST;
// Fallback method
case UNKNOWN;
};

14
src/Router/Request.php Normal file
View File

@ -0,0 +1,14 @@
<?php declare(strict_types=1);
namespace Lewisdale\Webmentions\Router;
class Request {
public function __construct(
public array $params,
public readonly string $uri,
public readonly Method $method,
public readonly array $post = [],
) {}
}
?>

37
src/Router/Response.php Normal file
View File

@ -0,0 +1,37 @@
<?php declare(strict_types=1);
namespace Lewisdale\Webmentions\Router;
class Response {
public StatusCode $status_code = StatusCode::Ok;
public string $body = "";
public function __construct()
{
}
public function json(mixed $obj): string {
return json_encode($obj);
}
/**
* Redirect the user to a new page.
*
* Unlike other response methods, this is evaulated immediately, and takes precedence over other routes.
*
* @param url - the URL to redirect to
*/
public function redirect(string $url) {
header("Location: $url", true, 301);
die();
}
// /**
// * Render a template, with an array of values to provide to the template
// */
// public function render(string $template, array $values = []) {
// $view = new View($template);
// return $view->render($values);
// }
}

14
src/Router/Route.php Normal file
View File

@ -0,0 +1,14 @@
<?php declare(strict_types=1);
namespace Lewisdale\Webmentions\Router;
class Route {
public function __construct(
public readonly Method $type,
public readonly string $pattern,
public readonly mixed $fn
)
{}
}
?>

102
src/Router/Router.php Normal file
View File

@ -0,0 +1,102 @@
<?php declare(strict_types=1);
namespace Lewisdale\Webmentions\Router;
class Router {
private array $routes;
private function trim_route(string $route): string {
$route = $route == "/" ? $route : rtrim($route, "/");
$route = str_replace("/", "\/", $route);
return $route;
}
private function format_route(string $route): string {
$formatted = $this->trim_route($route);
return "#^$formatted$#";
}
public function get(string $route, callable $fn) {
$this->routes[] = new Route(Method::GET, $this->format_route($route), $fn);
}
public function post(string $route, callable $fn) {
$this->routes[] = new Route(Method::POST, $this->format_route($route), $fn);
}
private function method_to_enum(string $method): Method {
switch ($method) {
case "GET":
return Method::GET;
case "POST":
return Method::POST;
default:
return Method::UNKNOWN;
}
}
private function get_params(array $matches): array {
array_shift($matches);
$results = array();
foreach ($matches as $match) {
$results[] = $match[0];
}
return $results;
}
public function dispatch() {
$uri = $_SERVER['REQUEST_URI'];
$method = $this->method_to_enum($_SERVER['REQUEST_METHOD']);
$num_matched = 0;
$response = new Response();
foreach($this->routes as $route) {
if ($method == $route->type) {
$matches = array();
if (preg_match_all($route->pattern, $uri, $matches)) {
$num_matched++;
$fn = $route->fn;
$params = $this->get_params($matches);
$response->status_code = StatusCode::Ok;
$response->body .= $fn(new Request($params, $uri, $method, $_POST), $response);
}
}
}
if (!$num_matched) {
// Handle 404
$response->status_code = StatusCode::NotFound;
}
$this->respond($response);
}
private function map_status_code(StatusCode $code): int {
switch ($code) {
case StatusCode::NotFound:
return 404;
case StatusCode::Ok:
return 200;
case StatusCode::InternalError:
return 500;
case StatusCode::Redirect:
return 300;
case StatusCode::BadRequest:
return 400;
default:
return 501;
}
}
private function respond(Response $response) {
http_response_code($this->map_status_code($response->status_code));
echo $response->body;
}
}
?>

13
src/Router/StatusCode.php Normal file
View File

@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace Lewisdale\Webmentions\Router;
enum StatusCode {
case NotFound;
case Ok;
case Redirect;
case InternalError;
case BadRequest;
}
?>

View File

@ -34,8 +34,6 @@ class SqliteGatewayTest extends TestCase
public function testCanRetrieveAWebmention() public function testCanRetrieveAWebmention()
{ {
$this->gateway = new SqliteGateway(":memory:");
$webmention = new Webmention( $webmention = new Webmention(
null, null,
"https://lewisdale.dev/post/a-post", "https://lewisdale.dev/post/a-post",
@ -53,8 +51,6 @@ class SqliteGatewayTest extends TestCase
public function testCanDeleteAWebmention() public function testCanDeleteAWebmention()
{ {
$this->gateway = new SqliteGateway(":memory:");
$webmention = new Webmention( $webmention = new Webmention(
null, null,
"https://lewisdale.dev/post/a-post", "https://lewisdale.dev/post/a-post",
@ -73,8 +69,6 @@ class SqliteGatewayTest extends TestCase
public function testCanGetByPost() public function testCanGetByPost()
{ {
$this->gateway = new SqliteGateway(":memory:");
foreach(range(0, 4) as $_) { foreach(range(0, 4) as $_) {
$this->gateway->save(new Webmention( $this->gateway->save(new Webmention(
null, null,
@ -99,4 +93,56 @@ class SqliteGatewayTest extends TestCase
$this->assertCount(5, $mentions); $this->assertCount(5, $mentions);
} }
public function testCanFindByParams()
{
$this->gateway->save(new Webmention(
null,
"https://lewisdale.dev/post/a-new-post",
"https://a-source.url",
"No content",
"Some Author Name"
));
$this->gateway->save(new Webmention(
null,
"https://lewisdale.dev/post/a-new-post",
"https://a-different-source.url",
"No content",
"Some Author Name"
));
$this->gateway->save(new Webmention(
null,
"https://lewisdale.dev/post/a-new-post",
"https://a-source.url",
"Some content",
"Some Author Name"
));
$this->assertCount(
2,
$this->gateway->find([
"target" => "https://lewisdale.dev/post/a-new-post",
"source" => "https://a-source.url"
])
);
$this->assertCount(
1,
$this->gateway->find([
"target" => "https://lewisdale.dev/post/a-new-post",
"source" => "https://a-different-source.url"
])
);
$this->assertCount(
1,
$this->gateway->find([
"target" => "https://lewisdale.dev/post/a-new-post",
"source" => "https://a-source.url",
"content" => "Some content"
])
);
}
} }