--- /dev/null
+/data/config.php
--- /dev/null
+
+
+
+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;
+
+
--- /dev/null
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf-8"/>
+ <title>Comment to {{attribute(json, 'in-reply-to').0}}</title>
+ </head>
+ <body class="h-entry">
+ <h1>Comment #{{crow.comment_id}}</h1>
+ <p>
+ {% spaceless %}
+ <a class="p-author h-card" href="{{author.url}}">
+ <img class="u-photo" src="{{author.imageurl}}" alt="" width="32" height="32" />
+ {{author.name}}</a>
+ {% endspaceless %}
+ wrote the following reply to
+ <a rel="in-reply-to u-in-reply-to"
+ href="{{attribute(json, 'in-reply-to').0}}">{{attribute(json, 'in-reply-to').0}}</a>:
+ </p>
+ <div class="e-content">{{htmlContent|raw}}</div>
+ <p>
+ <a href="{{replyUrl}}">Reply to this comment</a>
+ </p>
+ </body>
+</html>
--- /dev/null
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf-8"/>
+ <title>Profile of {{name}}</title>
+ <link rel="authorization_endpoint" href="{{baseurl}}auth.php"/>
+ <link rel="token_endpoint" href="{{baseurl}}token.php"/>
+ <link rel="micropub" href="{{baseurl}}micropub.php"/>
+ <link rel="stylesheet" type="text/css" href="/css/user.css"/>
+ </head>
+ <body class="h-card">
+ <h1 class="p-name">{{name}}</h1>
+ <div id="img">
+ <img class="u-photo" src="{{imageurl}}" alt="" height="100" width="100"/>
+ </div>
+ </body>
+</html>
--- /dev/null
+#!/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
--- /dev/null
+<?php
+namespace anoweco;
+
+class Storage
+{
+ public function __construct()
+ {
+ require __DIR__ . '/../../data/config.php';
+ $this->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;
+ }
+}
+?>
--- /dev/null
+<?php
+namespace anoweco;
+
+class Urls
+{
+ public static function comment($id)
+ {
+ return '/comment/' . intval($id) . '.htm';
+ }
+
+ public static function user($id)
+ {
+ return '/user/' . intval($id) . '.htm';
+ }
+
+ public static function userImg($rowUser)
+ {
+ if ($rowUser->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;
+ }
+}
+?>
--- /dev/null
+<?php
+/**
+ * Autoloader setup for anoweco
+ *
+ * @author Christian Weiske <cweiske@cweiske.de>
+ */
+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;
+ }
+ }
+);
+?>
--- /dev/null
+# 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
--- /dev/null
+<?php
+require_once 'Net/URL2.php';
+
+header('IndieAuth: authorization_endpoint');
+if ($_SERVER['REQUEST_METHOD'] == 'GET') {
+ //var_dump($_GET);die();
+ $token = 'keinthema';
+ $url = new Net_URL2($_GET['redirect_uri']);
+ $url->setQueryVariable('code', $token);
+ $url->setQueryVariable('me', $_GET['me']);
+ $url->setQueryVariable('state', $_GET['state']);
+ header('Location: ' . $url->getURL());
+ exit();
+} else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+}
+?>
--- /dev/null
+<?php
+namespace anoweco;
+require 'www-header.php';
+
+if (!isset($_GET['id'])) {
+ header('HTTP/1.0 400 Bad Request');
+ header('Content-Type: text/plain');
+ echo "id parameter missing\n";
+ exit(1);
+}
+if (!is_numeric($_GET['id'])) {
+ header('HTTP/1.0 400 Bad Request');
+ header('Content-Type: text/plain');
+ echo "Invalid id parameter value\n";
+ exit(1);
+}
+
+$id = intval($_GET['id']);
+
+$storage = new Storage();
+$comment = $storage->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))
+ ),
+ )
+);
+?>
--- /dev/null
+* {
+ 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%;
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>\r
+<!-- Written by Treer (gitlab.com/Treer) -->\r
+<svg \r
+ version="1.1" \r
+ xmlns="http://www.w3.org/2000/svg" \r
+ xmlns:xlink="http://www.w3.org/1999/xlink" \r
+ width="600" \r
+ height="600"\r
+ stroke="black"\r
+ stroke-width="30"\r
+ fill="none">\r
+\r
+ <title>Abstract user icon</title>\r
+ \r
+ <circle cx="300" cy="300" r="265" />\r
+ <circle cx="300" cy="230" r="115" /> \r
+ <path d="M106.81863443903,481.4 a205,205 1 0,1 386.36273112194,0" stroke-linecap="butt" />\r
+</svg>
\ No newline at end of file
--- /dev/null
+<html>
+ <head>
+ <title>anoweco</title>
+ <link rel="authorization_endpoint" href="http://anoweco.bogo/auth.php"/>
+ <link rel="token_endpoint" href="http://anoweco.bogo/token.php"/>
+ <link rel="micropub" href="http://anoweco.bogo/micropub.php"/>
+ </head>
+ <body>
+ <h1>anoweco</h1>
+ <p>
+ Anonymous web comments.
+ </p>
+ </body>
+</html>
--- /dev/null
+<?php
+namespace anoweco;
+/**
+ * Micropub endpoint that stores comments in the database
+ *
+ * @author Christian Weiske <cweiske@cweiske.de>
+ */
+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
--- /dev/null
+<?php
+header('HTTP/1.0 500 Internal Server Error');
+
+function error($msg)
+{
+ header('HTTP/1.0 400 Bad Request');
+ header('Content-type: text/plain; charset=utf-8');
+ echo $msg . "\n";
+ exit(1);
+}
+
+function verifyParameter($givenParams, $paramName)
+{
+ if (!isset($givenParams[$paramName])) {
+ error('"' . $paramName . '" parameter missing');
+ }
+ return $givenParams[$paramName];
+}
+function verifyUrlParameter($givenParams, $paramName)
+{
+ verifyParameter($givenParams, $paramName);
+ $url = parse_url($givenParams[$paramName]);
+ if (!isset($url['scheme'])) {
+ error('Invalid URL in "' . $paramName . '" parameter: scheme missing');
+ }
+ if (!isset($url['host'])) {
+ error('Invalid URL in "' . $paramName . '" parameter: host missing');
+ }
+
+ return $givenParams[$paramName];
+}
+function getOptionalParameter($givenParams, $paramName, $default)
+{
+ if (!isset($givenParams[$paramName])) {
+ return $default;
+ }
+ return $givenParams[$paramName];
+}
+
+if ($_SERVER['REQUEST_METHOD'] == 'GET') {
+ //verify token
+ if (!isset($_SERVER['HTTP_AUTHORIZATION'])) {
+ error('Authorization HTTP header missing');
+ }
+ list($bearer, $token) = explode(' ', $_SERVER['HTTP_AUTHORIZATION'], 2);
+ if ($bearer !== 'Bearer') {
+ error('Authorization header must start with "Bearer"');
+ }
+
+ //FIXME: use real decryption
+ $data = json_decode($token);
+ if ($data === null) {
+ error('Invalid token');
+ }
+ $data = (array) $data;
+ $me = verifyUrlParameter($data, 'me');
+ $client_id = verifyUrlParameter($data, 'client_id');
+ $scope = verifyParameter($data, 'scope');
+
+ header('HTTP/1.0 200 OK');
+ header('Content-type: application/x-www-form-urlencoded');
+ echo http_build_query(
+ array(
+ 'me' => $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 = '<h1>"\'' . 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
+ )
+ );
+}
+?>
--- /dev/null
+<?php
+namespace anoweco;
+require 'www-header.php';
+
+if (!isset($_GET['id'])) {
+ header('HTTP/1.0 400 Bad Request');
+ header('Content-Type: text/plain');
+ echo "id parameter missing\n";
+ exit(1);
+}
+if (!is_numeric($_GET['id'])) {
+ header('HTTP/1.0 400 Bad Request');
+ header('Content-Type: text/plain');
+ echo "Invalid id parameter value\n";
+ exit(1);
+}
+
+$id = intval($_GET['id']);
+
+$storage = new Storage();
+$rowUser = $storage->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),
+ )
+);
+?>
--- /dev/null
+<?php
+require_once __DIR__ . '/../src/anoweco/autoload.php';
+
+\Twig_Autoloader::register();
+
+$loader = new \Twig_Loader_Filesystem(__DIR__ . '/../data/templates/');
+$twig = new \Twig_Environment(
+ $loader,
+ array(
+ //'cache' => '/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