From 82b26d15fece4129880e4350ed6e3479ef0cd9fa Mon Sep 17 00:00:00 2001 From: Ryan Barrett Date: Tue, 2 Feb 2016 21:50:26 -0800 Subject: [PATCH] add new optional target_domain parameter to /webmention endpoint. for #2 when used instead of target, finds all links to the target domain and subdomains, enqueues webmentions for them all, and returns all status URLs in the response's 'location' field. the HTTP Location header is omitted. --- controllers/API.php | 84 ++++++++++++++++--------- tests/APITest.php | 70 +++++++++++++++++---- tests/data/source.example.com/basictest | 2 +- 3 files changed, 112 insertions(+), 44 deletions(-) diff --git a/controllers/API.php b/controllers/API.php index e116abc..be65c20 100644 --- a/controllers/API.php +++ b/controllers/API.php @@ -44,19 +44,28 @@ class API { ]); } - # Require source and target parameters - if((!$source=$request->get('source')) || (!$target=$request->get('target'))) { + # Require source and target or target_domain parameters + $target = $target_domain = null; + if((!$source=$request->get('source')) || ((!$target=$request->get('target')) && (!$target_domain=$request->get('target_domain')))) { return $this->respond($response, 400, [ 'error' => 'missing_parameters', - 'error_description' => 'The source or target parameters were missing' + 'error_description' => 'The source or target or target_domain parameters were missing' + ]); + } + if($target && $target_domain) { + return $this->respond($response, 400, [ + 'error' => 'invalid_parameter', + 'error_description' => 'Can\'t provide both target and target_domain together' ]); } $urlregex = '/^https?:\/\/[^ ]+\.[^ ]+$/'; + $domainregex = '/^[^ ]+$/'; # Verify source, target, and callback are URLs $callback = $request->get('callback'); - if(!preg_match($urlregex, $source) || !preg_match($urlregex, $target) || + if(!preg_match($urlregex, $source) || + (!preg_match($urlregex, $target) && !preg_match($domainregex, $target_domain)) || ($callback && !preg_match($urlregex, $callback))) { return $this->respond($response, 400, [ 'error' => 'invalid_parameter', @@ -90,44 +99,59 @@ class API { $xpath = new DOMXPath($doc); - $found = false; + $found = []; foreach($xpath->query('//a[@href]') as $href) { - if($href->getAttribute('href') == $target) { - $found = true; - continue; + $url = $href->getAttribute('href'); + $domain = parse_url($url, PHP_URL_HOST); + if($url == $target || $domain == $target_domain || + # subdomain check + ($target_domain and substr_compare($domain, '.' . $target_domain, -(strlen($target_domain) + 1)) == 0)) { + $found[$url] = null; } } if(!$found) { return $this->respond($response, 400, [ 'error' => 'no_link_found', - 'error_description' => 'The source document does not have a link to the target URL' + 'error_description' => 'The source document does not have a link to the target URL or domain' ]); } # Everything checked out, so write the webmention to the log and queue a job to start sending + # TODO: database transaction? + + $statusURLs = []; + foreach($found as $url=>$_) { + $w = ORM::for_table('webmentions')->create(); + $w->site_id = $role->site_id; + $w->created_by = $role->user_id; + $w->created_at = date('Y-m-d H:i:s'); + $w->token = self::generateStatusToken(); + $w->source = $source; + $w->target = $url; + $w->vouch = $request->get('vouch'); + $w->callback = $callback; + $w->save(); + + q()->queue('Telegraph\Webmention', 'send', [$w->id]); + + $statusURLs[] = Config::$base . 'webmention/' . $w->token; + } - $w = ORM::for_table('webmentions')->create(); - $w->site_id = $role->site_id; - $w->created_by = $role->user_id; - $w->created_at = date('Y-m-d H:i:s'); - $w->token = self::generateStatusToken(); - $w->source = $source; - $w->target = $target; - $w->vouch = $request->get('vouch'); - $w->callback = $callback; - $w->save(); - - q()->queue('Telegraph\Webmention', 'send', [$w->id]); - - $statusURL = Config::$base . 'webmention/' . $w->token; - - return $this->respond($response, 201, [ - 'status' => 'queued', - 'location' => $statusURL - ], [ - 'Location' => $statusURL - ]); + if ($target) { + $body = [ + 'status' => 'queued', + 'location' => $statusURLs[0] + ]; + $headers = ['Location' => $statusURLs[0]]; + } else { + $body = [ + 'status' => 'queued', + 'location' => $statusURLs + ]; + $headers = []; + } + return $this->respond($response, 201, $body, $headers); } public function webmention_status(Request $request, Response $response, $args) { diff --git a/tests/APITest.php b/tests/APITest.php index 9012386..a447f0b 100644 --- a/tests/APITest.php +++ b/tests/APITest.php @@ -46,6 +46,22 @@ class APITest extends PHPUnit_Framework_TestCase { $role->save(); } + private function _assertQueued($source, $target, $status_url) { + preg_match('/\/webmention\/(.+)/', $status_url, $match); + $this->assertNotNull($match); + + # Verify it queued the mention in the database + $d = ORM::for_table('webmentions')->where(['source' => $source, 'target' => $target])->find_one(); + $this->assertNotNull($d); + $this->assertEquals($match[1], $d->token); + + # Check the status endpoint to make sure it says it's still queued + $response = $this->status($d->token); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode($response->getContent()); + $this->assertEquals('queued', $data->status); + } + public function testAuthentication() { $response = $this->webmention([]); $this->assertEquals(401, $response->getStatusCode()); @@ -82,6 +98,20 @@ class APITest extends PHPUnit_Framework_TestCase { $this->assertEquals(400, $response->getStatusCode()); $data = json_decode($response->getContent()); $this->assertEquals('missing_parameters', $data->error); + + $response = $this->webmention(['token'=>'a','target_domain'=>'foo']); + $this->assertEquals(400, $response->getStatusCode()); + $data = json_decode($response->getContent()); + $this->assertEquals('missing_parameters', $data->error); + } + + public function testTargetAndTargetDomain() { + $this->_createExampleAccount(); + + $response = $this->webmention(['token'=>'a','source'=>'foo','target'=>'foo','target_domain'=>'foo']); + $this->assertEquals(400, $response->getStatusCode()); + $data = json_decode($response->getContent()); + $this->assertEquals('invalid_parameter', $data->error); } public function testInvalidURLs() { @@ -122,7 +152,7 @@ class APITest extends PHPUnit_Framework_TestCase { $this->assertEquals(false, property_exists($data, 'error')); } - public function testQueuesWebmention() { + public function testTargetQueuesWebmention() { $this->_createExampleAccount(); $response = $this->webmention(['token'=>'a','source'=>'http://source.example.com/basictest','target'=>'http://target.example.com']); @@ -130,21 +160,35 @@ class APITest extends PHPUnit_Framework_TestCase { $data = json_decode($response->getContent()); $this->assertEquals(false, property_exists($data, 'error')); $this->assertEquals('queued', $data->status); - $this->assertEquals(true, property_exists($data, 'location')); + $this->_assertQueued('http://source.example.com/basictest', 'http://target.example.com', $data->location); + } - preg_match('/\/webmention\/(.+)/', $data->location, $match); - $this->assertNotNull($match); + public function testTargetDomainQueuesOneWebmention() { + $this->_createExampleAccount(); - # Verify it queued the mention in the database - $d = ORM::for_table('webmentions')->where(['source' => 'http://source.example.com/basictest', 'target' => 'http://target.example.com'])->find_one(); - $this->assertNotNull($d); - $this->assertEquals($match[1], $d->token); + $response = $this->webmention(['token'=>'a','source'=>'http://source.example.com/basictest','target_domain'=>'target.example.com']); + $body = $response->getContent(); + $this->assertEquals(201, $response->getStatusCode(), $body); + $data = json_decode($body); + $this->assertEquals(false, property_exists($data, 'error'), $body); + $this->assertEquals('queued', $data->status, $body); + $this->assertEquals(true, property_exists($data, 'location'), $body); + $this->assertEquals(1, count($data->location), $body); + $this->_assertQueued('http://source.example.com/basictest', 'http://target.example.com', $data->location[0]); + } - # Check the status endpoint to make sure it says it's still queued - $response = $this->status($d->token); - $this->assertEquals(200, $response->getStatusCode()); - $data = json_decode($response->getContent()); - $this->assertEquals('queued', $data->status); + public function testTargetDomainQueuesMultipleWebmentions() { + $this->_createExampleAccount(); + + $response = $this->webmention(['token'=>'a','source'=>'http://source.example.com/basictest','target_domain'=>'example.com']); + $body = $response->getContent(); + $this->assertEquals(201, $response->getStatusCode(), $body); + $data = json_decode($body); + $this->assertEquals(false, property_exists($data, 'error'), $body); + $this->assertEquals('queued', $data->status, $body); + $this->assertEquals(2, count($data->location), $body); + $this->_assertQueued('http://source.example.com/basictest', 'http://target.example.com', $data->location[0]); + $this->_assertQueued('http://source.example.com/basictest', 'http://target2.example.com', $data->location[1]); } public function testStatusNotFound() { diff --git a/tests/data/source.example.com/basictest b/tests/data/source.example.com/basictest index 0841787..b22f10b 100644 --- a/tests/data/source.example.com/basictest +++ b/tests/data/source.example.com/basictest @@ -9,6 +9,6 @@ Connection: keep-alive Test -

This page has a link to target.example.com.

+

This page has links to target.example.com and target2.example.com.