Add some routing, start working on actually searching webmentions
This commit is contained in:
parent
15d03799bf
commit
45024faa5f
12
index.php
12
index.php
@ -2,10 +2,20 @@
|
||||
|
||||
require_once __DIR__ . "/vendor/autoload.php";
|
||||
|
||||
use Lewisdale\Webmentions\Router\Router;
|
||||
use Lewisdale\Webmentions\Router\Response;
|
||||
use Lewisdale\Webmentions\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();
|
||||
|
||||
?>
|
@ -2,7 +2,11 @@
|
||||
|
||||
namespace Lewisdale\Webmentions;
|
||||
|
||||
use Lewisdale\Webmentions\Exceptions\InvalidTargetException;
|
||||
use Lewisdale\Webmentions\Exceptions\InvalidUrlException;
|
||||
use Lewisdale\Webmentions\Gateways\WebmentionGatewayInterface;
|
||||
use Lewisdale\Webmentions\Models\Webmention;
|
||||
use Symfony\Component\HttpClient\HttpClient;
|
||||
|
||||
class Endpoint {
|
||||
private readonly WebmentionGatewayInterface $gateway;
|
||||
@ -16,19 +20,35 @@ class Endpoint {
|
||||
// Validate that both source and target are actual domains
|
||||
if (!$this->validateUrl($source) || !$this->validateUrl($target))
|
||||
{
|
||||
return;
|
||||
throw new InvalidUrlException();
|
||||
}
|
||||
|
||||
// Validate that the webmention target is a domain I care about
|
||||
if (!str_contains($target, "https://lewisdale.dev")) {
|
||||
// Should throw a Bad Request error
|
||||
return;
|
||||
throw new InvalidTargetException();
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
6
src/Exceptions/InvalidTargetException.php
Normal file
6
src/Exceptions/InvalidTargetException.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Lewisdale\Webmentions\Exceptions;
|
||||
|
||||
class InvalidTargetException extends \Exception
|
||||
{}
|
6
src/Exceptions/InvalidUrlException.php
Normal file
6
src/Exceptions/InvalidUrlException.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Lewisdale\Webmentions\Exceptions;
|
||||
|
||||
class InvalidUrlException extends \Exception
|
||||
{}
|
@ -97,5 +97,21 @@ class SqliteGateway extends WebmentionGatewayInterface {
|
||||
$statement->execute(["id" => $webmention->id]);
|
||||
$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();
|
||||
}
|
||||
}
|
||||
?>
|
@ -10,6 +10,7 @@ abstract class WebmentionGatewayInterface {
|
||||
abstract public function save(Webmention $webmention) : ?int;
|
||||
abstract public function delete(Webmention $webmention) : void;
|
||||
abstract public function get(int $id) : ?Webmention;
|
||||
abstract public function find(array $values) : array;
|
||||
}
|
||||
|
||||
?>
|
11
src/Router/Method.php
Normal file
11
src/Router/Method.php
Normal 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
14
src/Router/Request.php
Normal 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
37
src/Router/Response.php
Normal 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
14
src/Router/Route.php
Normal 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
102
src/Router/Router.php
Normal 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
13
src/Router/StatusCode.php
Normal 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;
|
||||
}
|
||||
|
||||
?>
|
@ -34,8 +34,6 @@ class SqliteGatewayTest extends TestCase
|
||||
|
||||
public function testCanRetrieveAWebmention()
|
||||
{
|
||||
$this->gateway = new SqliteGateway(":memory:");
|
||||
|
||||
$webmention = new Webmention(
|
||||
null,
|
||||
"https://lewisdale.dev/post/a-post",
|
||||
@ -53,8 +51,6 @@ class SqliteGatewayTest extends TestCase
|
||||
|
||||
public function testCanDeleteAWebmention()
|
||||
{
|
||||
$this->gateway = new SqliteGateway(":memory:");
|
||||
|
||||
$webmention = new Webmention(
|
||||
null,
|
||||
"https://lewisdale.dev/post/a-post",
|
||||
@ -73,8 +69,6 @@ class SqliteGatewayTest extends TestCase
|
||||
|
||||
public function testCanGetByPost()
|
||||
{
|
||||
$this->gateway = new SqliteGateway(":memory:");
|
||||
|
||||
foreach(range(0, 4) as $_) {
|
||||
$this->gateway->save(new Webmention(
|
||||
null,
|
||||
@ -99,4 +93,56 @@ class SqliteGatewayTest extends TestCase
|
||||
|
||||
$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"
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user