Show micropub error descriptions
[tt-rss-micropub.git] / init.php
index 986266d..d561aa5 100644 (file)
--- a/init.php
+++ b/init.php
@@ -40,12 +40,12 @@ class Micropub extends Plugin implements IHandler
         return 2;
     }
 
+
     /**
      * Register our hooks
      */
     public function init(/*PluginHost*/ $host)
     {
-        parent::init($host);
         static::$myhost = $host;
         $host->add_hook($host::HOOK_PREFS_TAB, $this);
         $host->add_hook($host::HOOK_RENDER_ARTICLE, $this);
@@ -55,11 +55,6 @@ class Micropub extends Plugin implements IHandler
         );
     }
 
-    function get_css()
-    {
-        return file_get_contents(__DIR__ . '/init.css');
-    }
-
     /**
      * @param array $article Article data. Keys:
      *                       - id
@@ -88,7 +83,18 @@ class Micropub extends Plugin implements IHandler
             . '?reply=' . urlencode($article['link']);
         // did I tell you I hate dojo/dijit?
 
-        $accounts = array_keys(PluginHost::getInstance()->get($this, 'accounts', []));
+        $accounts = PluginHost::getInstance()->get($this, 'accounts', []);
+        if (!count($accounts)) {
+            return $article;
+        }
+
+        $accountUrls = array_keys($accounts);
+        $defaultAccount = null;
+        foreach ($accounts as $url => $account) {
+            if ($account['default']) {
+                $defaultAccount = $url;
+            }
+        }
 
         ob_start();
         include __DIR__ . '/commentform.phtml';
@@ -113,10 +119,30 @@ class Micropub extends Plugin implements IHandler
         }
 
         $accounts = PluginHost::getInstance()->get($this, 'accounts', []);
+        if (isset($_REQUEST['accordion'])
+            && $_REQUEST['accordion'] == 'micropub'
+        ) {
+            $accordionActive = 'selected="true"';
+        } else {
+            $accordionActive = '';
+        }
+
+        foreach ($accounts as $url => $account) {
+            $accounts[$url]['checked'] = '';
+            if ($account['default']) {
+                $accounts[$url]['checked'] = 'checked="checked"';
+            }
+        }
 
+        //FIXME: default identity
         include __DIR__ . '/settings.phtml';
     }
 
+    public function get_prefs_js()
+    {
+        return file_get_contents(__DIR__ . '/settings.js');
+    }
+
     /**
      * CLI command
      */
@@ -130,6 +156,12 @@ class Micropub extends Plugin implements IHandler
         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'])) {
@@ -144,34 +176,54 @@ class Micropub extends Plugin implements IHandler
             return $this->authreturnAction();
         } else if ($mode == 'post') {
             return $this->postAction();
+        } else if ($mode == 'deleteIdentity') {
+            return $this->deleteIdentityAction();
+        } else if ($mode == 'setDefaultIdentity') {
+            return $this->setDefaultIdentityAction();
         } else {
-            $this->errorOut('Unsupported mode');
+            return $this->errorOut('Unsupported mode');
         }
     }
 
