You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

228 lines
6.2 KiB

  1. <?php
  2. namespace p3k\WebSub;
  3. use p3k\HTTP;
  4. use p3k;
  5. use DOMXPath;
  6. class Client {
  7. private $http;
  8. public function __construct($http=false) {
  9. if($http) {
  10. $this->http = $http;
  11. } else {
  12. $this->http = new HTTP('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) p3k-websub/0.1.0');
  13. }
  14. }
  15. public function discover($url, $headfirst=true, $verbose=false) {
  16. $hub = false;
  17. $self = false;
  18. $type = 'unknown';
  19. $http = [
  20. 'hub' => [],
  21. 'self' => [],
  22. 'type' => $type,
  23. ];
  24. $body = [
  25. 'hub' => [],
  26. 'self' => [],
  27. 'type' => $type,
  28. ];
  29. if($headfirst) {
  30. // First make a HEAD request, and if the links are found there, stop.
  31. $topic = $this->http->head($url);
  32. // Get the values from the Link headers
  33. if(array_key_exists('hub', $topic['rels'])) {
  34. $http['hub'] = $topic['rels']['hub'];
  35. $hub = $http['hub'][0];
  36. }
  37. if(array_key_exists('self', $topic['rels'])) {
  38. $http['self'] = $topic['rels']['self'];
  39. $self = $http['self'][0];
  40. }
  41. $content_type = '';
  42. if(array_key_exists('Content-Type', $topic['headers'])) {
  43. $content_type = $topic['headers']['Content-Type'];
  44. if(is_array($content_type))
  45. $content_type = $content_type[count($content_type)-1];
  46. if(strpos($content_type, 'text/html') !== false) {
  47. $type = $http['type'] = 'html';
  48. } else if(strpos($content_type, 'xml') !== false) {
  49. if(strpos('rss', $content_type) !== false) {
  50. $type = $http['type'] = 'rss';
  51. } else if(strpos($content_type, 'atom') !== false) {
  52. $type = $http['type'] = 'atom';
  53. }
  54. }
  55. }
  56. }
  57. if(!$hub || !$self) {
  58. // If we're missing hub or self, now make a GET request
  59. $topic = $this->http->get($url);
  60. $content_type = '';
  61. if(array_key_exists('Content-Type', $topic['headers'])) {
  62. $content_type = $topic['headers']['Content-Type'];
  63. if(is_array($content_type))
  64. $content_type = $content_type[count($content_type)-1];
  65. // Get the values from the Link headers
  66. if(array_key_exists('hub', $topic['rels'])) {
  67. $http['hub'] = $topic['rels']['hub'];
  68. }
  69. if(array_key_exists('self', $topic['rels'])) {
  70. $http['self'] = $topic['rels']['self'];
  71. }
  72. if(preg_match('|text/html|', $content_type)) {
  73. $type = $body['type'] = 'html';
  74. $dom = p3k\html_to_dom_document($topic['body']);
  75. $xpath = new DOMXPath($dom);
  76. foreach($xpath->query('*/link[@href]') as $link) {
  77. $rel = $link->getAttribute('rel');
  78. $url = $link->getAttribute('href');
  79. if($rel == 'hub') {
  80. $body['hub'][] = $url;
  81. } else if($rel == 'self') {
  82. $body['self'][] = $url;
  83. }
  84. }
  85. } else if(preg_match('|xml|', $content_type)) {
  86. $dom = p3k\xml_to_dom_document($topic['body']);
  87. $xpath = new DOMXPath($dom);
  88. $xpath->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
  89. if($xpath->query('/rss')->length) {
  90. $type = $body['type'] = 'rss';
  91. } elseif($xpath->query('/atom:feed')->length) {
  92. $type = $body['type'] = 'atom';
  93. }
  94. // Look for atom link elements in the feed
  95. foreach($xpath->query('/atom:feed/atom:link[@href]') as $link) {
  96. $rel = $link->getAttribute('rel');
  97. $url = $link->getAttribute('href');
  98. if($rel == 'hub') {
  99. $body['hub'][] = $url;
  100. } else if($rel == 'self') {
  101. $body['self'][] = $url;
  102. }
  103. }
  104. // Some RSS feeds include the link element as an atom attribute
  105. foreach($xpath->query('/rss/channel/atom:link[@href]') as $link) {
  106. $rel = $link->getAttribute('rel');
  107. $url = $link->getAttribute('href');
  108. if($rel == 'hub') {
  109. $body['hub'][] = $url;
  110. } else if($rel == 'self') {
  111. $body['self'][] = $url;
  112. }
  113. }
  114. }
  115. }
  116. }
  117. // Prioritize the HTTP headers
  118. if($http['hub']) {
  119. $hub = $http['hub'][0];
  120. $hub_source = 'http';
  121. }
  122. elseif($body['hub']) {
  123. $hub = $body['hub'][0];
  124. $hub_source = 'body';
  125. } else {
  126. $hub_source = false;
  127. }
  128. if($http['self']) {
  129. $self = $http['self'][0];
  130. $self_source = 'http';
  131. }
  132. elseif($body['self']) {
  133. $self = $body['self'][0];
  134. $self_source = 'body';
  135. } else {
  136. $self_source = false;
  137. }
  138. if(!($hub && $self)) {
  139. return false;
  140. }
  141. $response = [
  142. 'hub' => $hub,
  143. 'hub_source' => $hub_source,
  144. 'self' => $self,
  145. 'self_source' => $self_source,
  146. 'type' => $type,
  147. ];
  148. if($verbose) {
  149. $response['details'] = [
  150. 'http' => $http,
  151. 'body' => $body
  152. ];
  153. }
  154. return $response;
  155. }
  156. public function subscribe($hub, $topic, $callback, $options=[]) {
  157. $params = [
  158. 'hub.mode' => 'subscribe',
  159. 'hub.topic' => $topic,
  160. 'hub.callback' => $callback,
  161. ];
  162. if(isset($options['lease_seconds'])) {
  163. $params['hub.lease_seconds'] = $options['lease_seconds'];
  164. }
  165. if(isset($options['secret'])) {
  166. $params['hub.secret'] = $options['secret'];
  167. }
  168. $response = $this->http->post($hub, http_build_query($params));
  169. // TODO: Check for HTTP 307/308 and subscribe at the new location
  170. return $response;
  171. }
  172. public function unsubscribe($hub, $topic, $callback) {
  173. $params = [
  174. 'hub.mode' => 'unsubscribe',
  175. 'hub.topic' => $topic,
  176. 'hub.callback' => $callback,
  177. ];
  178. $response = $this->http->post($hub, http_build_query($params));
  179. // TODO: Check for HTTP 307/308 and unsubscribe at the new location
  180. return $response;
  181. }
  182. public static function verify_signature($body, $signature_header, $secret) {
  183. if($signature_header && is_string($signature_header)
  184. && preg_match('/(sha(?:1|256|384|512))=(.+)/', $signature_header, $match)) {
  185. $alg = $match[1];
  186. $sig = $match[2];
  187. $expected_signature = hash_hmac($alg, $body, $secret);
  188. return $sig == $expected_signature;
  189. } else {
  190. return false;
  191. }
  192. }
  193. }