diff --git a/src/Endpoint.php b/src/Endpoint.php index efe961f..60865d9 100644 --- a/src/Endpoint.php +++ b/src/Endpoint.php @@ -2,19 +2,39 @@ namespace Lewisdale\Webmentions; +use League\Uri\Exceptions\SyntaxError; 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; +use League\Uri\Uri; +use Lewisdale\Webmentions\Exceptions\SourceNotFoundException; +use Lewisdale\Webmentions\Exceptions\TargetNotMentionedException; +use Symfony\Component\DomCrawler\Crawler; +use Symfony\Contracts\HttpClient\HttpClientInterface; class Endpoint { private readonly WebmentionGatewayInterface $gateway; - public function validateUrl(string $url) : bool { - return !!filter_var($url, FILTER_VALIDATE_URL); - } + function __construct( + private readonly HttpClientInterface $httpClient + ) + {} + 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 @@ -29,21 +49,29 @@ class Endpoint { } // Parse content from the source - $client = HttpClient::create(); - $response = $client->request('GET', $source); + $response = $this->httpClient->request('GET', $source); - if ($response->getStatusCode() === 200) + if ($response->getStatusCode() < 400) { - $content = $this->parseContent($response->getContent()); + $content = $this->parseContent($response->getContent(), $target); $author = $this->parseAuthor($response->getContent()); $webmention = new Webmention(null, $target, $source, $content, $author); $this->gateway->save($webmention); + } else { + throw new SourceNotFoundException(); } } - private function parseContent(string $content) : ?string + private function parseContent(string $content, string $target) : ?string { + $body = new Crawler($content); + $anchors = $body->filter('a[href="'. $target . '"]'); + + if (!$anchors->count()) { + throw new TargetNotMentionedException(); + } + return null; } diff --git a/src/Exceptions/SourceNotFoundException.php b/src/Exceptions/SourceNotFoundException.php new file mode 100644 index 0000000..889a24f --- /dev/null +++ b/src/Exceptions/SourceNotFoundException.php @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/src/Exceptions/TargetNotMentionedException.php b/src/Exceptions/TargetNotMentionedException.php new file mode 100644 index 0000000..5a38d3d --- /dev/null +++ b/src/Exceptions/TargetNotMentionedException.php @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/tests/EndpointTest.php b/tests/EndpointTest.php index 778ef9f..52496de 100644 --- a/tests/EndpointTest.php +++ b/tests/EndpointTest.php @@ -1,16 +1,105 @@ createMock(HttpClientInterface::class)); $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"])] + public function testThrowsInvalidUrlExceptionIfTheUrlIsInvalid(string $source, string $target) + { + $this->expectException(InvalidUrlException::class); + $endpoint = new Endpoint($this->createMock(HttpClientInterface::class)); + $endpoint->receiveWebmention($source, $target); + } + + public function testThrowsInvalidTargetExceptionIfTheTargetUrlIsNotOnCurrentSite() + { + $this->expectException(InvalidTargetException::class); + $endpoint = new Endpoint($this->createMock(HttpClientInterface::class)); + $endpoint->receiveWebmention("https://a-valid-source.url", "https://not-my-site.com"); + } + + #[TestWith([404])] + #[TestWith([401])] + #[TestWith([500])] + #[TestWith([501])] + #[TestWith([502])] + #[TestWith([503])] + #[TestWith([500])] + public function testItShouldThrowASourceNotFoundExceptionIfTheSourceUrlIsUnreachable(int $statusCode) + { + $source = "https://my-valid-source-url.com"; + $target = "https://lewisdale.dev/post/a-post-page"; + + $mockClient = $this->createMock(HttpClientInterface::class); + $mockResponse = $this->createMock(ResponseInterface::class); + + $mockClient->expects($this->once()) + ->method('request') + ->with($this->identicalTo('GET'), $this->identicalTo($source)) + ->will($this->returnValue($mockResponse)); + + $mockResponse->method('getStatusCode') + ->will($this->returnValue($statusCode)); + + $this->expectException(SourceNotFoundException::class); + + $endpoint = new Endpoint($mockClient); + $endpoint->receiveWebmention($source, $target); + } + + public function testItShouldThrowATargetNotMentionedErrorIfTheSourceDoesNotMentionTheTarget() + { + $source = "https://my-valid-source-url.com"; + $target = "https://lewisdale.dev/post/a-post-page"; + + $content = << + + + +

Some content

+

Here's some body content. It contains a url.

+

But it does not mention the target.

+ + + XML; + + $mockClient = $this->createMock(HttpClientInterface::class); + $mockResponse = $this->createMock(ResponseInterface::class); + + $mockClient->expects($this->once()) + ->method('request') + ->with($this->identicalTo('GET'), $this->identicalTo($source)) + ->will($this->returnValue($mockResponse)); + + $mockResponse->method('getStatusCode') + ->will($this->returnValue(200)); + + $mockResponse->method('getContent') + ->willReturn($content); + + $this->expectException(TargetNotMentionedException::class); + + $endpoint = new Endpoint($mockClient); + $endpoint->receiveWebmention($source, $target); + } } ?> \ No newline at end of file