X-Git-Url: https://git.cweiske.de/tt-rss-micropub.git/blobdiff_plain/f9455a1dfd41897773093475019485226905d264..0851b177ef2a5bbc2b56ff6454029b9ff6dded83:/init.php
diff --git a/init.php b/init.php
index 8c5558c..2fbabab 100644
--- a/init.php
+++ b/init.php
@@ -8,67 +8,426 @@
* @license AGPLv3 or later
* @link https://www.w3.org/TR/micropub/
*/
-class Micropub extends Plugin
+class Micropub extends Plugin implements IHandler
{
+ /**
+ * Dumb workaround for "private $host" in Plugin class
+ * + the re-creation of the plugin instance without calling init().
+ *
+ * @var PluginHost
+ * @see https://discourse.tt-rss.org/t//208
+ * @see https://discourse.tt-rss.org/t//209
+ */
+ protected static $myhost;
+
+ public function __construct()
+ {
+ //do nothing. only here to not have micropub() called as constructor
+ }
+
public function about()
{
- return array(
+ return [
0.1,
'Micropub',
'cweiske',
false
- );
+ ];
}
-
- public function init($host)
+
+ public function api_version()
{
- $host->add_hook($host::HOOK_RENDER_ARTICLE, $this);
+ return 2;
}
- function get_css()
+
+ /**
+ * Register our hooks
+ */
+ public function init(/*PluginHost*/ $host)
{
- return file_get_contents(__DIR__ . '/init.css');
+ static::$myhost = $host;
+ $host->add_hook($host::HOOK_PREFS_TAB, $this);
+ $host->add_hook($host::HOOK_RENDER_ARTICLE, $this);
+ //command line option --micropub
+ $host->add_command(
+ 'micropub', 'Add Micropub identity', $this, ':', 'MODE'
+ );
}
/**
* @param array $article Article data. Keys:
- * - id
- * - title
- * - link
- * - content
- * - feed_id
- * - comments
- * - int_id
- * - lang
- * - updated
- * - site_url
- * - feed_title
- * - hide_images
- * - always_display_enclosures
- * - num_comments
- * - author
- * - guid
- * - orig_feed_id
- * - note
- * - tags
+ * - id
+ * - title
+ * - link
+ * - content
+ * - feed_id
+ * - comments
+ * - int_id
+ * - lang
+ * - updated
+ * - site_url
+ * - feed_title
+ * - hide_images
+ * - always_display_enclosures
+ * - num_comments
+ * - author
+ * - guid
+ * - orig_feed_id
+ * - note
+ * - tags
*/
public function hook_render_article($article)
{
$quillUrl = 'https://quill.p3k.io/new'
. '?reply=' . urlencode($article['link']);
- $hQuillUrl = htmlspecialchars($quillUrl);
// did I tell you I hate dojo/dijit?
- $article['content'] .= '
';
+
+ $accounts = array_keys(PluginHost::getInstance()->get($this, 'accounts', []));
+ if (!count($accounts)) {
+ return $article;
+ }
+ ob_start();
+ include __DIR__ . '/commentform.phtml';
+ $html = ob_get_clean();
+ $article['content'] .= $html;
+
return $article;
}
- public function api_version()
+ /**
+ * Render our configuration page.
+ * Directly echo it out.
+ *
+ * @param string $args Preferences tab that is currently open
+ *
+ * @return void
+ */
+ public function hook_prefs_tab($args)
{
- return 2;
+ 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('#]+?>#', $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;
}
}
?>