From: Christian Weiske Date: Wed, 3 Aug 2016 18:32:14 +0000 (+0200) Subject: wip X-Git-Tag: v1.0.0~43 X-Git-Url: https://git.cweiske.de/anoweco.git/commitdiff_plain/ccb7bb3c75555c01e7dbc78e0b971abc86f3a59d wip --- ccb7bb3c75555c01e7dbc78e0b971abc86f3a59d diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d041e45 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/data/config.php diff --git a/data/schema.sql b/data/schema.sql new file mode 100644 index 0000000..61eae79 --- /dev/null +++ b/data/schema.sql @@ -0,0 +1,23 @@ + + + +CREATE TABLE `comments` ( + `comment_id` int(11) NOT NULL AUTO_INCREMENT, + `comment_user_id` int(11) NOT NULL, + `comment_published` datetime NOT NULL, + `comment_of_url` varchar(2048) NOT NULL, + `comment_type` varchar(32) NOT NULL, + `comment_json` mediumtext NOT NULL, + PRIMARY KEY (`comment_id`) +) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8; + + +CREATE TABLE `users` ( + `user_id` int(11) NOT NULL AUTO_INCREMENT, + `user_name` varchar(128) NOT NULL, + `user_email` varchar(256) NOT NULL, + `user_imageurl` varchar(1024) NOT NULL, + PRIMARY KEY (`user_id`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; + + diff --git a/data/templates/comment.htm b/data/templates/comment.htm new file mode 100644 index 0000000..24c2769 --- /dev/null +++ b/data/templates/comment.htm @@ -0,0 +1,23 @@ + + + + Comment to {{attribute(json, 'in-reply-to').0}} + + +

Comment #{{crow.comment_id}}

+

+ {% spaceless %} + + + {{author.name}} + {% endspaceless %} + wrote the following reply to + {{attribute(json, 'in-reply-to').0}}: +

+
{{htmlContent|raw}}
+

+ Reply to this comment +

+ + diff --git a/data/templates/user.htm b/data/templates/user.htm new file mode 100644 index 0000000..9f50013 --- /dev/null +++ b/data/templates/user.htm @@ -0,0 +1,16 @@ + + + + Profile of {{name}} + + + + + + +

{{name}}

