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": [
|
"packages-dev": [
|
||||||
{
|
{
|
||||||
"name": "myclabs/deep-copy",
|
"name": "myclabs/deep-copy",
|
||||||
"version": "1.11.0",
|
"version": "1.11.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/myclabs/DeepCopy.git",
|
"url": "https://github.com/myclabs/DeepCopy.git",
|
||||||
"reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614"
|
"reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614",
|
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
|
||||||
"reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614",
|
"reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@ -1071,7 +1071,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/myclabs/DeepCopy/issues",
|
"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": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -1079,7 +1079,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2022-03-03T13:19:32+00:00"
|
"time": "2023-03-08T13:26:56+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nikic/php-parser",
|
"name": "nikic/php-parser",
|
||||||
@ -1568,16 +1568,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpunit/phpunit",
|
"name": "phpunit/phpunit",
|
||||||
"version": "10.0.14",
|
"version": "10.0.16",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||||
"reference": "7065dbebcb0f66cf16a45fc9cfc28c2351e06169"
|
"reference": "07d386a11ac7094032900f07cada1c8975d16607"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/7065dbebcb0f66cf16a45fc9cfc28c2351e06169",
|
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/07d386a11ac7094032900f07cada1c8975d16607",
|
||||||
"reference": "7065dbebcb0f66cf16a45fc9cfc28c2351e06169",
|
"reference": "07d386a11ac7094032900f07cada1c8975d16607",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@ -1609,7 +1609,7 @@
|
|||||||
"sebastian/version": "^4.0"
|
"sebastian/version": "^4.0"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
"ext-soap": "*"
|
"ext-soap": "To be able to generate mocks based on WSDL files"
|
||||||
},
|
},
|
||||||
"bin": [
|
"bin": [
|
||||||
"phpunit"
|
"phpunit"
|
||||||
@ -1648,7 +1648,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
"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": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -1664,7 +1664,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2023-03-01T05:37:49+00:00"
|
"time": "2023-03-13T09:02:40+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "sebastian/cli-parser",
|
"name": "sebastian/cli-parser",
|
||||||
|
18
index.php
18
index.php
@ -3,8 +3,11 @@
|
|||||||
require_once __DIR__ . "/vendor/autoload.php";
|
require_once __DIR__ . "/vendor/autoload.php";
|
||||||
|
|
||||||
use Lewisdale\Webmentions\Endpoint;
|
use Lewisdale\Webmentions\Endpoint;
|
||||||
|
use Lewisdale\Webmentions\Exceptions\InvalidSourceException;
|
||||||
use Lewisdale\Webmentions\Exceptions\InvalidTargetException;
|
use Lewisdale\Webmentions\Exceptions\InvalidTargetException;
|
||||||
use Lewisdale\Webmentions\Exceptions\InvalidUrlException;
|
use Lewisdale\Webmentions\Exceptions\InvalidUrlException;
|
||||||
|
use Lewisdale\Webmentions\Exceptions\SourceNotFoundException;
|
||||||
|
use Lewisdale\Webmentions\Exceptions\TargetNotMentionedException;
|
||||||
use Lewisdale\Webmentions\Gateways\SqliteGateway;
|
use Lewisdale\Webmentions\Gateways\SqliteGateway;
|
||||||
use Lewisdale\Webmentions\Router\Request;
|
use Lewisdale\Webmentions\Router\Request;
|
||||||
use Lewisdale\Webmentions\Router\Response;
|
use Lewisdale\Webmentions\Router\Response;
|
||||||
@ -18,7 +21,13 @@ $router = new Router();
|
|||||||
$router->post("/send", function (Request $req, Response $response) {
|
$router->post("/send", function (Request $req, Response $response) {
|
||||||
$mentioner = new Webmention();
|
$mentioner = new Webmention();
|
||||||
$source = $req->query["source"];
|
$source = $req->query["source"];
|
||||||
|
try {
|
||||||
$mentioner->sendForPage($source);
|
$mentioner->sendForPage($source);
|
||||||
|
return;
|
||||||
|
} catch (InvalidSourceException $e) {
|
||||||
|
$response->status_code = StatusCode::BadRequest;
|
||||||
|
return $e->getMessage();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$router->post("/endpoint", function (Request $req, Response $response) {
|
$router->post("/endpoint", function (Request $req, Response $response) {
|
||||||
@ -29,21 +38,22 @@ $router->post("/endpoint", function (Request $req, Response $response) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$endpoint->receiveWebmention($source, $target);
|
$endpoint->receiveWebmention($source, $target);
|
||||||
} catch (InvalidUrlException $e) {
|
} catch (InvalidUrlException) {
|
||||||
$response->status_code = StatusCode::BadRequest;
|
$response->status_code = StatusCode::BadRequest;
|
||||||
return "Source and target must be valid URLs";
|
return "Source and target must be valid URLs";
|
||||||
} catch (InvalidTargetException $e) {
|
} catch (InvalidTargetException) {
|
||||||
$response->status_code = StatusCode::BadRequest;
|
$response->status_code = StatusCode::BadRequest;
|
||||||
return "Target must be on the domain lewisdale.dev";
|
return "Target must be on the domain lewisdale.dev";
|
||||||
} catch (\Lewisdale\Webmentions\Exceptions\SourceNotFoundException $e) {
|
} catch (SourceNotFoundException $e) {
|
||||||
$response->status_code = StatusCode::BadRequest;
|
$response->status_code = StatusCode::BadRequest;
|
||||||
return "Source URL was unreachable";
|
return "Source URL was unreachable";
|
||||||
} catch (\Lewisdale\Webmentions\Exceptions\TargetNotMentionedException) {
|
} catch (TargetNotMentionedException) {
|
||||||
$response->status_code = StatusCode::BadRequest;
|
$response->status_code = StatusCode::BadRequest;
|
||||||
return "Source does not mention the target";
|
return "Source does not mention the target";
|
||||||
}
|
}
|
||||||
|
|
||||||
$response->status_code = StatusCode::Created;
|
$response->status_code = StatusCode::Created;
|
||||||
|
return;
|
||||||
});
|
});
|
||||||
|
|
||||||
$router->get('/', fn($req, $res) => "<h1>Webmention server</h1>");
|
$router->get('/', fn($req, $res) => "<h1>Webmention server</h1>");
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
namespace Lewisdale\Webmentions;
|
namespace Lewisdale\Webmentions;
|
||||||
|
|
||||||
use League\Uri\Exceptions\SyntaxError;
|
|
||||||
use League\Uri\Uri;
|
|
||||||
use Lewisdale\Webmentions\Exceptions\InvalidTargetException;
|
use Lewisdale\Webmentions\Exceptions\InvalidTargetException;
|
||||||
use Lewisdale\Webmentions\Exceptions\InvalidUrlException;
|
use Lewisdale\Webmentions\Exceptions\InvalidUrlException;
|
||||||
use Lewisdale\Webmentions\Exceptions\SourceNotFoundException;
|
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
|
public function receiveWebmention(string $source, string $target): void
|
||||||
{
|
{
|
||||||
// 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 (!Url::validateUrl($source) || !Url::validateUrl($target)) {
|
||||||
throw new InvalidUrlException();
|
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 (!Url::matchesHost($target, "lewisdale.dev")) {
|
||||||
throw new InvalidTargetException();
|
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;
|
namespace Lewisdale\Webmentions;
|
||||||
|
|
||||||
|
use Lewisdale\Webmentions\Exceptions\InvalidSourceException;
|
||||||
use Symfony\Component\DomCrawler\Crawler;
|
use Symfony\Component\DomCrawler\Crawler;
|
||||||
use Symfony\Component\HttpClient\HttpClient;
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
class Webmention {
|
class Webmention
|
||||||
|
{
|
||||||
private readonly HttpClientInterface $client;
|
private readonly HttpClientInterface $client;
|
||||||
|
|
||||||
function __construct()
|
function __construct()
|
||||||
@ -14,7 +16,12 @@ class Webmention {
|
|||||||
$this->client = HttpClient::create();
|
$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);
|
$urls = $this->getUrls($source);
|
||||||
|
|
||||||
foreach ($urls as $target) {
|
foreach ($urls as $target) {
|
||||||
@ -22,7 +29,8 @@ class Webmention {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getUrls(string $url) : array {
|
private function getUrls(string $url): array
|
||||||
|
{
|
||||||
$page = file_get_contents($url);
|
$page = file_get_contents($url);
|
||||||
$doc = new Crawler($page);
|
$doc = new Crawler($page);
|
||||||
|
|
||||||
@ -37,7 +45,8 @@ class Webmention {
|
|||||||
return $urls;
|
return $urls;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function sendWebmention(string $source, string $target) {
|
private function sendWebmention(string $source, string $target)
|
||||||
|
{
|
||||||
$endpoint = $this->getWebmentionEndpoint($target);
|
$endpoint = $this->getWebmentionEndpoint($target);
|
||||||
|
|
||||||
if ($endpoint) {
|
if ($endpoint) {
|
||||||
@ -46,13 +55,14 @@ class Webmention {
|
|||||||
[
|
[
|
||||||
"body" => [
|
"body" => [
|
||||||
"source" => $source,
|
"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);
|
$response = $this->client->request('GET', $url);
|
||||||
return EndpointParser::parse($response);
|
return EndpointParser::parse($response);
|
||||||
}
|
}
|
||||||
|
@ -49,14 +49,6 @@ class EndpointTest extends TestCase
|
|||||||
$endpoint->receiveWebmention($source, $target);
|
$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(["https://my-valid-source.url", "htt://my-invalid-target.com"])]
|
||||||
#[TestWith(["my-invalid-source", "https://my-valid-target.com"])]
|
#[TestWith(["my-invalid-source", "https://my-valid-target.com"])]
|
||||||
#[TestWith(["http:///an-invalid-source", "an-invalid-target"])]
|
#[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