| <?phpuse Symfony\Component\HttpFoundation\Request;use Symfony\Component\HttpFoundation\Response;
use Monolog\Logger;
class API {
  public $http;
  public function __construct() {    $this->http = new Telegraph\HTTP();  }
  private function respond(Response $response, $code, $params, $headers=[]) {    $response->setStatusCode($code);    foreach($headers as $k=>$v) {      $response->headers->set($k, $v);    }    $response->headers->set('Content-Type', 'application/json');    $response->setContent(json_encode($params, JSON_UNESCAPED_SLASHES+JSON_PRETTY_PRINT));    return $response;  }
  private static function toHtmlEntities($input) {    return mb_convert_encoding($input, 'HTML-ENTITIES', mb_detect_encoding($input));  }
  private static function generateStatusToken() {    $str = dechex(date('y'));    $chs = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';    $len = strlen($chs);    for($i = 0; $i < 16; $i++) {      $str .= $chs[mt_rand(0, $len - 1)];    }    return $str;  }
  public function webmention(Request $request, Response $response) {
    # Require the token or csrf parameter
    if($csrf=$request->get('_csrf')) {      session_start();      if($csrf != $_SESSION['_csrf']) {        return $this->respond($response, 401, [          'error' => 'invalid_csrf_token',          'error_description' => 'An error occurred. Make sure you have only one tab open.',        ]);      }    } else if(!$token=$request->get('token')) {      return $this->respond($response, 401, [        'error' => 'authentication_required',        'error_description' => 'A token is required to use the API'      ]);
      # Verify the token is valid
      $role = ORM::for_table('roles')->where('token', $token)->find_one();
      if(!$role) {        return $this->respond($response, 401, [          'error' => 'invalid_token',          'error_description' => 'The token provided is not valid'        ]);      }    }
    # Require source and target or target_domain parameters
    $target = $target_domain = null;    if((!$source=$request->get('source')) || ((!$target=$request->get('target')) && (!$target_domain=$request->get('target_domain')))) {      return $this->respond($response, 400, [        'error' => 'missing_parameters',        'error_description' => 'The source or target or target_domain parameters were missing'      ]);    }    if($target && $target_domain) {      return $this->respond($response, 400, [        'error' => 'invalid_parameter',        'error_description' => 'Can\'t provide both target and target_domain together'      ]);    }
    # Can only use source & target if no authentication(role) is set
    if(!isset($role)) {      if($target_domain) {        return $this->respond($response, 400, [          'error' => 'unauthorized',          'error_description' => 'Can only use the target_domain feature when providing a token from the API',        ]);      }    }
    $urlregex = '/^https?:\/\/[^ ]+\.[^ ]+$/';    $domainregex = '/^[^ ]+$/';
    # Verify source, target, and callback are URLs
    $callback = $request->get('callback');    if(!preg_match($urlregex, $source) ||       (!preg_match($urlregex, $target) && !preg_match($domainregex, $target_domain)) ||       ($callback && !preg_match($urlregex, $callback))) {      return $this->respond($response, 400, [        'error' => 'invalid_parameter',        'error_description' => 'The source, target, or callback parameters were invalid'      ]);    }
    # Don't send anything if the source domain matches the target domain
    # The problem is someone pushing to Superfeedr who is also subscribed, will cause a
    # request to be sent with the source of one of their posts, and their own target domain.
    # This causes a whole slew of webmentions to be queued up, almost all of which are not needed.
    if($target_domain) {      $source_domain = parse_url($source, PHP_URL_HOST);      if($target_domain == $source_domain) {        # Return 200 so Superfeedr doesn't think something is broken
        return $this->respond($response, 200, [          'error' => 'not_supported',          'error_description' => 'You cannot use the target_domain feature to send webmentions to the same domain as the source URL'        ]);      }    }
    # Check the blacklist of domains that are known to not accept webmentions
    if($target && !Telegraph\Webmention::isProbablySupported($target)) {      return $this->respond($response, 400, [        'error' => 'not_supported',        'error_description' => 'The target domain is known to not accept webmentions. If you believe this is in error, please file an issue at https://github.com/aaronpk/Telegraph/issues'      ]);    }
    # If there is no code given,
    # Synchronously check the source URL and verify that it actually contains
    # a link to the target. This way we prevent this API from sending known invalid mentions.
    if($request->get('code')) {      # target URL is required
      if(!$target) {        return $this->respond($response, 400, [          'error' => 'not_supported',          'error_description' => 'The target_domain parameter is not supported for sending private webmentions'        ]);      }
      $found[$target] = null;    } else {      $sourceData = $this->http->get($source, ['Accept: text/html, */*']);
      $doc = new DOMDocument();      libxml_use_internal_errors(true); # suppress parse errors and warnings
      @$doc->loadHTML(self::toHtmlEntities($sourceData['body']), LIBXML_NOWARNING|LIBXML_NOERROR);      libxml_clear_errors();
      if(!$doc) {        return $this->respond($response, 400, [          'error' => 'source_not_html',          'error_description' => 'The source document could not be parsed as HTML'        ]);      }
      $xpath = new DOMXPath($doc);
      $found = [];      $links = [];      foreach($xpath->query('//a[@href]') as $href) {        $url = $href->getAttribute('href');        if($target) {          $links[] = $url;          # target parameter was provided
          if($url == $target) {            $found[$url] = null;          }        } elseif($target_domain) {          # target_domain parameter was provided
          $domain = parse_url($url, PHP_URL_HOST);          if($domain && ($domain == $target_domain || str_ends_with($domain, '.' . $target_domain))) {            $found[$url] = null;          }        }      }
      if(!$found) {        return $this->respond($response, 400, [          'error' => 'no_link_found',          'error_description' => 'The source document does not have a link to the target URL or domain',          'links' => $links        ]);      }    }
    # Write the webmention to the database and queue a job to start sending
    $statusURLs = [];    foreach($found as $url=>$_) {      $w = ORM::for_table('webmentions')->create();      $w->site_id = isset($role) ? $role->site_id : 0;      $w->created_by = isset($role) ? $role->user_id : 0;      $w->created_at = date('Y-m-d H:i:s');      $w->token = self::generateStatusToken();      $w->source = $source;      $w->target = $url;      $w->vouch = $request->get('vouch');      $w->code = $request->get('code');      $w->realm = $request->get('realm');      $w->callback = $callback;      $w->save();
      q()->queue('Telegraph\Webmention', 'send', [$w->id]);
      $statusURLs[] = Config::$base . 'webmention/' . $w->token;    }
    if($target) {      $body = [        'status' => 'queued',        'location' => $statusURLs[0]      ];      $headers = ['Location' => $statusURLs[0]];    } else {      $body = [        'status' => 'queued',        'location' => $statusURLs      ];      $headers = [];    }    return $this->respond($response, 201, $body, $headers);  }
  public function superfeedr_tracker(Request $request, Response $response, $args) {    logger()->addInfo("Got payload from superfeedr: " . $request->getContent());
    $input = json_decode($request->getContent(), true);
    # Require the code parameter
    if(!$token=$args['token']) {      return $this->respond($response, 401, [        'error' => 'authentication_required',        'error_description' => 'A token is required to use the API'      ]);    }
    # Verify the token is valid
    $role = ORM::for_table('roles')->where('token', $token)->find_one();
    if(!$role) {      return $this->respond($response, 401, [        'error' => 'invalid_token',        'error_description' => 'The token provided is not valid'      ]);    }
    $site = ORM::for_table('sites')->where('id', $role->site_id)->find_one();
    if(is_array($input)      && array_key_exists('items', $input)      && ($items = $input['items'])      && is_array($items)      && array_key_exists(0, $items)      && ($item = $items[0])      && array_key_exists('permalinkUrl', $item)) {      $url = $item['permalinkUrl'];
      $domain = parse_url($site->url, PHP_URL_HOST);
      # Create a new request that looks like a request to the API with a target_domain parameter
      $new_request = new Request(['token' => $token, 'source' => $url, 'target_domain' => $domain]);
      return $this->webmention($new_request, $response);
    } else {      return $this->respond($response, 200, [        'error' => 'invalid_request',        'error_description' => 'Could not find source URL from the superfeedr payload'      ]);    }  }
  public function webmention_status(Request $request, Response $response, $args) {
    $webmention = ORM::for_table('webmentions')->where('token', $args['code'])->find_one();
    if(!$webmention) {      return $this->respond($response, 404, [        'status' => 'not_found',      ]);    }
    $status = ORM::for_table('webmention_status')->where('webmention_id', $webmention->id)->order_by_desc('created_at')->find_one();
    $statusURL = Config::$base . 'webmention/' . $webmention->token;
    if(!$status) {      $code = 'queued';    } else {      $code = $status->status;    }
    $data = [      'source' => $webmention->source,      'target' => $webmention->target,      'status' => $code,    ];
    if($webmention->webmention_endpoint) {      $data['type'] = 'webmention';      $data['endpoint'] = $webmention->webmention_endpoint;    }    if($webmention->pingback_endpoint) {      $data['type'] = 'pingback';      $data['endpoint'] = $webmention->pingback_endpoint;    }
    switch($code) {      case 'queued':        $summary = 'The webmention is still in the processing queue';        break;      case 'not_supported':        $summary = 'No webmention or pingback endpoint were found at the target';        break;      case 'accepted':        $summary = 'The '.$data['type'].' request was accepted';        break;      default:        $summary = false;    }
    if($status && $status->http_code)      $data['http_code'] = (int)$status->http_code;
    if($status && $status->raw_response) {      $data['http_body'] = $status->raw_response;    }
    if($summary)      $data['summary'] = $summary;
    if($webmention->complete == 0)      $data['location'] = $statusURL;
    return $this->respond($response, 200, $data);  }
}
 |