+    /**
+     * Post a comment, like or bookmark via micropub
+     */
     protected function postAction()
     {
-        if (!isset($_POST['me'])) {
-            return $this->errorOut('"me" parameter missing');
+        $action = 'comment';
+        if (isset($_POST['action'])) {
+            $action = trim($_POST['action']);
         }
-        $me = $_POST['me'];
-
-        if (!isset($_POST['replyTo'])) {
-            return $this->errorOut('"replyTo" parameter missing');
+        if (array_search($action, ['bookmark', 'comment', 'like']) === false) {
+            return $this->errorOut('"action" parameter invalid');
         }
-        $replyTo = $_POST['replyTo'];
 
-        if (!isset($_POST['content'])) {
-            return $this->errorOut('"content" parameter missing');
+        if (!isset($_POST['me'])) {
+            return $this->errorOut('"me" parameter missing');
         }
-        $content = $_POST['content'];
-
+        $me = trim($_POST['me']);
         $accounts = PluginHost::getInstance()->get($this, 'accounts', []);
         if (!isset($accounts[$me])) {
             return $this->errorOut('"me" parameter invalid');
         }
         $account = $accounts[$me];
 
+        if (!isset($_POST['postUrl'])) {
+            return $this->errorOut('"postUrl" parameter missing');
+        }
+        $postUrl = trim($_POST['postUrl']);
+
+        if ($action == 'comment') {
+            if (!isset($_POST['content'])) {
+                return $this->errorOut('"content" parameter missing');
+            }
+            $content = trim($_POST['content']);
+            if (!strlen($_POST['content'])) {
+                return $this->errorOut('"content" is empty');
+            }
+        }
+
+
         $links = $this->getLinks($me);
         if (!count($links)) {
             return $this->errorOut('No links found');
@@ -180,35 +232,83 @@ class Micropub extends Plugin implements IHandler
             return $this->errorOut('No micropub endpoint found');
         }
 
-        $res = fetch_file_contents(
-            [
-                //FIXME: add content-type header once this is fixed:
-                // https://discourse.tt-rss.org/t//207
-                'url'        => $links['micropub'],
-                //we use http_build_query to force cURL
-                // to use x-www-form-urlencoded
-                'post_query' => http_build_query(
-                    [
-                        'access_token' => $account['access_token'],
-                        'h'            => 'entry',
-                        'in-reply-to'  => $replyTo,
-                        'content'      => $content,
-                    ]
-                ),
-                'followlocation' => false,
+        $parameters = [
+            'access_token' => $account['access_token'],
+            'h'            => 'entry',
+        ];
+
+        if ($action == 'bookmark') {
+            $parameters['bookmark-of'] = $postUrl;
+
+        } else if ($action == 'comment') {
+            $parameters['in-reply-to'] = $postUrl;
+            $parameters['content']     = $content;
+
+        } else if ($action == 'like') {
+            $parameters['like-of'] = $postUrl;
+        }
+
+
+        /* 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($parameters),
+                '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) {
+            $errData = json_decode($content);
+            if (isset($errData->error_description)
+                && $errData->error_description != ''
+            ) {
+                return $this->errorOut(
+                    'Error creating post: '
+                    . $errData->error_description
+                );
+            }
+            return $this->errorOut(
+                'Error creating post: '
+                . $code . ' ' . $text.$content
+            );
+        }
 
-        if ($GLOBALS['fetch_last_error_code'] == 201) {
-            //FIXME: extract location header
-            echo "OK, comment post created\n";
-        } else {
-            $this->errorOut(
-                'An error occured: '
-                . $GLOBALS['fetch_last_error_code']
-                . ' ' . $GLOBALS['fetch_last_error_code_content']
+        $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 = [])
@@ -310,18 +410,136 @@ class Micropub extends Plugin implements IHandler
             'access_token' => $data['access_token'],
             'scope'        => $data['scope'],
         ];
+        $accounts = $this->fixDefaultIdentity($accounts);
         $host->set($this, 'accounts', $accounts);
 
         //all fine now.
-        header('Location: prefs.php');
+        //the accordion parameter will never work
+        // because fox has serious mental problems
+        // https://discourse.tt-rss.org/t/open-a-certain-accordion-in-preferences-by-url-parameter/234
+        header('Location: prefs.php?accordion=micropub');
     }
 
+    /**
+     * Backend preferences action: Remove a given account
+     */
+    protected function deleteIdentityAction()
+    {
+        if (!isset($_POST['me'])) {
+            return $this->errorOut('"me" parameter missing');
+        }
+        $me = trim($_POST['me']);
+
+        $host = PluginHost::getInstance();
+        $accounts = $host->get($this, 'accounts', []);
+        if (!isset($accounts[$me])) {
+            return $this->errorOut('Unknown identity');
+        }
+
+        unset($accounts[$me]);
+        $accounts = $this->fixDefaultIdentity($accounts);
+        $host->set($this, 'accounts', $accounts);
+
+        header('Content-type: application/json');
+        echo json_encode(
+            [
+                'code'     => '200',
+                'message'  => 'Identity removed',
+            ]
+        );
+        exit();
+    }
+
+    /**
+     * Backend preferences action: Make a given account the default
+     */
+    protected function setDefaultIdentityAction()
+    {
+        if (!isset($_POST['me'])) {
+            return $this->errorOut('"me" parameter missing');
+        }
+        $me = trim($_POST['me']);
+
+        $host = PluginHost::getInstance();
+        $accounts = $host->get($this, 'accounts', []);
+        if (!isset($accounts[$me])) {
+            return $this->errorOut('Unknown identity');
+        }
+        foreach ($accounts as $url => $data) {
+            $accounts[$url]['default'] = ($url == $me);
+        }
+        $host->set($this, 'accounts', $accounts);
+
+        header('Content-type: application/json');
+        echo json_encode(
+            [
+                'code'     => '200',
+                'message'  => 'Default account set',
+            ]
+        );
+        exit();
+    }
+
+    /**
+     * Set the default identity if there is none
+     *
+     * @param array $accounts Array of account data arrays
+     *
+     * @return array Array of account data arrays
+     */
+    protected function fixDefaultIdentity($accounts)
+    {
+        if (!count($accounts)) {
+            return $accounts;
+        }
+
+        $hasDefault = false;
+        foreach ($accounts as $account) {
+            if ($account['default']) {
+                $hasDefault = true;
+            }
+        }
+
+        if (!$hasDefault) {
+            reset($accounts);
+            $accounts[key($accounts)]['default'] = true;
+        }
+        return $accounts;
+    }
+
+    /**
+     * Send an error message.
+     * Automatically in the correct format (plain text or json)
+     *
+     * @param string $msg Error message
+     *
+     * @return void
+     */
     protected function errorOut($msg)
     {
-        echo $msg . "\n";
+        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
@@ -349,9 +567,27 @@ class Micropub extends Plugin implements IHandler
         return $links;
     }
 
+    /**
+     * If a valid CSRF token is necessary or not
+     *
+     * @param string $method Plugin method name (here: "action")
+     *
+     * @return boolean True if an invalid CSRF token shall be ignored
+     */
     function csrf_ignore($method)
     {
-        return true;
+        $mode = null;
+        if (isset($_POST['mode'])) {
+            $mode = $_POST['mode'];
+        } else if (isset($_GET['mode'])) {
+            $mode = $_GET['mode'];
+        }
+
+        if ($mode == 'authreturn') {
+            return true;
+        }
+
+        return false;
     }
 
     /**