<?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, http_build_query($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, http_build_query($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;
|
|
}
|
|
}
|
|
|
|
}
|
|
|