--- /dev/null
+**************************************
+shpub - micropub client for your shell
+**************************************
+Command line micropub client written in PHP.
--- /dev/null
+<?php
+namespace shpub;
+
+if (file_exists(__DIR__ . '/../src/shpub/Autoloader.php')) {
+ include_once __DIR__ . '/../src/shpub/Autoloader.php';
+ Autoloader::register();
+}
+$cli = new Cli();
+$cli->run();
+exit();
+/**
+ * @link http://micropub.net/draft/
+ * @link http://indieweb.org/authorization-endpoint
+ */
+$server = 'http://anoweco.bogo/';
+$user = 'http://anoweco.bogo/user/3.htm';
+
+require_once 'HTTP/Request2.php';
+
+$endpoints = discoverEndpoints($server);
+list($accessToken, $userUrl) = getAuthCode($user, $endpoints);
+var_dump($endpoints, $accessToken, $userUrl);
+
+
+function getAuthCode($user, $endpoints)
+{
+ //fetch temporary authorization token
+ $redirect_uri = 'http://127.0.0.1:12345/callback';
+ $state = time();
+ $client_id = 'http://cweiske.de/shpub.htm';
+
+ $browserUrl = $endpoints->authorization
+ . '?me=' . urlencode($user)
+ . '&client_id=' . urlencode($client_id)
+ . '&redirect_uri=' . urlencode($redirect_uri)
+ . '&state=' . $state
+ . '&scope=post'
+ . '&response_type=code';
+ echo "To authenticate, open the following URL:\n"
+ . $browserUrl . "\n";
+
+ $authParams = startHttpServer();
+
+ if ($authParams['state'] != $state) {
+ logError('Wrong "state" parameter value');
+ exit(2);
+ }
+
+ //verify indieauth params
+ $req = new HTTP_Request2($endpoints->authorization, 'POST');
+ $req->setHeader('Content-Type: application/x-www-form-urlencoded');
+ $req->setBody(
+ http_build_query(
+ [
+ 'code' => $authParams['code'],
+ 'state' => $state,
+ 'client_id' => $client_id,
+ 'redirect_uri' => $redirect_uri,
+ ]
+ )
+ );
+ $res = $req->send();
+ if ($res->getHeader('content-type') != 'application/x-www-form-urlencoded') {
+ logError('Wrong content type in auth verification response');
+ exit(2);
+ }
+ parse_str($res->getBody(), $verifiedParams);
+ if (!isset($verifiedParams['me'])
+ || $verifiedParams['me'] !== $authParams['me']
+ ) {
+ logError('Non-matching "me" values');
+ exit(2);
+ }
+
+ $userUrl = $verifiedParams['me'];
+
+
+ //fetch permanent access token
+ $req = new HTTP_Request2($endpoints->token, 'POST');
+ $req->setHeader('Content-Type: application/x-www-form-urlencoded');
+ $req->setBody(
+ http_build_query(
+ [
+ 'me' => $userUrl,
+ 'code' => $authParams['code'],
+ 'redirect_uri' => $redirect_uri,
+ 'client_id' => $client_id,
+ 'state' => $state,
+ ]
+ )
+ );
+ $res = $req->send();
+ if ($res->getHeader('content-type') != 'application/x-www-form-urlencoded') {
+ logError('Wrong content type in auth verification response');
+ exit(2);
+ }
+ parse_str($res->getBody(), $tokenParams);
+ if (!isset($tokenParams['access_token'])) {
+ logError('"access_token" missing');
+ exit(2);
+ }
+
+ $accessToken = $tokenParams['access_token'];
+
+ return [$accessToken, $userUrl];
+}
+
+function startHttpServer()
+{
+ $responseOk = "HTTP/1.0 200 OK\r\n"
+ . "Content-Type: text/plain\r\n"
+ . "\r\n"
+ . "Ok. You may close this tab and return to the shell.\r\n";
+ $responseErr = "HTTP/1.0 400 Bad Request\r\n"
+ . "Content-Type: text/plain\r\n"
+ . "\r\n"
+ . "Bad Request\r\n";
+
+ //5 minutes should be enough for the user to confirm
+ ini_set('default_socket_timeout', 60 * 5);
+ $server = stream_socket_server(
+ 'tcp://127.0.0.1:12345', $errno, $errstr
+ );
+ if (!$server) {
+ //TODO: log
+ return false;
+ }
+
+ do {
+ $sock = stream_socket_accept($server);
+ if (!$sock) {
+ //TODO: log
+ exit(1);
+ }
+
+ $headers = [];
+ $body = null;
+ $content_length = 0;
+ //read request headers
+ while (false !== ($line = trim(fgets($sock)))) {
+ if ('' === $line) {
+ break;
+ }
+ if (preg_match('#^Content-Length:\s*([[:digit:]]+)\s*$#i', $line, $matches)) {
+ $content_length = (int) $matches[1];
+ }
+ $headers[] = $line;
+ }
+
+ // read content/body
+ if ($content_length > 0) {
+ $body = fread($sock, $content_length);
+ }
+
+ // send response
+ list($method, $url, $httpver) = explode(' ', $headers[0]);
+ if ($method == 'GET') {
+ $parts = parse_url($url);
+ if (isset($parts['path']) && $parts['path'] == '/callback'
+ && isset($parts['query'])
+ ) {
+ parse_str($parts['query'], $query);
+ if (isset($query['code'])
+ && isset($query['state'])
+ && isset($query['me'])
+ ) {
+ fwrite($sock, $responseOk);
+ fclose($sock);
+ return $query;
+ }
+ }
+ }
+
+ fwrite($sock, $responseErr);
+ fclose($sock);
+ } while(true);
+}
+
+class Config_Endpoints
+{
+ public $micropub;
+ public $media;
+ public $token;
+ public $authorization;
+}
+
+function discoverEndpoints($url)
+{
+ $cfg = new Config_Endpoints();
+
+ //TODO: discovery via link headers
+
+ $sx = simplexml_load_file($url);
+ $sx->registerXPathNamespace('h', 'http://www.w3.org/1999/xhtml');
+
+ $auths = $sx->xpath(
+ '/h:html/h:head/h:link[@rel="authorization_endpoint" and @href]'
+ );
+ if (!count($auths)) {
+ logError('No authorization endpoint found');
+ exit(1);
+ }
+ $cfg->authorization = (string) $auths[0]['href'];
+
+ $tokens = $sx->xpath(
+ '/h:html/h:head/h:link[@rel="token_endpoint" and @href]'
+ );
+ if (!count($tokens)) {
+ logError('No token endpoint found');
+ exit(1);
+ }
+ $cfg->token = (string) $tokens[0]['href'];
+
+ $mps = $sx->xpath(
+ '/h:html/h:head/h:link[@rel="micropub" and @href]'
+ );
+ if (!count($mps)) {
+ logError('No micropub endpoint found');
+ exit(1);
+ }
+ $cfg->micropub = (string) $mps[0]['href'];
+
+ return $cfg;
+}
+
+function logError($msg)
+{
+ file_put_contents('php://stderr', $msg . "\n", FILE_APPEND);
+}
+?>
--- /dev/null
+<?php
+/**
+ * Part of shpub
+ *
+ * PHP version 5
+ *
+ * @category Tools
+ * @package shpub
+ * @author Christian Weiske <cweiske@cweiske.de>
+ * @copyright 2014 Christian Weiske
+ * @license http://www.gnu.org/licenses/agpl.html GNU AGPL v3
+ * @link http://cweiske.de/shpub.htm
+ */
+namespace shpub;
+
+/**
+ * Class autoloader, PSR-0 compliant.
+ *
+ * @category Tools
+ * @package shpub
+ * @author Christian Weiske <cweiske@cweiske.de>
+ * @copyright 2014 Christian Weiske
+ * @license http://www.gnu.org/licenses/agpl.html GNU AGPL v3
+ * @version Release: @package_version@
+ * @link http://cweiske.de/shpub.htm
+ */
+class Autoloader
+{
+ /**
+ * Load the given class
+ *
+ * @param string $class Class name
+ *
+ * @return void
+ */
+ public function load($class)
+ {
+ $file = strtr($class, '_\\', '//') . '.php';
+ if (stream_resolve_include_path($file)) {
+ include $file;
+ }
+ }
+
+ /**
+ * Register this autoloader
+ *
+ * @return void
+ */
+ public static function register()
+ {
+ set_include_path(
+ get_include_path() . PATH_SEPARATOR . __DIR__ . '/../'
+ );
+ spl_autoload_register(array(new self(), 'load'));
+ }
+}
+?>
\ No newline at end of file
--- /dev/null
+<?php
+namespace shpub;
+
+class Cli
+{
+ /**
+ * @var Config
+ */
+ protected $cfg;
+
+ public function run()
+ {
+ $this->cfg = new Config();
+ $this->cfg->load();
+
+ try {
+ $optParser = $this->loadOptParser();
+ $res = $this->parseParameters($optParser);
+
+ switch ($res->command_name) {
+ case 'connect':
+ $cmd = new Command_Connect($this->cfg);
+ $cmd->run(
+ $res->command->args['server'],
+ $res->command->args['user'],
+ $res->command->args['key']
+ );
+ break;
+ case 'like':
+ $this->requireValidHost();
+ $cmd = new Command_Like($this->cfg->host);
+ $cmd->run($res->command->args['url']);
+ break;
+ default:
+ var_dump($this->cfg->host, $res);
+ Log::err('FIXME');
+ }
+ } catch (\Exception $e) {
+ echo 'Error: ' . $e->getMessage() . "\n";
+ exit(1);
+ }
+ }
+
+ /**
+ * Let the CLI option parser parse the options.
+ *
+ * @param object $parser Option parser
+ *
+ * @return object Parsed command line parameters
+ */
+ protected function parseParameters(\Console_CommandLine $optParser)
+ {
+ try {
+ $res = $optParser->parse();
+ $opts = $res->options;
+
+ $this->cfg->host = new Config_Host();
+ if ($opts['server'] !== null) {
+ $key = $this->cfg->getHostByName($opts['server']);
+ if ($key === null) {
+ $this->cfg->host->server = $opts['server'];
+ } else {
+ $this->cfg->host = $this->cfg->hosts[$key];
+ }
+ }
+ if ($opts['user'] !== null) {
+ $this->cfg->host->user = $opts['user'];
+ }
+
+ return $res;
+ } catch (\Exception $exc) {
+ $optParser->displayError($exc->getMessage());
+ }
+ }
+
+ /**
+ * Load parameters for the CLI option parser.
+ *
+ * @return \Console_CommandLine CLI option parser
+ */
+ protected function loadOptParser()
+ {
+ $optParser = new \Console_CommandLine();
+ $optParser->description = 'shpub';
+ $optParser->version = '0.0.0';
+ $optParser->subcommand_required = true;
+
+ $optParser->addOption(
+ 'server',
+ array(
+ 'short_name' => '-s',
+ 'long_name' => '--server',
+ 'description' => 'Server URL',
+ 'help_name' => 'URL',
+ 'action' => 'StoreString',
+ 'default' => null,
+ )
+ );
+ $optParser->addOption(
+ 'user',
+ array(
+ 'short_name' => '-u',
+ 'long_name' => '--user',
+ 'description' => 'User URL',
+ 'help_name' => 'URL',
+ 'action' => 'StoreString',
+ 'default' => null,
+ )
+ );
+
+ $cmd = $optParser->addCommand('connect');
+ $cmd->addArgument(
+ 'server',
+ [
+ 'optional' => false,
+ 'description' => 'Server URL',
+ ]
+ );
+ $cmd->addArgument(
+ 'user',
+ [
+ 'optional' => false,
+ 'description' => 'User URL',
+ ]
+ );
+ $cmd->addArgument(
+ 'key',
+ [
+ 'optional' => true,
+ 'description' => 'Short name (key)',
+ ]
+ );
+
+ //$cmd = $optParser->addCommand('post');
+ $cmd = $optParser->addCommand('reply');
+ $cmd->addArgument(
+ 'url',
+ [
+ 'optional' => false,
+ 'description' => 'URL that is replied to',
+ ]
+ );
+ $cmd->addArgument(
+ 'text',
+ [
+ 'optional' => false,
+ 'description' => 'Reply text',
+ ]
+ );
+
+ $cmd = $optParser->addCommand('like');
+ $cmd->addArgument(
+ 'url',
+ [
+ 'optional' => false,
+ 'description' => 'URL that is liked',
+ ]
+ );
+
+ return $optParser;
+ }
+
+ protected function requireValidHost()
+ {
+ if ($this->cfg->host->server === null
+ || $this->cfg->host->user === null
+ || $this->cfg->host->token === null
+ ) {
+ throw new \Exception(
+ 'Server data incomplete. "shpub connect" first.'
+ );
+ }
+ }
+}
+?>
--- /dev/null
+<?php
+namespace shpub;
+
+class Command_Connect
+{
+ public function __construct(Config $cfg)
+ {
+ $this->cfg = $cfg;
+ }
+
+ public function run($server, $user, $key)
+ {
+ }
+}
+?>
--- /dev/null
+<?php
+namespace shpub;
+
+class Config
+{
+ public $hosts = [];
+
+ /**
+ * Currently selected host.
+ *
+ * @var Host
+ */
+ public $host;
+
+ protected function getConfigFilePath()
+ {
+ if (!isset($_SERVER['HOME'])) {
+ return false;
+ }
+
+ return $_SERVER['HOME'] . '/.config/shpub.ini';
+ }
+
+ public function load()
+ {
+ $cfgFile = $this->getConfigFilePath();
+ if ($cfgFile == false) {
+ return false;
+ }
+
+ if (!file_exists($cfgFile) || !is_readable($cfgFile)) {
+ return false;
+ }
+
+ $data = parse_ini_file($cfgFile, true);
+ foreach ($data as $key => $val) {
+ if (!is_array($val)) {
+ continue;
+ }
+ $host = new Config_Host();
+ foreach ($val as $hostProp => $hostVal) {
+ if (!property_exists($host, $hostProp)) {
+ Log::err('Invalid config key "' . $hostProp . '"');
+ exit(1);
+ }
+ $host->$hostProp = $hostVal;
+ }
+ $this->hosts[$key] = $host;
+ }
+ }
+
+ public function save()
+ {
+ $str = '';
+ foreach ($this->hosts as $hostName => $host) {
+ if ($str != '') {
+ $str .= "\n";
+ }
+ $str .= '[' . $hostName . "]\n";
+ foreach ($host as $hostProp => $hostVal) {
+ if ($hostProp == 'cache') {
+ continue;
+ }
+ if ($hostVal == '') {
+ continue;
+ }
+ $str .= $hostProp . '=' . $hostVal . "\n";
+ }
+ }
+ file_put_contents($this->getConfigFilePath(), $str);
+ }
+
+ public function getDefaultHost()
+ {
+ if (!count($this->hosts)) {
+ return null;
+ }
+ foreach ($this->hosts as $key => $host) {
+ if ($host->default) {
+ return $host;
+ }
+ }
+
+ reset($this->hosts);
+ return key($this->hosts);
+ }
+
+ public function getHostByName($keyOrServer)
+ {
+ if (!count($this->hosts)) {
+ return null;
+ }
+ foreach ($this->hosts as $key => $host) {
+ if ($key == $keyOrServer || $host->server == $keyOrServer) {
+ return $key;
+ }
+ }
+ return null;
+ }
+}
+?>
--- /dev/null
+<?php
+namespace shpub;
+
+class Config_Host
+{
+ /**
+ * (Base) server URL
+ *
+ * @var string
+ */
+ public $server;
+
+ /**
+ * User URL
+ *
+ * @var string
+ */
+ public $user;
+
+ /**
+ * Micropub access token
+ *
+ * @var string
+ */
+ public $token;
+
+ /**
+ * Host information cache
+ *
+ * @var Config_HostCache
+ */
+ public $cache;
+
+ /**
+ * If this host is the default one
+ *
+ * @var boolean
+ */
+ public $default;
+}
+?>
--- /dev/null
+<?php
+namespace shpub;
+
+class Config_HostCache
+{
+ /**
+ * Micropub endpoint URL
+ *
+ * @var string
+ */
+ public $micropub;
+
+ /**
+ * Micropub media endpoint URL
+ *
+ * @var string
+ */
+ public $media;
+
+ /**
+ * Access token endpoint URL
+ *
+ * @var string
+ */
+ public $token;
+
+ /**
+ * Authorization endpoint URL
+ *
+ * @var string
+ */
+ public $auth;
+}
+?>
--- /dev/null
+<?php
+namespace shpub;
+
+class Log
+{
+ public static function err($msg)
+ {
+ file_put_contents('php://stderr', $msg . "\n", FILE_APPEND);
+ }
+}
+?>