wip
authorChristian Weiske <cweiske@cweiske.de>
Wed, 3 Aug 2016 18:32:14 +0000 (20:32 +0200)
committerChristian Weiske <cweiske@cweiske.de>
Wed, 3 Aug 2016 18:32:14 +0000 (20:32 +0200)
18 files changed:
.gitignore [new file with mode: 0644]
data/schema.sql [new file with mode: 0644]
data/templates/comment.htm [new file with mode: 0644]
data/templates/user.htm [new file with mode: 0644]
scripts/dump-schema.sh [new file with mode: 0755]
src/anoweco/Storage.php [new file with mode: 0644]
src/anoweco/Urls.php [new file with mode: 0644]
src/anoweco/autoload.php [new file with mode: 0644]
www/.htaccess [new file with mode: 0644]
www/auth.php [new file with mode: 0644]
www/comment.php [new file with mode: 0644]
www/css/user.css [new file with mode: 0644]
www/img/anonymous.svg [new file with mode: 0644]
www/index.htm [new file with mode: 0644]
www/micropub.php [new file with mode: 0644]
www/token.php [new file with mode: 0644]
www/user.php [new file with mode: 0644]
www/www-header.php [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..d041e45
--- /dev/null
@@ -0,0 +1 @@
+/data/config.php
diff --git a/data/schema.sql b/data/schema.sql
new file mode 100644 (file)
index 0000000..61eae79
--- /dev/null
@@ -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 (file)
index 0000000..24c2769
--- /dev/null
@@ -0,0 +1,23 @@
+<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>
diff --git a/data/templates/user.htm b/data/templates/user.htm
new file mode 100644 (file)
index 0000000..9f50013
--- /dev/null
@@ -0,0 +1,16 @@
+<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>
diff --git a/scripts/dump-schema.sh b/scripts/dump-schema.sh
new file mode 100755 (executable)
index 0000000..da33d42
--- /dev/null
@@ -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 (file)
index 0000000..a843774
--- /dev/null
@@ -0,0 +1,101 @@
+<?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;
+    }
+}
+?>
diff --git a/src/anoweco/Urls.php b/src/anoweco/Urls.php
new file mode 100644 (file)
index 0000000..a82d36c
--- /dev/null
@@ -0,0 +1,34 @@
+<?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;
+    }
+}
+?>
diff --git a/src/anoweco/autoload.php b/src/anoweco/autoload.php
new file mode 100644 (file)
index 0000000..5fb876a
--- /dev/null
@@ -0,0 +1,37 @@
+<?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;
+        }
+    }
+);
+?>
diff --git a/www/.htaccess b/www/.htaccess
new file mode 100644 (file)
index 0000000..eae6500
--- /dev/null
@@ -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 (file)
index 0000000..847fbb4
--- /dev/null
@@ -0,0 +1,16 @@
+<?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') {
+}
+?>
diff --git a/www/comment.php b/www/comment.php
new file mode 100644 (file)
index 0000000..6dc1b0c
--- /dev/null
@@ -0,0 +1,54 @@
+<?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))
+        ),
+    )
+);
+?>
diff --git a/www/css/user.css b/www/css/user.css
new file mode 100644 (file)
index 0000000..cae3468
--- /dev/null
@@ -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 (file)
index 0000000..ca26d18
--- /dev/null
@@ -0,0 +1,18 @@
+<?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
diff --git a/www/index.htm b/www/index.htm
new file mode 100644 (file)
index 0000000..1a96ade
--- /dev/null
@@ -0,0 +1,14 @@
+<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>
diff --git a/www/micropub.php b/www/micropub.php
new file mode 100644 (file)
index 0000000..43c41b1
--- /dev/null
@@ -0,0 +1,154 @@
+<?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
diff --git a/www/token.php b/www/token.php
new file mode 100644 (file)
index 0000000..bf10e70
--- /dev/null
@@ -0,0 +1,100 @@
+<?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
+        )
+    );
+}
+?>
diff --git a/www/user.php b/www/user.php
new file mode 100644 (file)
index 0000000..95b817f
--- /dev/null
@@ -0,0 +1,38 @@
+<?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),
+    )
+);
+?>
diff --git a/www/www-header.php b/www/www-header.php
new file mode 100644 (file)
index 0000000..a221ca5
--- /dev/null
@@ -0,0 +1,26 @@
+<?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