+
+ +
+ + diff --git a/scripts/dump-schema.sh b/scripts/dump-schema.sh new file mode 100755 index 0000000..da33d42 --- /dev/null +++ b/scripts/dump-schema.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# update data/schema.sql +cd "`dirname "$0"`" +mysqldump\ + --skip-add-locks\ + --skip-disable-keys\ + --skip-add-drop-table\ + --no-data\ + -uanoweco -panoweco\ + anoweco\ + | grep -v '^/\\*'\ + | grep -v '^--'\ + > ../data/schema.sql diff --git a/src/anoweco/Storage.php b/src/anoweco/Storage.php new file mode 100644 index 0000000..a843774 --- /dev/null +++ b/src/anoweco/Storage.php @@ -0,0 +1,101 @@ +db = new \PDO($dbdsn, $dbuser, $dbpass); + $this->db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + } + + /** + * Store a new comment into the database + * + * @param object $json Micropub create JSON + * @param integer $userId ID of the user whom this comment belongs + * + * @return integer Comment ID + * @throws \Exception + */ + public function addComment($json, $userId) + { + $stmt = $this->db->prepare( + 'INSERT INTO comments SET' + . ' comment_user_id = :userId' + . ', comment_published = NOW()' + . ', comment_of_url = :ofUrl' + . ', comment_type = :type' + . ', comment_json = :json' + ); + + $ofUrl = ''; + if (isset($json->properties->{'in-reply-to'})) { + $ofUrl = reset($json->properties->{'in-reply-to'}); + } + $stmt->execute( + array( + ':userId' => $userId, + ':ofUrl' => $ofUrl, + ':type' => reset($json->type), + ':json' => json_encode($json), + ) + ); + return $this->db->lastInsertId(); + } + + /** + * @return null|object NULL if not found, comment object otherwise + */ + public function getComment($id) + { + $stmt = $this->db->prepare( + 'SELECT * FROM comments WHERE comment_id = ?' + ); + $stmt->execute([$id]); + $row = $stmt->fetchObject(); + + if ($row === false) { + return null; + } + + $json = json_decode($row->comment_json); + $json->Xrow = $row; + //FIXME: load user + + $stmt = $this->db->prepare( + 'SELECT * FROM users WHERE user_id = ?' + ); + $stmt->execute([$row->comment_user_id]); + $rowUser = $stmt->fetchObject(); + if ($rowUser === false) { + $rowUser = (object) array( + 'user_id' => 0, + 'user_name' => 'Anonymous', + 'user_imageurl' => '', + ); + } + + $json->user = $rowUser; + return $json; + } + + /** + * @return null|object NULL if not found, user database row otherwise + */ + public function getUser($id) + { + $stmt = $this->db->prepare( + 'SELECT * FROM users WHERE user_id = ?' + ); + $stmt->execute([$id]); + $row = $stmt->fetchObject(); + + if ($row === false) { + return null; + } + return $row; + } +} +?> diff --git a/src/anoweco/Urls.php b/src/anoweco/Urls.php new file mode 100644 index 0000000..a82d36c --- /dev/null +++ b/src/anoweco/Urls.php @@ -0,0 +1,34 @@ +user_imageurl != '') { + return $rowUser->user_imageurl; + } + return static::full('/img/anonymous.svg'); + } + + public static function full($str) + { + if (!isset($_SERVER['REQUEST_SCHEME'])) { + $_SERVER['REQUEST_SCHEME'] = 'http'; + } + return $_SERVER['REQUEST_SCHEME'] . '://' + . $_SERVER['HTTP_HOST'] + . $str; + } +} +?> diff --git a/src/anoweco/autoload.php b/src/anoweco/autoload.php new file mode 100644 index 0000000..5fb876a --- /dev/null +++ b/src/anoweco/autoload.php @@ -0,0 +1,37 @@ + + */ +if (file_exists(__DIR__ . '/../../lib/PEAR.php')) { + //phing-installed dependencies available ("phing collectdeps") + set_include_path( + __DIR__ . '/../' + . PATH_SEPARATOR . __DIR__ . '/../../lib/' + . PATH_SEPARATOR . '.' + ); +} else if (file_exists(__DIR__ . '/../../lib/autoload.php')) { + //composer-installed dependencies available + set_include_path( + __DIR__ . '/../' + . PATH_SEPARATOR . '.' + ); + require_once __DIR__ . '/../../lib/autoload.php'; +} else { + //use default include path for dependencies + set_include_path( + __DIR__ . '/../' + . PATH_SEPARATOR . get_include_path() + ); +} + +spl_autoload_register( + function ($class) { + $file = str_replace(array('\\', '_'), '/', $class) . '.php'; + if (stream_resolve_include_path($file)) { + require $file; + } + } +); +?> diff --git a/www/.htaccess b/www/.htaccess new file mode 100644 index 0000000..eae6500 --- /dev/null +++ b/www/.htaccess @@ -0,0 +1,10 @@ +# PHP does not see the "Authorization" header by default +# so we have to manually pass it to PHP +SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 + +RewriteEngine On +RewriteBase / +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^comment/([0-9]+).htm$ comment.php?id=$1 +RewriteRule ^user/([0-9]+).htm$ user.php?id=$1 diff --git a/www/auth.php b/www/auth.php new file mode 100644 index 0000000..847fbb4 --- /dev/null +++ b/www/auth.php @@ -0,0 +1,16 @@ +setQueryVariable('code', $token); + $url->setQueryVariable('me', $_GET['me']); + $url->setQueryVariable('state', $_GET['state']); + header('Location: ' . $url->getURL()); + exit(); +} else if ($_SERVER['REQUEST_METHOD'] == 'POST') { +} +?> diff --git a/www/comment.php b/www/comment.php new file mode 100644 index 0000000..6dc1b0c --- /dev/null +++ b/www/comment.php @@ -0,0 +1,54 @@ +getComment($id); +if ($comment === null) { + header('HTTP/1.0 404 Not Found'); + header('Content-Type: text/plain'); + echo "Comment not found\n"; + exit(1); +} + +if (isset($comment->properties->content['html'])) { + $htmlContent = $comment->properties->content['html']; +} else { + $htmlContent = nl2br($comment->properties->content[0]); +} + +$rowComment = $comment->Xrow; +$rowUser = $comment->user; +render( + 'comment', + array( + 'json' => $comment->properties, + 'crow' => $rowComment, + 'comment' => $comment, + 'author' => array( + 'name' => $rowUser->user_name, + 'url' => Urls::full(Urls::user($rowUser->user_id)), + 'imageurl' => Urls::userImg($rowUser), + ), + 'htmlContent' => $htmlContent, + 'replyUrl' => Urls::full( + '/reply.php?url=' . urlencode(Urls::full($rowComment->comment_id)) + ), + ) +); +?> diff --git a/www/css/user.css b/www/css/user.css new file mode 100644 index 0000000..cae3468 --- /dev/null +++ b/www/css/user.css @@ -0,0 +1,47 @@ +* { + box-sizing: border-box; +} +body { + display: flex; + height: 100%; + margin: 0px; + background-color: #EEF; +} +h1 { + flex: 1 1 50%; + align-self: stretch; + text-align: right; + margin-top: auto; + margin-bottom: auto; + order: 1; +} +#img { + flex: 1 1 50%; + align-self: stretch; + text-align: center; + margin: auto; + order: 2; +} +img { + width: 100%; + height: auto; + max-width: 50%; + max-height: 50%; +} + +@media all and (max-width: 640px) { + body { + flex-flow: column; + } + h1 { + text-align: center; + } + #img { + order: 0; + } + img { + height: 50%; + width: auto; + margin-top: 13%; + } +} diff --git a/www/img/anonymous.svg b/www/img/anonymous.svg new file mode 100644 index 0000000..ca26d18 --- /dev/null +++ b/www/img/anonymous.svg @@ -0,0 +1,18 @@ + + + + + Abstract user icon + + + + + \ No newline at end of file diff --git a/www/index.htm b/www/index.htm new file mode 100644 index 0000000..1a96ade --- /dev/null +++ b/www/index.htm @@ -0,0 +1,14 @@ + + + anoweco + + + + + +

