Move URL into separate file, handle source URL validation for sending webmentions
This commit is contained in:
parent
fca08de828
commit
ec83d67bde
26
composer.lock
generated
26
composer.lock
generated
@ -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",
|
||||
|
18
index.php
18
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"];
|
||||
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) => "<h1>Webmention server</h1>");
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
11
src/Exceptions/InvalidSourceException.php
Normal file
11
src/Exceptions/InvalidSourceException.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Lewisdale\Webmentions\Exceptions;
|
||||
|
||||
class InvalidSourceException extends \Exception
|
||||
{
|
||||
function __construct()
|
||||
{
|
||||
parent::__construct("Source url is invalid");
|
||||
}
|
||||
}
|
30
src/Url.php
Normal file
30
src/Url.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Lewisdale\Webmentions;
|
||||
|
||||
use League\Uri\Exceptions\SyntaxError;
|
||||
use League\Uri\Uri;
|
||||
|
||||
class Url
|
||||
{
|
||||
public static function matchesHost(string $source, string $host): bool
|
||||
{
|
||||
$uri = Uri::createFromString($source);
|
||||
return $uri->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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
@ -37,7 +45,8 @@ class Webmention {
|
||||
return $urls;
|
||||
}
|
||||
|
||||
private function sendWebmention(string $source, string $target) {
|
||||
private function sendWebmention(string $source, string $target)
|
||||
{
|
||||
$endpoint = $this->getWebmentionEndpoint($target);
|
||||
|
||||
if ($endpoint) {
|
||||
@ -46,13 +55,14 @@ class Webmention {
|
||||
[
|
||||
"body" => [
|
||||
"source" => $source,
|
||||
"target" => $target
|
||||
]
|
||||
"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);
|
||||
}
|
||||
|
@ -49,14 +49,6 @@ class EndpointTest extends TestCase
|
||||
$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"])]
|
||||
|
28
tests/UrlTest.php
Normal file
28
tests/UrlTest.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
|
||||
use Lewisdale\Webmentions\Url;
|
||||
use PHPUnit\Framework\Attributes\TestWith;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class UrlTest extends TestCase
|
||||
{
|
||||
|
||||
#[TestWith(["https://lewisdale.dev/post/a-post", "lewisdale.dev", true])]
|
||||
#[TestWith(["https://css-tricks.com/test/host", "lewisdale.dev", false])]
|
||||
#[TestWith(["ht:/lewisdale.dev/not-a-valid-url", "lewisdale.dev", false])]
|
||||
public function testItValidatesAHost(string $source, string $target, bool $expected)
|
||||
{
|
||||
$this->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));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user