From: Christian Weiske Date: Mon, 5 Sep 2016 22:53:33 +0000 (+0200) Subject: WIP X-Git-Tag: v0.0.1~16 X-Git-Url: https://git.cweiske.de/shpub.git/commitdiff_plain/be472fa254f752d28b4254fc308d27c8057f2aab?ds=sidebyside WIP --- be472fa254f752d28b4254fc308d27c8057f2aab diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..5ba8153 --- /dev/null +++ b/README.rst @@ -0,0 +1,4 @@ +************************************** +shpub - micropub client for your shell +************************************** +Command line micropub client written in PHP. diff --git a/bin/shpub.php b/bin/shpub.php new file mode 100644 index 0000000..7e9729b --- /dev/null +++ b/bin/shpub.php @@ -0,0 +1,230 @@ +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); +} +?> diff --git a/src/shpub/Autoloader.php b/src/shpub/Autoloader.php new file mode 100644 index 0000000..5c0781b --- /dev/null +++ b/src/shpub/Autoloader.php @@ -0,0 +1,57 @@ + + * @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 + * @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 diff --git a/src/shpub/Cli.php b/src/shpub/Cli.php new file mode 100644 index 0000000..823e297 --- /dev/null +++ b/src/shpub/Cli.php @@ -0,0 +1,175 @@ +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.' + ); + } + } +} +?> diff --git a/src/shpub/Command/Connect.php b/src/shpub/Command/Connect.php new file mode 100644 index 0000000..867bbe3 --- /dev/null +++ b/src/shpub/Command/Connect.php @@ -0,0 +1,15 @@ +cfg = $cfg; + } + + public function run($server, $user, $key) + { + } +} +?> diff --git a/src/shpub/Config.php b/src/shpub/Config.php new file mode 100644 index 0000000..5da9c4a --- /dev/null +++ b/src/shpub/Config.php @@ -0,0 +1,101 @@ +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; + } +} +?> diff --git a/src/shpub/Config/Host.php b/src/shpub/Config/Host.php new file mode 100644 index 0000000..6ef05d9 --- /dev/null +++ b/src/shpub/Config/Host.php @@ -0,0 +1,41 @@ + diff --git a/src/shpub/Config/HostCache.php b/src/shpub/Config/HostCache.php new file mode 100644 index 0000000..03311c3 --- /dev/null +++ b/src/shpub/Config/HostCache.php @@ -0,0 +1,34 @@ + diff --git a/src/shpub/Log.php b/src/shpub/Log.php new file mode 100644 index 0000000..99d9694 --- /dev/null +++ b/src/shpub/Log.php @@ -0,0 +1,11 @@ +