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));
+ }
+}