+ if ($args != "prefPrefs") {
+ return;
+ }
+
+ $accounts = PluginHost::getInstance()->get($this, 'accounts', []);
+
+ //FIXME: default identity
+ include __DIR__ . '/settings.phtml';
+ }
+
+ /**
+ * CLI command
+ */
+ public function micropub($args)
+ {
+ //we do not get all arguments passed here, to we work around
+ $args = $GLOBALS['argv'];
+ array_shift($args);//update.php
+ array_shift($args);//--micropub
+ $mode = array_shift($args);
+ return $this->action($mode, $args);
+ }
+
+ /**
+ * HTTP command.
+ * Also used by micropub() cli command method.
+ *
+ * /backend.php?op=pluginhandler&plugin=micropub&method=action
+ */
+ public function action($mode = null, $args = [])
+ {
+ if (isset($_POST['mode'])) {
+ $mode = $_POST['mode'];
+ } else if (isset($_GET['mode'])) {
+ $mode = $_GET['mode'];
+ }
+
+ if ($mode == 'authorize') {
+ return $this->authorizeAction($args);
+ } else if ($mode == 'authreturn') {
+ return $this->authreturnAction();
+ } else if ($mode == 'post') {
+ return $this->postAction();
+ } else {
+ return $this->errorOut('Unsupported mode');
+ }
+ }
+
+ protected function postAction()
+ {
+ if (!isset($_POST['me'])) {
+ return $this->errorOut('"me" parameter missing');
+ }
+ $me = $_POST['me'];
+
+ if (!isset($_POST['replyTo'])) {
+ return $this->errorOut('"replyTo" parameter missing');
+ }
+ $replyTo = $_POST['replyTo'];
+
+ if (!isset($_POST['content'])) {
+ return $this->errorOut('"content" parameter missing');
+ }
+ $content = $_POST['content'];
+
+ $accounts = PluginHost::getInstance()->get($this, 'accounts', []);
+ if (!isset($accounts[$me])) {
+ return $this->errorOut('"me" parameter invalid');
+ }
+ $account = $accounts[$me];
+
+ $links = $this->getLinks($me);
+ if (!count($links)) {
+ return $this->errorOut('No links found');
+ }
+ if (!isset($links['micropub'])) {
+ return $this->errorOut('No micropub endpoint found');
+ }
+
+ /* unfortunately fetch_file_contents() does not return headers
+ so we have to bring our own way to POST data */
+ $opts = [
+ 'http' => [
+ 'method' => 'POST',
+ 'header' => 'Content-type: application/x-www-form-urlencoded',
+ 'content' => http_build_query(
+ [
+ 'access_token' => $account['access_token'],
+ 'h' => 'entry',
+ 'in-reply-to' => $replyTo,
+ 'content' => $content,
+ ]
+ ),
+ 'ignore_errors' => true,
+ ]
+ ];
+ $stream = fopen(
+ $links['micropub'], 'r', false,
+ stream_context_create($opts)
+ );
+ $meta = stream_get_meta_data($stream);
+ $headers = $meta['wrapper_data'];
+ $content = stream_get_contents($stream);
+
+ //we hope there were no redirects and this is actually the only
+ // HTTP line in the headers
+ $status = array_shift($headers);
+ list($httpver, $code, $text) = explode(' ', $status, 3);
+ if ($code != 201 && $code != 202) {
+ return $this->errorOut(
+ 'An error occured: '
+ . $code . ' ' . $text
+ );
+ }
+
+ $location = null;
+ foreach ($headers as $header) {
+ $parts = explode(':', $header, 2);
+ if (count($parts) == 2 && strtolower($parts[0]) == 'location') {
+ $location = trim($parts[1]);
+ }
+ }
+ if ($location === null) {
+ return $this->errorOut(
+ 'Location header missing in successful creation response.'
+ );
+ }
+
+ header('Content-type: application/json');
+ echo json_encode(
+ [
+ 'code' => intval($code),
+ 'location' => $location,
+ 'message' => 'Post created',
+ ]
+ );
+ exit();
+ }
+
+ protected function authorizeAction($args = [])
+ {
+ if (count($args)) {
+ $url = array_shift($args);
+ } else if (isset($_POST['url'])) {
+ $url = $_POST['url'];
+ }
+ if (!filter_var($url, FILTER_VALIDATE_URL)) {
+ return $this->errorOut('Invalid URL');
+ }
+
+ //step 1: micropub discovery
+ $links = $this->getLinks($url);
+
+ if (!count($links)) {
+ return $this->errorOut('No links found');
+ }
+ if (!isset($links['micropub'])) {
+ return $this->errorOut('No micropub endpoint found');
+ }
+ if (!isset($links['token_endpoint'])) {
+ return $this->errorOut('No token endpoint found');
+ }
+ if (!isset($links['authorization_endpoint'])) {
+ return $this->errorOut('No authorization endpoint found');
+ }
+
+ $redirUrl = get_self_url_prefix() . '/backend.php'
+ . '?op=micropub&method=action&mode=authreturn';
+ $authUrl = $links['authorization_endpoint']
+ . '?me=' . $url
+ . '&redirect_uri=' . urlencode($redirUrl)
+ . '&client_id=' . urlencode(get_self_url_prefix())//FIXME: app info
+ //. '&state=' . 'FIXME'
+ . '&scope=create'
+ . '&response_type=code';
+ header('Location: ' . $authUrl);
+ echo $authUrl . "\n";
+ exit();
+ }
+
+ /**
+ * Return from authorization
+ */
+ public function authreturnAction()
+ {
+ if (!isset($_GET['me'])) {
+ return $this->errorOut('"me" parameter missing');
+ }
+ if (!isset($_GET['code'])) {
+ return $this->errorOut('"code" parameter missing');
+ }
+
+ $links = $this->getLinks($_GET['me']);
+ if (!isset($links['token_endpoint'])) {
+ return $this->errorOut('No token endpoint found');
+ }
+
+ //obtain access token from the code
+ $redirUrl = get_self_url_prefix() . '/backend.php'
+ . '?op=micropub&method=action&mode=authreturn';
+ $res = fetch_file_contents(
+ [
+ //FIXME: add accept header once this is fixed:
+ // https://discourse.tt-rss.org/t//207
+ 'url' => $links['token_endpoint'],
+ 'post_query' => [
+ 'grant_type' => 'authorization_code',
+ 'me' => $_GET['me'],
+ 'code' => $_GET['code'],
+ 'redirect_uri' => $redirUrl,
+ 'client_id' => get_self_url_prefix()
+ ]
+ ]
+ );
+
+ //we have no way to get the content type :/
+ if ($res{0} == '{') {
+ //json
+ $data = json_decode($res);
+ } else {
+ parse_str($res, $data);
+ }
+ if (!isset($data['access_token'])) {
+ return $this->errorOut('access token missing');
+ }
+ if (!isset($data['me'])) {
+ return $this->errorOut('access token missing');
+ }
+ if (!isset($data['scope'])) {
+ return $this->errorOut('scope token missing');
+ }
+
+ $host = PluginHost::getInstance();
+ $accounts = $host->get($this, 'accounts', []);
+ $accounts[$data['me']] = [
+ 'access_token' => $data['access_token'],
+ 'scope' => $data['scope'],
+ ];
+ $host->set($this, 'accounts', $accounts);
+
+ //all fine now.
+ header('Location: prefs.php');
+ }
+
+ /**
+ * Send an error message.
+ * Automatically in the correct format (plain text or json)
+ *
+ * @param string $msg Error message
+ *
+ * @return void
+ */
+ protected function errorOut($msg)
+ {
+ header('HTTP/1.0 400 Bad Request');
+
+ //this does not take "q"uality values into account, I know.
+ if (isset($_SERVER['HTTP_ACCEPT'])
+ && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false
+ ) {
+ //send json error
+ header('Content-type: application/json');
+ echo json_encode(
+ [
+ 'error' => $msg,
+ ]
+ );
+ } else {
+ header('Content-type: text/plain');
+ echo $msg . "\n";
+ }
+ exit(1);
+ }
+
+ /**
+ * Extract link relations from a given URL
+ */
+ protected function getLinks($url)
+ {
+ //FIXME: HTTP Link header support with HTTP2
+ $html = fetch_file_contents(
+ [
+ 'url' => $url,
+ ]
+ );
+ //Loading invalid HTML is tedious.
+ // quick hack with regex. yay!
+ preg_match_all('#<link[^>]+?>#', $html, $matches);
+ $links = [];
+ foreach ($matches[0] as $match) {
+ if (substr($match, -2) != '/>') {
+ //make it valid xml...
+ $match = substr($match, 0, -1) . '/>';
+ }
+ $sx = simplexml_load_string($match);
+ if (isset($sx['rel']) && isset($sx['href'])
+ && !isset($links[(string) $sx['rel']])
+ ) {
+ $links[(string) $sx['rel']] = (string) $sx['href'];
+ }
+ }
+ return $links;
+ }
+
+ function csrf_ignore($method)
+ {
+ return true;
+ }
+
+ /**
+ * Check which method is allowed via HTTP
+ */
+ function before($method)
+ {
+ if ($method == 'action') {
+ return true;
+ }
+ return false;
+ }
+
+ function after()
+ {
+ return true;