From ec83d67bdedd9c686774ef709e1629fa818286a5 Mon Sep 17 00:00:00 2001 From: Lewis Dale Date: Fri, 17 Mar 2023 10:19:50 +0000 Subject: [PATCH] Move URL into separate file, handle source URL validation for sending webmentions --- composer.lock | 26 +++++++-------- index.php | 20 +++++++++--- src/Endpoint.php | 20 ++---------- src/Exceptions/InvalidSourceException.php | 11 +++++++ src/Url.php | 30 +++++++++++++++++ src/Webmention.php | 40 ++++++++++++++--------- tests/EndpointTest.php | 10 +----- tests/UrlTest.php | 28 ++++++++++++++++ 8 files changed, 125 insertions(+), 60 deletions(-) create mode 100644 src/Exceptions/InvalidSourceException.php create mode 100644 src/Url.php create mode 100644 tests/UrlTest.php diff --git a/composer.lock b/composer.lock index 142251e..dcd7582 100644 --- a/composer.lock +++ b/composer.lock @@ -1024,16 +1024,16 @@ "packages-dev": [ { "name": "myclabs/deep-copy", - "version": "1.11.0", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", "shasum": "" }, "require": { @@ -1071,7 +1071,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" }, "funding": [ { @@ -1079,7 +1079,7 @@ "type": "tidelift" } ], - "time": "2022-03-03T13:19:32+00:00" + "time": "2023-03-08T13:26:56+00:00" }, { "name": "nikic/php-parser", @@ -1568,16 +1568,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.0.14", + "version": "10.0.16", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "7065dbebcb0f66cf16a45fc9cfc28c2351e06169" + "reference": "07d386a11ac7094032900f07cada1c8975d16607" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/7065dbebcb0f66cf16a45fc9cfc28c2351e06169", - "reference": "7065dbebcb0f66cf16a45fc9cfc28c2351e06169", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/07d386a11ac7094032900f07cada1c8975d16607", + "reference": "07d386a11ac7094032900f07cada1c8975d16607", "shasum": "" }, "require": { @@ -1609,7 +1609,7 @@ "sebastian/version": "^4.0" }, "suggest": { - "ext-soap": "*" + "ext-soap": "To be able to generate mocks based on WSDL files" }, "bin": [ "phpunit" @@ -1648,7 +1648,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.0.14" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.0.16" }, "funding": [ { @@ -1664,7 +1664,7 @@ "type": "tidelift" } ], - "time": "2023-03-01T05:37:49+00:00" + "time": "2023-03-13T09:02:40+00:00" }, { "name": "sebastian/cli-parser", diff --git a/index.php b/index.php index 51d3a5d..e64d476 100644 --- a/index.php +++ b/index.php @@ -3,8 +3,11 @@ require_once __DIR__ . "/vendor/autoload.php"; use Lewisdale\Webmentions\Endpoint; +use Lewisdale\Webmentions\Exceptions\InvalidSourceException; use Lewisdale\Webmentions\Exceptions\InvalidTargetException; use Lewisdale\Webmentions\Exceptions\InvalidUrlException; +use Lewisdale\Webmentions\Exceptions\SourceNotFoundException; +use Lewisdale\Webmentions\Exceptions\TargetNotMentionedException; use Lewisdale\Webmentions\Gateways\SqliteGateway; use Lewisdale\Webmentions\Router\Request; use Lewisdale\Webmentions\Router\Response; @@ -18,7 +21,13 @@ $router = new Router(); $router->post("/send", function (Request $req, Response $response) { $mentioner = new Webmention(); $source = $req->query["source"]; - $mentioner->sendForPage($source); + try { + $mentioner->sendForPage($source); + return; + } catch (InvalidSourceException $e) { + $response->status_code = StatusCode::BadRequest; + return $e->getMessage(); + } }); $router->post("/endpoint", function (Request $req, Response $response) { @@ -29,21 +38,22 @@ $router->post("/endpoint", function (Request $req, Response $response) { try { $endpoint->receiveWebmention($source, $target); - } catch (InvalidUrlException $e) { + } catch (InvalidUrlException) { $response->status_code = StatusCode::BadRequest; return "Source and target must be valid URLs"; - } catch (InvalidTargetException $e) { + } catch (InvalidTargetException) { $response->status_code = StatusCode::BadRequest; return "Target must be on the domain lewisdale.dev"; - } catch (\Lewisdale\Webmentions\Exceptions\SourceNotFoundException $e) { + } catch (SourceNotFoundException $e) { $response->status_code = StatusCode::BadRequest; return "Source URL was unreachable"; - } catch (\Lewisdale\Webmentions\Exceptions\TargetNotMentionedException) { + } catch (TargetNotMentionedException) { $response->status_code = StatusCode::BadRequest; return "Source does not mention the target"; } $response->status_code = StatusCode::Created; + return; }); $router->get('/', fn($req, $res) => "

Webmention server

"); diff --git a/src/Endpoint.php b/src/Endpoint.php index bcfd9d6..9966a46 100644 --- a/src/Endpoint.php +++ b/src/Endpoint.php @@ -2,8 +2,6 @@ namespace Lewisdale\Webmentions; -use League\Uri\Exceptions\SyntaxError; -use League\Uri\Uri; use Lewisdale\Webmentions\Exceptions\InvalidTargetException; use Lewisdale\Webmentions\Exceptions\InvalidUrlException; use Lewisdale\Webmentions\Exceptions\SourceNotFoundException; @@ -25,29 +23,15 @@ class Endpoint { } - public function validateUrl(string $url): bool - { - try { - $uri = Uri::createFromString($url); - $scheme = $uri->getScheme(); - $schemeValid = in_array($scheme, ["http", "https"]); - - return $schemeValid && !!filter_var($url, FILTER_VALIDATE_URL); - - } catch (SyntaxError $e) { - return false; - } - } - public function receiveWebmention(string $source, string $target): void { // Validate that both source and target are actual domains - if (!$this->validateUrl($source) || !$this->validateUrl($target)) { + if (!Url::validateUrl($source) || !Url::validateUrl($target)) { throw new InvalidUrlException(); } // Validate that the webmention target is a domain I care about - if (!str_contains($target, "https://lewisdale.dev")) { + if (!Url::matchesHost($target, "lewisdale.dev")) { throw new InvalidTargetException(); } diff --git a/src/Exceptions/InvalidSourceException.php b/src/Exceptions/InvalidSourceException.php new file mode 100644 index 0000000..9b57f3f --- /dev/null +++ b/src/Exceptions/InvalidSourceException.php @@ -0,0 +1,11 @@ +getHost() === $host; + } + + public static function validateUrl(string $url): bool + { + try { + $uri = Uri::createFromString($url); + $scheme = $uri->getScheme(); + $schemeValid = in_array($scheme, ["http", "https"]); + [, $tld] = explode(".", $uri->getHost() ?? ""); + + return !empty($tld) && $schemeValid && !!filter_var($url, FILTER_VALIDATE_URL); + + } catch (SyntaxError) { + return false; + } + } +} \ No newline at end of file diff --git a/src/Webmention.php b/src/Webmention.php index 078550a..82a0f64 100644 --- a/src/Webmention.php +++ b/src/Webmention.php @@ -2,11 +2,13 @@ namespace Lewisdale\Webmentions; +use Lewisdale\Webmentions\Exceptions\InvalidSourceException; use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\HttpClient\HttpClient; use Symfony\Contracts\HttpClient\HttpClientInterface; -class Webmention { +class Webmention +{ private readonly HttpClientInterface $client; function __construct() @@ -14,15 +16,21 @@ class Webmention { $this->client = HttpClient::create(); } - public function sendForPage(string $source) : void { + public function sendForPage(string $source): void + { + if (!Url::matchesHost($source, "lewisdale.dev")) { + throw new InvalidSourceException(); + } + $urls = $this->getUrls($source); - foreach($urls as $target) { + foreach ($urls as $target) { $this->sendWebmention($source, $target); } } - private function getUrls(string $url) : array { + private function getUrls(string $url): array + { $page = file_get_contents($url); $doc = new Crawler($page); @@ -31,28 +39,30 @@ class Webmention { $target_url = $anchor->attributes->getNamedItem('href')->textContent; if ($target_url !== null && strlen($target_url)) { - $urls[] = $target_url; - } + $urls[] = $target_url; + } } return $urls; } - private function sendWebmention(string $source, string $target) { + private function sendWebmention(string $source, string $target) + { $endpoint = $this->getWebmentionEndpoint($target); - + if ($endpoint) { echo "Sending for " . $endpoint . "
"; $this->client->request('POST', $endpoint, - [ - "body" => [ - "source" => $source, - "target" => $target - ] - ]); + [ + "body" => [ + "source" => $source, + "target" => $target, + ], + ]); } } - private function getWebmentionEndpoint(string $url) : string | null { + private function getWebmentionEndpoint(string $url): string|null + { $response = $this->client->request('GET', $url); return EndpointParser::parse($response); } diff --git a/tests/EndpointTest.php b/tests/EndpointTest.php index ceac350..b48a601 100644 --- a/tests/EndpointTest.php +++ b/tests/EndpointTest.php @@ -48,15 +48,7 @@ class EndpointTest extends TestCase $endpoint = new Endpoint($this->mockClient, $this->mockGateway); $endpoint->receiveWebmention($source, $target); } - - #[TestWith(["https://my.url.com", true])] - #[TestWith(["my.url.com", false])] - public function testValidatesUrls(string $url, bool $expected) - { - $endpoint = new Endpoint($this->mockClient, $this->mockGateway); - $this->assertEquals($expected, $endpoint->validateUrl($url), "Expected $url"); - } - + #[TestWith(["https://my-valid-source.url", "htt://my-invalid-target.com"])] #[TestWith(["my-invalid-source", "https://my-valid-target.com"])] #[TestWith(["http:///an-invalid-source", "an-invalid-target"])] diff --git a/tests/UrlTest.php b/tests/UrlTest.php new file mode 100644 index 0000000..cb0df0b --- /dev/null +++ b/tests/UrlTest.php @@ -0,0 +1,28 @@ +assertEquals($expected, Url::matchesHost($source, $target, $expected)); + } + + #[TestWith(["http://a-valid-http-url.com", true])] + #[TestWith(["ht://an-invalid-url.com", false])] + #[TestWith(["https://no-tld./", false])] + #[TestWith(["https://a-valid-https.com", true])] + #[TestWith(["ftp://wrong-protocol.com", false])] + public function testItValidatesAUrl(string $url, bool $expected) + { + $this->assertEquals($expected, Url::validateUrl($url)); + } +}