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.

140 lines
4.0 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->setContent(json_encode($params));
  15. return $response;
  16. }
  17. private static function toHtmlEntities($input) {
  18. return mb_convert_encoding($input, 'HTML-ENTITIES', mb_detect_encoding($input));
  19. }
  20. private static function generateStatusToken() {
  21. $str = dechex(date('y'));
  22. $chs = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
  23. $len = strlen($chs);
  24. for($i = 0; $i < 16; $i++) {
  25. $str .= $chs[mt_rand(0, $len - 1)];
  26. }
  27. return $str;
  28. }
  29. public function webmention(Request $request, Response $response) {
  30. # Require the token parameter
  31. if(!$token=$request->get('token')) {
  32. return $this->respond($response, 401, [
  33. 'error' => 'authentication_required',
  34. 'error_description' => 'A token is required to use the API'
  35. ]);
  36. }
  37. # Require source and target parameters
  38. if((!$source=$request->get('source')) || (!$target=$request->get('target'))) {
  39. return $this->respond($response, 400, [
  40. 'error' => 'missing_parameters',
  41. 'error_description' => 'The source or target parameters were missing'
  42. ]);
  43. }
  44. $urlregex = '/^https?:\/\/[^ ]+\.[^ ]+$/';
  45. # Verify source and target are URLs
  46. if(!preg_match($urlregex, $source) || !preg_match($urlregex, $target)) {
  47. return $this->respond($response, 400, [
  48. 'error' => 'invalid_parameter',
  49. 'error_description' => 'The source or target parameters were invalid'
  50. ]);
  51. }
  52. # If a callback was provided, verify it is a URL
  53. if($callback=$request->get('callback')) {
  54. if(!preg_match($urlregex, $source) || !preg_match($urlregex, $target)) {
  55. return $this->respond($response, 400, [
  56. 'error' => 'invalid_parameter',
  57. 'error_description' => 'The callback parameter was invalid'
  58. ]);
  59. }
  60. }
  61. # Verify the token is valid
  62. $role = ORM::for_table('roles')->where('token', $token)->find_one();
  63. if(!$role) {
  64. return $this->respond($response, 401, [
  65. 'error' => 'invalid_token',
  66. 'error_description' => 'The token provided is not valid'
  67. ]);
  68. }
  69. # Synchronously check the source URL and verify that it actually contains
  70. # a link to the target. This way we prevent this API from sending known invalid mentions.
  71. $sourceData = $this->http->get($source);
  72. $doc = new DOMDocument();
  73. @$doc->loadHTML(self::toHtmlEntities($sourceData['body']));
  74. if(!$doc) {
  75. return $this->respond($response, 400, [
  76. 'error' => 'source_not_html',
  77. 'error_description' => 'The source document could not be parsed as HTML'
  78. ]);
  79. }
  80. $xpath = new DOMXPath($doc);
  81. $found = false;
  82. foreach($xpath->query('//a[@href]') as $href) {
  83. if($href->getAttribute('href') == $target) {
  84. $found = true;
  85. continue;
  86. }
  87. }
  88. if(!$found) {
  89. return $this->respond($response, 400, [
  90. 'error' => 'no_link_found',
  91. 'error_description' => 'The source document does not have a link to the target URL'
  92. ]);
  93. }
  94. # Everything checked out, so write the webmention to the log and queue a job to start sending
  95. $w = ORM::for_table('webmentions')->create();
  96. $w->site_id = $role->site_id;
  97. $w->created_by = $role->user_id;
  98. $w->created_at = date('Y-m-d H:i:s');
  99. $w->token = self::generateStatusToken();
  100. $w->source = $source;
  101. $w->target = $target;
  102. $w->vouch = $request->get('vouch');
  103. $w->callback = $callback;
  104. $w->save();
  105. $statusURL = Config::$base . 'webmention/' . $w->token;
  106. return $this->respond($response, 201, [
  107. 'result' => 'queued',
  108. 'status' => $statusURL
  109. ], [
  110. 'Location' => $statusURL
  111. ]);
  112. }
  113. }