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