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.

200 lines
5.7 KiB

  1. <?php
  2. use Symfony\Component\HttpFoundation\Request;
  3. use Symfony\Component\HttpFoundation\Response;
  4. class API {
  5. public $http;
  6. public function __construct() {
  7. $this->http = new Telegraph\HTTP();
  8. }
  9. private function respond(Response $response, $code, $params, $headers=[]) {
  10. $response->setStatusCode($code);
  11. foreach($headers as $k=>$v) {
  12. $response->headers->set($k, $v);
  13. }
  14. $response->headers->set('Content-Type', 'application/json');
  15. $response->setContent(json_encode($params));
  16. return $response;
  17. }
  18. private static function toHtmlEntities($input) {
  19. return mb_convert_encoding($input, 'HTML-ENTITIES', mb_detect_encoding($input));
  20. }
  21. private static function generateStatusToken() {
  22. $str = dechex(date('y'));
  23. $chs = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
  24. $len = strlen($chs);
  25. for($i = 0; $i < 16; $i++) {
  26. $str .= $chs[mt_rand(0, $len - 1)];
  27. }
  28. return $str;
  29. }
  30. public function webmention(Request $request, Response $response) {
  31. # Require the token parameter
  32. if(!$token=$request->get('token')) {
  33. return $this->respond($response, 401, [
  34. 'error' => 'authentication_required',
  35. 'error_description' => 'A token is required to use the API'
  36. ]);
  37. }
  38. # Require source and target parameters
  39. if((!$source=$request->get('source')) || (!$target=$request->get('target'))) {
  40. return $this->respond($response, 400, [
  41. 'error' => 'missing_parameters',
  42. 'error_description' => 'The source or target parameters were missing'
  43. ]);
  44. }
  45. $urlregex = '/^https?:\/\/[^ ]+\.[^ ]+$/';
  46. # Verify source and target are URLs
  47. if(!preg_match($urlregex, $source) || !preg_match($urlregex, $target)) {
  48. return $this->respond($response, 400, [
  49. 'error' => 'invalid_parameter',
  50. 'error_description' => 'The source or target parameters were invalid'
  51. ]);
  52. }
  53. # If a callback was provided, verify it is a URL
  54. if($callback=$request->get('callback')) {
  55. if(!preg_match($urlregex, $source) || !preg_match($urlregex, $target)) {
  56. return $this->respond($response, 400, [
  57. 'error' => 'invalid_parameter',
  58. 'error_description' => 'The callback parameter was invalid'
  59. ]);
  60. }
  61. }
  62. # Verify the token is valid
  63. $role = ORM::for_table('roles')->where('token', $token)->find_one();
  64. if(!$role) {
  65. return $this->respond($response, 401, [
  66. 'error' => 'invalid_token',
  67. 'error_description' => 'The token provided is not valid'
  68. ]);
  69. }
  70. # Synchronously check the source URL and verify that it actually contains
  71. # a link to the target. This way we prevent this API from sending known invalid mentions.
  72. $sourceData = $this->http->get($source);
  73. $doc = new DOMDocument();
  74. @$doc->loadHTML(self::toHtmlEntities($sourceData['body']));
  75. if(!$doc) {
  76. return $this->respond($response, 400, [
  77. 'error' => 'source_not_html',
  78. 'error_description' => 'The source document could not be parsed as HTML'
  79. ]);
  80. }
  81. $xpath = new DOMXPath($doc);
  82. $found = false;
  83. foreach($xpath->query('//a[@href]') as $href) {
  84. if($href->getAttribute('href') == $target) {
  85. $found = true;
  86. continue;
  87. }
  88. }
  89. if(!$found) {
  90. return $this->respond($response, 400, [
  91. 'error' => 'no_link_found',
  92. 'error_description' => 'The source document does not have a link to the target URL'
  93. ]);
  94. }
  95. # Everything checked out, so write the webmention to the log and queue a job to start sending
  96. $w = ORM::for_table('webmentions')->create();
  97. $w->site_id = $role->site_id;
  98. $w->created_by = $role->user_id;
  99. $w->created_at = date('Y-m-d H:i:s');
  100. $w->token = self::generateStatusToken();
  101. $w->source = $source;
  102. $w->target = $target;
  103. $w->vouch = $request->get('vouch');
  104. $w->callback = $callback;
  105. $w->save();
  106. q()->queue('Telegraph\Webmention', 'send', [$w->id]);
  107. $statusURL = Config::$base . 'webmention/' . $w->token;
  108. return $this->respond($response, 201, [
  109. 'status' => 'queued',
  110. 'location' => $statusURL
  111. ], [
  112. 'Location' => $statusURL
  113. ]);
  114. }
  115. public function webmention_status(Request $request, Response $response, $args) {
  116. $webmention = ORM::for_table('webmentions')->where('token', $args['code'])->find_one();
  117. if(!$webmention) {
  118. return $this->respond($response, 404, [
  119. 'status' => 'not_found',
  120. ]);
  121. }
  122. $status = ORM::for_table('webmention_status')->where('webmention_id', $webmention->id)->order_by_desc('created_at')->find_one();
  123. $statusURL = Config::$base . 'webmention/' . $webmention->token;
  124. if(!$status) {
  125. $code = 'queued';
  126. } else {
  127. $code = $status->status;
  128. }
  129. $data = [
  130. 'status' => $code,
  131. ];
  132. if($webmention->webmention_endpoint) {
  133. $data['type'] = 'webmention';
  134. $data['endpoint'] = $webmention->webmention_endpoint;
  135. }
  136. if($webmention->pingback_endpoint) {
  137. $data['type'] = 'pingback';
  138. $data['endpoint'] = $webmention->pingback_endpoint;
  139. }
  140. switch($code) {
  141. case 'queued':
  142. $summary = 'The webmention is still in the processing queue';
  143. break;
  144. case 'not_supported':
  145. $summary = 'No webmention or pingback endpoint were found at the target';
  146. break;
  147. case 'accepted':
  148. $summary = 'The '.$data['type'].' request was accepted';
  149. break;
  150. default:
  151. $summary = false;
  152. }
  153. if($status && $status->http_code)
  154. $data['http_code'] = (int)$status->http_code;
  155. if($summary)
  156. $data['summary'] = $summary;
  157. if($webmention->complete == 0)
  158. $data['location'] = $statusURL;
  159. return $this->respond($response, 200, $data);
  160. }
  161. }