|
|
@ -0,0 +1,228 @@ |
|
|
|
<?php |
|
|
|
namespace p3k\WebSub; |
|
|
|
|
|
|
|
use p3k\HTTP; |
|
|
|
use p3k; |
|
|
|
use DOMXPath; |
|
|
|
|
|
|
|
class Client { |
|
|
|
|
|
|
|
private $http; |
|
|
|
|
|
|
|
public function __construct($http=false) { |
|
|
|
if($http) { |
|
|
|
$this->http = $http; |
|
|
|
} else { |
|
|
|
$this->http = new HTTP('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) p3k-websub/0.1.0'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public function discover($url, $headfirst=true, $verbose=false) { |
|
|
|
$hub = false; |
|
|
|
$self = false; |
|
|
|
$type = 'unknown'; |
|
|
|
|
|
|
|
$http = [ |
|
|
|
'hub' => [], |
|
|
|
'self' => [], |
|
|
|
'type' => $type, |
|
|
|
]; |
|
|
|
$body = [ |
|
|
|
'hub' => [], |
|
|
|
'self' => [], |
|
|
|
'type' => $type, |
|
|
|
]; |
|
|
|
|
|
|
|
if($headfirst) { |
|
|
|
// First make a HEAD request, and if the links are found there, stop.
|
|
|
|
$topic = $this->http->head($url); |
|
|
|
|
|
|
|
// Get the values from the Link headers
|
|
|
|
if(array_key_exists('hub', $topic['rels'])) { |
|
|
|
$http['hub'] = $topic['rels']['hub']; |
|
|
|
$hub = $http['hub'][0]; |
|
|
|
} |
|
|
|
if(array_key_exists('self', $topic['rels'])) { |
|
|
|
$http['self'] = $topic['rels']['self']; |
|
|
|
$self = $http['self'][0]; |
|
|
|
} |
|
|
|
|
|
|
|
$content_type = ''; |
|
|
|
if(array_key_exists('Content-Type', $topic['headers'])) { |
|
|
|
$content_type = $topic['headers']['Content-Type']; |
|
|
|
if(is_array($content_type)) |
|
|
|
$content_type = $content_type[count($content_type)-1]; |
|
|
|
|
|
|
|
if(strpos($content_type, 'text/html') !== false) { |
|
|
|
$type = $http['type'] = 'html'; |
|
|
|
} else if(strpos($content_type, 'xml') !== false) { |
|
|
|
if(strpos('rss', $content_type) !== false) { |
|
|
|
$type = $http['type'] = 'rss'; |
|
|
|
} else if(strpos($content_type, 'atom') !== false) { |
|
|
|
$type = $http['type'] = 'atom'; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if(!$hub || !$self) { |
|
|
|
// If we're missing hub or self, now make a GET request
|
|
|
|
$topic = $this->http->get($url); |
|
|
|
|
|
|
|
$content_type = ''; |
|
|
|
if(array_key_exists('Content-Type', $topic['headers'])) { |
|
|
|
$content_type = $topic['headers']['Content-Type']; |
|
|
|
if(is_array($content_type)) |
|
|
|
$content_type = $content_type[count($content_type)-1]; |
|
|
|
|
|
|
|
// Get the values from the Link headers
|
|
|
|
if(array_key_exists('hub', $topic['rels'])) { |
|
|
|
$http['hub'] = $topic['rels']['hub']; |
|
|
|
} |
|
|
|
if(array_key_exists('self', $topic['rels'])) { |
|
|
|
$http['self'] = $topic['rels']['self']; |
|
|
|
} |
|
|
|
|
|
|
|
if(preg_match('|text/html|', $content_type)) { |
|
|
|
$type = $body['type'] = 'html'; |
|
|
|
|
|
|
|
$dom = p3k\html_to_dom_document($topic['body']); |
|
|
|
$xpath = new DOMXPath($dom); |
|
|
|
|
|
|
|
foreach($xpath->query('*/link[@href]') as $link) { |
|
|
|
$rel = $link->getAttribute('rel'); |
|
|
|
$url = $link->getAttribute('href'); |
|
|
|
if($rel == 'hub') { |
|
|
|
$body['hub'][] = $url; |
|
|
|
} else if($rel == 'self') { |
|
|
|
$body['self'][] = $url; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} else if(preg_match('|xml|', $content_type)) { |
|
|
|
$dom = p3k\xml_to_dom_document($topic['body']); |
|
|
|
$xpath = new DOMXPath($dom); |
|
|
|
$xpath->registerNamespace('atom', 'http://www.w3.org/2005/Atom'); |
|
|
|
|
|
|
|
if($xpath->query('/rss')->length) { |
|
|
|
$type = $body['type'] = 'rss'; |
|
|
|
} elseif($xpath->query('/atom:feed')->length) { |
|
|
|
$type = $body['type'] = 'atom'; |
|
|
|
} |
|
|
|
|
|
|
|
// Look for atom link elements in the feed
|
|
|
|
foreach($xpath->query('/atom:feed/atom:link[@href]') as $link) { |
|
|
|
$rel = $link->getAttribute('rel'); |
|
|
|
$url = $link->getAttribute('href'); |
|
|
|
if($rel == 'hub') { |
|
|
|
$body['hub'][] = $url; |
|
|
|
} else if($rel == 'self') { |
|
|
|
$body['self'][] = $url; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Some RSS feeds include the link element as an atom attribute
|
|
|
|
foreach($xpath->query('/rss/channel/atom:link[@href]') as $link) { |
|
|
|
$rel = $link->getAttribute('rel'); |
|
|
|
$url = $link->getAttribute('href'); |
|
|
|
if($rel == 'hub') { |
|
|
|
$body['hub'][] = $url; |
|
|
|
} else if($rel == 'self') { |
|
|
|
$body['self'][] = $url; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Prioritize the HTTP headers
|
|
|
|
if($http['hub']) { |
|
|
|
$hub = $http['hub'][0]; |
|
|
|
$hub_source = 'http'; |
|
|
|
} |
|
|
|
elseif($body['hub']) { |
|
|
|
$hub = $body['hub'][0]; |
|
|
|
$hub_source = 'body'; |
|
|
|
} else { |
|
|
|
$hub_source = false; |
|
|
|
} |
|
|
|
|
|
|
|
if($http['self']) { |
|
|
|
$self = $http['self'][0]; |
|
|
|
$self_source = 'http'; |
|
|
|
} |
|
|
|
elseif($body['self']) { |
|
|
|
$self = $body['self'][0]; |
|
|
|
$self_source = 'body'; |
|
|
|
} else { |
|
|
|
$self_source = false; |
|
|
|
} |
|
|
|
|
|
|
|
if(!($hub && $self)) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
$response = [ |
|
|
|
'hub' => $hub, |
|
|
|
'hub_source' => $hub_source, |
|
|
|
'self' => $self, |
|
|
|
'self_source' => $self_source, |
|
|
|
'type' => $type, |
|
|
|
]; |
|
|
|
|
|
|
|
if($verbose) { |
|
|
|
$response['details'] = [ |
|
|
|
'http' => $http, |
|
|
|
'body' => $body |
|
|
|
]; |
|
|
|
} |
|
|
|
|
|
|
|
return $response; |
|
|
|
} |
|
|
|
|
|
|
|
public function subscribe($hub, $topic, $callback, $options=[]) { |
|
|
|
$params = [ |
|
|
|
'hub.mode' => 'subscribe', |
|
|
|
'hub.topic' => $topic, |
|
|
|
'hub.callback' => $callback, |
|
|
|
]; |
|
|
|
if(isset($options['lease_seconds'])) { |
|
|
|
$params['hub.lease_seconds'] = $options['lease_seconds']; |
|
|
|
} |
|
|
|
if(isset($options['secret'])) { |
|
|
|
$params['hub.secret'] = $options['secret']; |
|
|
|
} |
|
|
|
$response = $this->http->post($hub, $params); |
|
|
|
|
|
|
|
// TODO: Check for HTTP 307/308 and subscribe at the new location
|
|
|
|
|
|
|
|
return $response; |
|
|
|
} |
|
|
|
|
|
|
|
public function unsubscribe($hub, $topic, $callback) { |
|
|
|
$params = [ |
|
|
|
'hub.mode' => 'unsubscribe', |
|
|
|
'hub.topic' => $topic, |
|
|
|
'hub.callback' => $callback, |
|
|
|
]; |
|
|
|
$response = $this->http->post($hub, $params); |
|
|
|
|
|
|
|
// TODO: Check for HTTP 307/308 and unsubscribe at the new location
|
|
|
|
|
|
|
|
return $response; |
|
|
|
} |
|
|
|
|
|
|
|
public static function verify_signature($body, $signature_header, $secret) { |
|
|
|
if($signature_header && is_string($signature_header) |
|
|
|
&& preg_match('/(sha(?:1|256|384|512))=(.+)/', $signature_header, $match)) { |
|
|
|
$alg = $match[1]; |
|
|
|
$sig = $match[2]; |
|
|
|
$expected_signature = hash_hmac($alg, $body, $secret); |
|
|
|
return $sig == $expected_signature; |
|
|
|
} else { |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|