anoweco

+

+ Anonymous web comments. +

+ + diff --git a/www/micropub.php b/www/micropub.php new file mode 100644 index 0000000..43c41b1 --- /dev/null +++ b/www/micropub.php @@ -0,0 +1,154 @@ + + */ +header('HTTP/1.0 500 Internal Server Error'); +require 'www-header.php'; + +/** + * Send out an error + * + * @param string $status HTTP status code line + * @param string $code One of the allowed status types: + * - forbidden + * - insufficient_scope + * - invalid_request + * - not_found + * @param string $description + */ +function error($status, $code, $description) +{ + header($status); + header('Content-Type: application/json'); + echo json_encode( + ['error' => $code, 'error_description' => $description] + ) . "\n"; + exit(1); +} + +function handleCreate($json) +{ + if (!isset($json->properties->{'in-reply-to'})) { + error( + 'HTTP/1.0 400 Bad Request', + 'invalid_request', + 'Only replies accepted' + ); + } + //FIXME: read bearer token + //FIXME: get user ID + $storage = new Storage(); + try { + $id = $storage->addComment($json, 0); + + header('HTTP/1.0 201 Created'); + header('Location: ' . Urls::full(Urls::comment($id))); + exit(); + } catch (\Exception $e) { + //FIXME: return correct status code + header('HTTP/1.0 500 Internal Server Error'); + exit(); + } +} + +if ($_SERVER['REQUEST_METHOD'] == 'GET') { + if (!isset($_GET['q'])) { + error( + 'HTTP/1.1 400 Bad Request', + 'invalid_request', + 'Parameter "q" missing.' + ); + } else if ($_GET['q'] === 'config') { + header('HTTP/1.0 200 OK'); + header('Content-Type: application/json'); + echo '{}'; + exit(); + } else if ($_GET['q'] === 'syndicate-to') { + header('HTTP/1.0 200 OK'); + header('Content-Type: application/json'); + echo '{}'; + exit(); + } else { + //FIXME: maybe implement $q=source + header('HTTP/1.1 501 Not Implemented'); + header('Content-Type: text/plain'); + echo 'Unsupported "q" value: ' . $_GET['q'] . "\n"; + exit(); + } +} else if ($_SERVER['REQUEST_METHOD'] == 'POST') { + if (!isset($_SERVER['CONTENT_TYPE'])) { + error( + 'HTTP/1.1 400 Bad Request', + 'invalid_request', + 'Content-Type header missing.' + ); + } + $ctype = $_SERVER['CONTENT_TYPE']; + if ($ctype == 'application/x-www-form-urlencoded') { + if (!isset($_POST['action'])) { + $_POST['action'] = 'create'; + } + if ($_POST['action'] != 'create') { + header('HTTP/1.1 501 Not Implemented'); + header('Content-Type: text/plain'); + echo "Creation of posts supported only\n"; + exit(); + } + + $data = $_POST; + $base = (object) [ + 'type' => ['h-entry'], + ]; + if (isset($data['h'])) { + $base->type = ['h-' . $data['h']]; + unset($data['h']); + } + //reserved properties + foreach (['access_token', 'q', 'url', 'action'] as $key) { + if (isset($data[$key])) { + $base->$key = $data[$key]; + unset($data[$key]); + } + } + //"mp-" reserved for future use + foreach ($data as $key => $value) { + if (substr($key, 0, 3) == 'mp-') { + $base->$key = $value; + unset($data[$key]); + } else if (!is_array($value)) { + //convert to array + $data[$key] = [$value]; + } + } + $json = $base; + $json->properties = (object) $data; + handleCreate($json); + } else if ($ctype == 'application/javascript') { + $input = file_get_contents('php://stdin'); + $json = json_decode($input); + if ($json === null) { + error( + 'HTTP/1.1 400 Bad Request', + 'invalid_request', + 'Invalid JSON' + ); + } + handleCreate($json); + } else { + error( + 'HTTP/1.1 400 Bad Request', + 'invalid_request', + 'Unsupported POST content type' + ); + } +} else { + error( + 'HTTP/1.0 400 Bad Request', + 'invalid_request', + 'Unsupported HTTP request method' + ); +} +?> \ No newline at end of file diff --git a/www/token.php b/www/token.php new file mode 100644 index 0000000..bf10e70 --- /dev/null +++ b/www/token.php @@ -0,0 +1,100 @@ + $me, + 'client_id' => $client_id, + 'scope' => $scope, + ) + ); + +} else if ($_SERVER['REQUEST_METHOD'] == 'POST') { + //generate token + $me = verifyUrlParameter($_POST, 'me'); + $redirect_uri = verifyUrlParameter($_POST, 'redirect_uri'); + $client_id = verifyUrlParameter($_POST, 'client_id'); + $code = verifyParameter($_POST, 'code');//auth token + $state = getOptionalParameter($_POST, 'state', null); + //FIXME: check if code and state are set + //FIXME: check auth endpoint if parameters are valid + // and to get the scope + $scope = 'post'; + + //FIXME: use real encryption + $access_token = '

"\'' . json_encode( + array( + 'me' => $me, + 'client_id' => $client_id, + 'scope' => $scope + ) + ); + header('HTTP/1.0 200 OK'); + header('Content-type: application/x-www-form-urlencoded'); + echo http_build_query( + array( + 'access_token' => $access_token, + 'me' => $me, + 'scope' => $scope + ) + ); +} +?> diff --git a/www/user.php b/www/user.php new file mode 100644 index 0000000..95b817f --- /dev/null +++ b/www/user.php @@ -0,0 +1,38 @@ +getUser($id); +if ($rowUser === null) { + header('HTTP/1.0 404 Not Found'); + header('Content-Type: text/plain'); + echo "User not found\n"; + exit(1); +} + +render( + 'user', + array( + 'baseurl' => Urls::full('/'), + 'name' => $rowUser->user_name, + 'url' => Urls::full(Urls::user($rowUser->user_id)), + 'imageurl' => Urls::userImg($rowUser), + ) +); +?> diff --git a/www/www-header.php b/www/www-header.php new file mode 100644 index 0000000..a221ca5 --- /dev/null +++ b/www/www-header.php @@ -0,0 +1,26 @@ + '/path/to/compilation_cache', + 'debug' => true + ) +); +//$twig->addExtension(new Twig_Extension_Debug()); + +function render($tplname, $vars = array(), $return = false) +{ + $template = $GLOBALS['twig']->loadTemplate($tplname . '.htm'); + + if ($return) { + return $template->render($vars); + } else { + echo $template->render($vars); + } +} +?> \ No newline at end of file