diff --git a/docker_dev/mysql_schema.sql b/docker_dev/mysql_schema.sql
index 6801fcd..869935a 100644
--- a/docker_dev/mysql_schema.sql
+++ b/docker_dev/mysql_schema.sql
@@ -10,22 +10,46 @@ CREATE TABLE posts (
post_path_depth INTEGER NOT NULL DEFAULT 0,
post_create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- post_update_time DATETIME NOT NULL
- DEFAULT CURRENT_TIMESTAMP
- ON UPDATE CURRENT_TIMESTAMP,
+ post_update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ post_access_count INTEGER DEFAULT 0,
post_metadata JSON NOT NULL,
+ post_settings_cache JSON DEFAULT NULL,
post_content MEDIUMTEXT,
PRIMARY KEY(post_id),
CONSTRAINT unique_post_path UNIQUE (post_path),
- INDEX(post_path, post_update_time),
- INDEX(post_path, post_create_time),
- INDEX(post_path_depth, post_path)
+ INDEX(post_path),
+ INDEX(post_path_depth, post_path),
+ INDEX(post_create_time),
+ INDEX(post_update_time)
);
+CREATE TABLE path_access_counts (
+ access_time DATETIME NOT NULL,
+ post_path VARCHAR(255),
+ agent VARCHAR(255),
+ referrer VARCHAR(255),
+
+ path_access_count INTEGER DEFAULT 0,
+ path_processing_time DOUBLE PRECISION DEFAULT 0,
+
+ PRIMARY KEY(access_time, post_path, agent, referrer)
+);
+
+CREATE TABLE feed_cache (
+ search_path VARCHAR(255),
+ export_type VARCHAR(255),
+
+ feed_created_on DATETIME DEFAULT CURRENT_TIMESTAMP,
+
+ feed_content MEDIUMTEXT,
+
+ PRIMARY KEY(search_path, export_type)
+);
INSERT INTO posts (post_path, post_path_depth, post_metadata, post_content)
VALUES (
diff --git a/test_entries/README.md b/test_entries/README.md
new file mode 100644
index 0000000..01c17d1
--- /dev/null
+++ b/test_entries/README.md
@@ -0,0 +1,8 @@
+---
+settings:
+ colourscheme: fun
+ post_style: generic
+ banners:
+ - src: /banner/0.png
+ - src: /banner/1.png
+---
\ No newline at end of file
diff --git a/test_entries/about/neira/Neira_Queen.png b/test_entries/about/neira/Neira_Queen.png
new file mode 100644
index 0000000..078fba6
Binary files /dev/null and b/test_entries/about/neira/Neira_Queen.png differ
diff --git a/test_entries/about/neira/README.md b/test_entries/about/neira/README.md
new file mode 100644
index 0000000..9faac34
--- /dev/null
+++ b/test_entries/about/neira/README.md
@@ -0,0 +1,10 @@
+---
+directory:
+ settings:
+ banners:
+ - src: /about/neira/Neira_Queen.png
+ from: 0.5
+ to: 0.95
+---
+
+# She is soft~<3
\ No newline at end of file
diff --git a/test_entries/about/neira/really_long_pathname_oh_god/README.md b/test_entries/about/neira/really_long_pathname_oh_god/README.md
new file mode 100644
index 0000000..0571644
--- /dev/null
+++ b/test_entries/about/neira/really_long_pathname_oh_god/README.md
@@ -0,0 +1,5 @@
+---
+title: Oh, the pain
+---
+
+# AAA
\ No newline at end of file
diff --git a/www/static/banner/0.png b/test_entries/banner/0.png
similarity index 100%
rename from www/static/banner/0.png
rename to test_entries/banner/0.png
diff --git a/www/static/banner/banner0.jpeg b/test_entries/banner/banner0.jpeg
similarity index 100%
rename from www/static/banner/banner0.jpeg
rename to test_entries/banner/banner0.jpeg
diff --git a/test_entries/test/the/images/README.md b/test_entries/test/the/images/README.md
index 7b9fd95..c92c2dd 100644
--- a/test_entries/test/the/images/README.md
+++ b/test_entries/test/the/images/README.md
@@ -1,5 +1,10 @@
---
title: A little image test idea
+directory:
+ settings:
+ colourscheme: spicy
+ banners:
+ - src: /about/neira/Neira_Queen.png
---
# README concept
diff --git a/www/.htaccess b/www/.htaccess
index 3ceba7d..d624f34 100644
--- a/www/.htaccess
+++ b/www/.htaccess
@@ -1,5 +1,7 @@
AddType text/plain .md
+AddType text/plain .atom
+AddType text/plain .rss
php_value upload_max_filesize 40M
php_value post_max_size 42M
@@ -7,7 +9,7 @@ php_value post_max_size 42M
RewriteEngine On
RewriteBase /
-RewriteCond %{REQUEST_URI} !^/?(static|raw)/.*
+RewriteCond %{REQUEST_URI} !^/?(static|raw|robots\.txt).*
RewriteRule (.*) router.php
Allow from all
diff --git a/www/composer.json b/www/composer.json
index 752d618..eb01954 100644
--- a/www/composer.json
+++ b/www/composer.json
@@ -3,6 +3,7 @@
"twig/twig": "^3.0",
"twig/markdown-extra": "^3.6",
"league/commonmark": "^2.4",
- "spatie/yaml-front-matter": "^2.0"
+ "spatie/yaml-front-matter": "^2.0",
+ "laminas/laminas-feed": "^2.6"
}
}
diff --git a/www/fontawesome.php b/www/fontawesome.php
new file mode 100644
index 0000000..61f4c87
--- /dev/null
+++ b/www/fontawesome.php
@@ -0,0 +1,10 @@
+ '',
+ 'image' => '',
+ 'folder' => '',
+ 'rss' => ''
+];
+
+?>
\ No newline at end of file
diff --git a/www/mysql_adapter.php b/www/mysql_adapter.php
index 5d5de2f..e9bdb5d 100644
--- a/www/mysql_adapter.php
+++ b/www/mysql_adapter.php
@@ -29,21 +29,38 @@ class MySQLAdapter {
}
}
- function _exec($qery, $argtypes, ...$args) {
+ function _sanitize_path($post_path) {
+ $post_path = chop($post_path, '/');
+
+ if($post_path == "") {
+ return "";
+ }
+
+ if(!preg_match('/^(?:\/[\w-]+)+(?:\.[\w-]+)*$/', $post_path)) {
+ echo "Post path match against " . $post_path . " failed!";
+ die();
+ }
+
+ return $post_path;
+ }
+
+ function _exec($qery, $argtypes = '', ...$args) {
$stmt = $this->raw->prepare($qery);
- $stmt->bind_param($argtypes, ...$args);
+ if($argtypes != ""){
+ $stmt->bind_param($argtypes, ...$args);
+ }
$stmt->execute();
return $stmt->get_result();
}
function _normalize_post_data($post_data) {
- if($post_data == null) {
- return [
- "found" => false
- ];
- }
+ $post_data ??= ['found' => null];
+ if(isset($post_data['found']) && $post_data['found'] == false) {
+ return $post_data;
+ }
+
$post_data["found"] = true;
$post_data['post_metadata'] = json_decode($post_data["post_metadata"], true) ?? [];
@@ -53,7 +70,7 @@ class MySQLAdapter {
}
function bump_post($post_path, $post_metadata = [], $create_dirs = true) {
- $post_path = chop($post_path, '/');
+ $post_path = $this->_sanitize_path($post_path);
$path_depth = substr_count($post_path, "/");
if($create_dirs) {
@@ -88,18 +105,127 @@ class MySQLAdapter {
}
}
+ function log_post_access($post_path, $agent, $referrer, $time) {
+ $post_path = $this->_sanitize_path($post_path);
+
+ $qry = "INSERT INTO path_access_counts
+ (access_time,
+ post_path, agent, referrer,
+ path_access_count,
+ path_processing_time)
+ VALUES ( from_unixtime(floor(unix_timestamp(CURRENT_TIMESTAMP) / 300)*300),
+ ?, ?, ?, 1, ?
+ ) AS new
+ ON DUPLICATE KEY
+ UPDATE path_access_count=path_access_counts.path_access_count+1,
+ path_processing_time=path_access_counts.path_processing_time+new.path_processing_time;
+ ";
+
+ $this->_exec($qry, "sssd", $post_path, $agent, $referrer, $time);
+
+ if(preg_match('/^user/', $agent)) {
+ $this->_exec("UPDATE posts SET post_access_count=post_access_count+1 WHERE post_path=?", "s", $post_path);
+ }
+ }
+
+ function get_post_access_counters() {
+ $qry = "
+ SELECT post_path, agent, path_access_count, path_processing_time
+ FROM path_access_counts
+ WHERE path_last_access_time > ( CURRENT_TIMESTAMP - INTERVAL 10 MINUTE );
+ ";
+
+ $data = $this->_exec($qry, "")->fetch_all(MYSQLI_ASSOC);
+
+ $out_data = [];
+
+ foreach($data AS $post_data) {
+ $path = $post_data['post_path'];
+
+ $agent_data = ($out_data[$path] ?? []);
+
+ $agent_data[$post_data['agent']] = [
+ 'count' => $post_data['path_access_count'],
+ 'time' => round($post_data['path_processing_time'], 6)
+ ];
+
+ $out_data[$path] = $agent_data;
+ }
+
+ return $out_data;
+ }
+
+ function get_post_access_counters_line() {
+ $qry = "
+ SELECT access_time, post_path, agent, referrer, path_access_count, path_processing_time
+ FROM path_access_counts
+ WHERE access_time < ( CURRENT_TIMESTAMP - INTERVAL 6 MINUTE )
+ ORDER BY access_time DESC;
+ ";
+
+ $this->raw->begin_transaction();
+ $top_access_time = null;
+
+ try {
+ $data = $this->_exec($qry, "")->fetch_all(MYSQLI_ASSOC);
+
+ $data_prefix="access_metrics,host=" . $_SERVER['SERVER_NAME'];
+
+ $out_data = "";
+
+ foreach($data AS $post_data) {
+ $top_access_time ??= $post_data['access_time'];
+
+ $path = $post_data['post_path'];
+ if($path == '') {
+ $path = '/';
+ }
+ $out_data .= $data_prefix . ",agent=".$post_data['agent'].",path=".$path.",referrer=".$post_data['referrer'];
+
+ $out_data .= " access_sum=" . $post_data['path_access_count'] . ",time_sum=" . $post_data['path_processing_time'];
+ $out_data .= " " . strtotime($post_data['access_time']) . "000000000\n";
+ }
+
+
+ $this->_exec("DELETE FROM path_access_counts WHERE access_time <= ?", "s", $top_access_time);
+
+ $this->raw->commit();
+ return $out_data;
+
+ } catch (\Throwable $th) {
+ $this->raw->rollback();
+
+ throw $th;
+ }
+ }
+
+ function reset_post_settings_cache($post_path) {
+ $post_path = $this->_sanitize_path($post_path);
+
+ $this->_exec("
+ UPDATE posts
+ SET post_settings_cache=NULL
+ WHERE post_path LIKE ?;
+ ", "s", $post_path . "%");
+ }
+
function update_or_create_post($post_path, $post_metadata, $post_content) {
- $post_path = chop($post_path, '/');
+ $post_path = $this->_sanitize_path($post_path);
$path_depth = substr_count($post_path, "/");
$this->make_post_directory(dirname($post_path));
+ $this->reset_post_settings_cache($post_path);
+
$qry = "
INSERT INTO posts
(post_path, post_path_depth, post_metadata, post_content)
VALUES
( ?, ?, ?, ?) AS new
- ON DUPLICATE KEY UPDATE post_metadata=new.post_metadata, post_content=new.post_content;";
+ ON DUPLICATE KEY
+ UPDATE post_metadata=new.post_metadata,
+ post_content=new.post_content,
+ post_update_time=CURRENT_TIMESTAMP;";
$this->_exec($qry, "siss",
$post_path,
@@ -108,17 +234,78 @@ class MySQLAdapter {
$post_content);
}
- function get_post_by_path($post_path, $with_subposts = true) {
- $qry = "SELECT * FROM posts WHERE post_path = ?";
+ function get_settings_for_path($post_path) {
+ $post_path = $this->_sanitize_path($post_path);
- $post_path = chop($post_path, '/');
+ $post_settings = $this->_exec("
+ SELECT post_path, post_settings_cache
+ FROM posts
+ WHERE post_path = ?
+ ", "s", $post_path)->fetch_assoc();
+
+ if(!isset($post_settings)) {
+ return [];
+ }
+ if(isset($post_settings['post_settings_cache'])) {
+ return json_decode($post_settings['post_settings_cache'], true);
+ }
+
+ $parent_settings = [];
+ if($post_path != "") {
+ $parent_settings = $this->get_settings_for_path(dirname($post_path));
+ }
+
+ $post_settings = [];
+ $post_metadata = $this->_exec("
+ SELECT post_path, post_metadata
+ FROM posts
+ WHERE post_path = ?
+ ", "s", $post_path)->fetch_assoc();
+
+ if(isset($post_metadata['post_metadata'])) {
+ $post_metadata = json_decode($post_metadata['post_metadata'], true);
+
+ if(isset($post_metadata['settings'])) {
+ $post_settings = $post_metadata['settings'];
+ }
+ }
+
+ $post_settings = array_merge($parent_settings, $post_settings);
+
+ $this->_exec("UPDATE posts SET post_settings_cache=? WHERE post_path=?", "ss",
+ json_encode($post_settings), $post_path);
+
+ return $post_settings;
+ }
+
+ function get_post_by_path($post_path,
+ $with_subposts = false, $with_settings = true) {
+
+ $qry = "SELECT *
+ FROM posts WHERE post_path = ?
+ ";
+
+ $post_path = $this->_sanitize_path($post_path);
$post_data = $this->_exec($qry, "s", $post_path)->fetch_assoc();
+
+ if(!isset($post_data)) {
+ $post_data = ['found' => false];
+ }
+ $post_data['post_path'] = $post_path;
+
$post_data = $this->_normalize_post_data($post_data);
+
+ if(!$post_data['found']) {
+ return $post_data;
+ }
if($with_subposts) {
$post_data['subposts'] = $this->get_subposts_by_path($post_path);
}
+ if($with_settings) {
+ $post_data['settings'] = $this->get_settings_for_path($post_path);
+ }
return $post_data;
}
@@ -126,7 +313,7 @@ class MySQLAdapter {
function get_subposts_by_path($path) {
global $sql;
- $path = chop($path, '/');
+ $path = $this->_sanitize_path($path);
$path_depth = substr_count($path, "/");
diff --git a/www/post_adapter.php b/www/post_adapter.php
index ff082ec..fc0297e 100644
--- a/www/post_adapter.php
+++ b/www/post_adapter.php
@@ -3,6 +3,7 @@
require_once 'mysql_adapter.php';
use Spatie\YamlFrontMatter\YamlFrontMatter;
+use Laminas\Feed\Writer\Feed;
class PostHandler extends MySQLAdapter {
public $data_directory;
@@ -16,12 +17,19 @@ class PostHandler extends MySQLAdapter {
function _normalize_post_data($post_data) {
$post_data = parent::_normalize_post_data($post_data);
+ if(!$post_data['found']) {
+ return $post_data;
+ }
+
$post_data["post_basename"] = basename($post_data["post_path"]);
$post_meta = $post_data['post_metadata'];
$post_meta["title"] ??= basename($post_data["post_path"]);
-
+ if($post_meta["title"] == "") {
+ $post_meta["title"] = "root";
+ }
+
if(!isset($post_meta['type'])) {
$type = null;
@@ -31,17 +39,33 @@ class PostHandler extends MySQLAdapter {
'' => 'directory',
'md' => 'text/markdown',
'png' => 'image',
+ 'jpg' => 'image',
+ 'jpeg' => 'image'
];
if(isset($ext_mapping[$ext])) {
$post_meta['type'] = $ext_mapping[$ext];
}
+ else {
+ $post_meta['type'] = '?';
+ }
}
- $post_data["post_file_dir"] = '/' . $this->data_directory . $post_data["post_path"];
+ if(!isset($post_meta['icon'])) {
+ $icon_mapping = [
+ '' => 'question',
+ 'text/markdown' => 'markdown',
+ 'directory' => 'folder',
+ 'image' => 'image'
+ ];
+ $post_meta['icon'] = $icon_mapping[$post_meta['type']] ?? 'question';
+ }
+
$post_data['post_metadata'] = $post_meta;
+ $post_data["post_file_dir"] = '/' . $this->data_directory . $post_data["post_path"];
+
return $post_data;
}
@@ -53,6 +77,11 @@ class PostHandler extends MySQLAdapter {
parent::make_post_directory($directory);
}
+ function update_or_create_post(...$args) {
+ $this->_exec("TRUNCATE feed_cache");
+ parent::update_or_create_post(...$args);
+ }
+
function save_file($post_path, $file_path) {
$this->bump_post($post_path);
move_uploaded_file($file_path, $this->data_directory . $post_path);
@@ -60,15 +89,18 @@ class PostHandler extends MySQLAdapter {
function save_markdown_post($post_path, $post_data) {
$frontmatter_post = YamlFrontMatter::parse($post_data);
- $post_path = chop($post_path, '/');
+ $post_path = $this->_sanitize_path($post_path);
$post_content = $frontmatter_post->body();
$post_metadata = $frontmatter_post->matter();
if(basename($post_path) == "README.md") {
$readme_metadata = [];
- if(isset($post_metadata['directory_data'])) {
- $readme_metadata = $post_metadata['directory_data'];
+ if(isset($post_metadata['settings'])) {
+ $readme_metadata['settings'] = $post_metadata['settings'];
+ }
+ if(isset($post_metadata['directory'])) {
+ $readme_metadata = $post_metadata['directory'];
}
$this->update_or_create_post(dirname($post_path),
@@ -94,6 +126,86 @@ class PostHandler extends MySQLAdapter {
$this->save_file($post_path, $file_path);
}
}
+
+ function try_get_cached_feed($path, $export_opt) {
+ $post_cache = $this->_exec("SELECT feed_content, feed_created_on
+ FROM feed_cache
+ WHERE search_path=? AND export_type=?", "ss", $path, $export_opt)->fetch_assoc();
+
+ if(!isset($post_cache)) {
+ return null;
+ }
+
+ return ['feed' => $post_cache['feed_content'], 'feed_ts' => $post_cache['feed_created_on']];
+ }
+
+ function construct_feed($path) {
+ $path = $this->_sanitize_path($path);
+
+ $feed = @new Feed;
+
+ $feed->setTitle("DergFeed");
+ $feed->setLink("https://lucidragons.de" . $path);
+ $feed->setFeedLink("https://lucidragons.de/feeds/atom" . $path, "atom");
+
+ $feed->setDateModified(time());
+
+ $feed->setDescription("DergenFeed for all your " . $path . " needs <3");
+
+ $feed_posts = $this->_exec("SELECT
+ post_path,
+ post_create_time, post_update_time,
+ post_content,
+ post_metadata
+ FROM posts
+ WHERE (post_path = ?) OR (post_path LIKE ?)
+ ORDER BY post_create_time DESC LIMIT 200",
+ "ss", $path, $path . '/%');
+
+ while($row = $feed_posts->fetch_array(MYSQLI_ASSOC)) {
+ $row = $this->_normalize_post_data($row);
+ $pmeta = $row['post_metadata'];
+
+ if($pmeta['type'] == 'directory') {
+ continue;
+ }
+
+ $entry = $feed->createEntry();
+
+ $entry->setTitle($row['post_path'] . '> ' . $pmeta['title']);
+ $entry->setLink('https://lucidragons.de' . $row['post_path']);
+ $entry->setDateModified(strtotime($row['post_update_time']));
+ $entry->setDateCreated(strtotime($row['post_create_time']));
+
+ $entry->setDescription($pmeta['brief'] ?? $pmeta['title']);
+
+ $feed->addEntry($entry);
+ }
+
+ return $feed;
+ }
+
+ function get_laminas_feed($path, $export_opt) {
+ $path = $this->_sanitize_path($path);
+
+ $feed_cache = $this->try_get_cached_feed($path, $export_opt);
+
+ if(isset($feed_cache)) {
+ return $feed_cache;
+ }
+
+ $feed = $this->construct_feed($path);
+
+ $this->_exec("INSERT INTO feed_cache
+ (search_path, export_type, feed_content)
+ VALUES
+ (?, 'atom', ?),
+ (?, 'rss', ?)",
+ "ssss", $path, $feed->export('atom'),
+ $path, $feed->export('rss'));
+
+ return $this->try_get_cached_feed($path, $export_opt);
+ }
}
?>
\ No newline at end of file
diff --git a/www/robots.txt b/www/robots.txt
new file mode 100644
index 0000000..14267e9
--- /dev/null
+++ b/www/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Allow: /
\ No newline at end of file
diff --git a/www/router.php b/www/router.php
index 04388c8..4f2f0d8 100644
--- a/www/router.php
+++ b/www/router.php
@@ -1,20 +1,14 @@
';
-// echo 'Error number: ' . mysqli_connect_errno() . '
';
-// echo 'Error message: ' . mysqli_connect_error() . '
';
-// die();
-// }
-
$loader = new \Twig\Loader\FilesystemLoader(['./templates', './user_content']);
$twig = new \Twig\Environment($loader,['debug' => true]);
@@ -24,6 +18,42 @@ use Twig\Extra\Markdown\DefaultMarkdown;
use Twig\Extra\Markdown\MarkdownRuntime;
use Twig\RuntimeLoader\RuntimeLoaderInterface;
+function deduce_user_agent() {
+ $real_agent=$_SERVER['HTTP_USER_AGENT'];
+
+ if(preg_match('/(Googlebot|\w*Google\w*)/', $real_agent, $match)) {
+ return "bot/google/" . $match[1];
+ }
+ elseif(preg_match('/(Mozilla|Chrome|Chromium)/', $real_agent, $match)) {
+ return "user/" . $match[1];
+ }
+ else {
+ return "unidentified";
+ }
+}
+
+function log_and_die($path, $die_code = 0, $referrer = null) {
+ global $data_time_start;
+ global $adapter;
+
+ $data_time_end = microtime(true);
+
+ if(!isset($referrer)) {
+ $referrer = 'magic';
+
+ if(isset($_SERVER['HTTP_REFERER'])) {
+ $referrer = parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST);
+ }
+ }
+
+ $adapter->log_post_access($path,
+ deduce_user_agent(),
+ $referrer,
+ $data_time_end - $data_time_start);
+
+ die($die_code);
+}
+
$twig->addRuntimeLoader(new class implements RuntimeLoaderInterface {
public function load($class) {
if (MarkdownRuntime::class === $class) {
@@ -32,86 +62,156 @@ $twig->addRuntimeLoader(new class implements RuntimeLoaderInterface {
}
});
-$SURI = $_SERVER['REQUEST_URI'];
+function render_twig($template, $args = []) {
+ global $twig;
+ global $FONT_AWESOME_ARRAY;
-if($SURI == '/') {
- echo $twig->render('root.html');
-} elseif(preg_match('/^\/api\/admin/', $SURI)) {
- header('Content-Type: application/json');
-
- $user_api_key = '';
- if(isset($_GET['api_key'])) {
- $user_api_key = $_GET['api_key'];
- }
- if(isset($_POST['api_key'])) {
- $user_api_key = $_POST['api_key'];
+ $args['fa'] = $FONT_AWESOME_ARRAY;
+
+ $post = $args['post'] ?? [];
+ $settings = $post['settings'] ?? [];
+ $meta = $post['post_metadata'] ?? [];
+
+ $args['banner'] ??= $settings['banners'] ?? [
+ ["src"=> "/static/banner/0.png"],
+ ["src" => "/static/banner/1.png"]
+ ];
+
+ $args['og'] = array_merge([
+ "title" => $meta['title'] ?? "Dergennibble",
+ "url" => $_SERVER['REQUEST_URI'],
+ "description" => $meta['description']
+ ?? $settings['description']
+ ?? "The softest spot to find dragons on"
+ ], $args['og'] ?? []);
+
+ if(($meta['type'] ?? '') == 'image') {
+ $args['og']['image'] ??= "https://lucidragons.de" . $post['post_file_dir'];
}
- if($user_api_key != file_get_contents('secrets/api_admin_key')) {
- http_response_code(401);
+ $args['og']['image'] ??= 'https://lucidragons.de' . $args['banner'][0]["src"];
- echo json_encode([
- "authorized" => false
- ]);
+ $args['banner'] = json_encode($args['banner']);
- die();
- }
-
- if($SURI = '/api/admin/upload') {
- $adapter->handle_upload($_POST['post_path'], $_FILES['post_data']['tmp_name']);
-
- echo json_encode(["ok" => true]);
- }
-} elseif(preg_match('/^\/api/', $SURI)) {
- if(preg_match('/^\/api\/posts(.*)$/', $SURI, $match)) {
-
- header('Content-Type: application/json');
- echo json_encode($adapter->get_post_by_path($match[1]));
-
- } elseif(preg_match('/^\/api\/subposts(.*)$/', $SURI, $match)) {
-
- header('Content-Type: application/json');
- echo json_encode(get_subposts($match[1]));
- } elseif($SURI == '/api/upload') {
-
- echo $twig->render('upload.html');
- }
-} elseif($_SERVER['HTTP_SEC_FETCH_DEST'] == 'image') {
- header('Location: /raw' . $SURI);
- exit(0);
-} elseif(true) {
- $post = $adapter->get_post_by_path($SURI);
-
- if($post['post_metadata']['type'] == 'directory') {
- if(preg_match('/^(.*[^\/])((?:#.*)?)$/', $SURI, $match)) {
- header('Location: ' . $match[1] . '/' . $match[2]);
- exit(0);
- }
-
- echo $twig->render('post_types/directory.html', [
- "post" => $post,
- "subposts" => $post['subposts']
- ]);
- }
- elseif($post['post_metadata']['type'] == 'text/markdown') {
- echo $twig->render('post_types/markdown.html', [
- "post" => $post,
- "subposts" => $post['subposts']
- ]);
- }
- elseif($post['post_metadata']['type'] == 'image') {
- echo $twig->render('post_types/image.html', [
- "post" => $post
- ]);
- }
-
-} else {
- echo $twig->render('rrror.html',[
- "error_code" => '404 Hoard not found!',
- "error_description" => "Well, we searched
- far and wide for `" . $SURI . "` but
- somehow it must have gotten lost... Sorry!"
- ]);
+ echo $twig->render($template, $args);
}
+function try_render_post($SURI) {
+ global $adapter;
+
+ $post = $adapter->get_post_by_path($SURI);
+
+ if(!$post['found']) {
+ echo render_twig('post_types/rrror.html',[
+ "error_code" => '404 Hoard not found!',
+ "error_description" => "Well, we searched
+ far and wide for `" . $SURI . "` but
+ somehow it must have gotten lost... Sorry!",
+ "post" => array_merge($post, [
+ "post_metadata" => ["title" => "404 ???"]
+ ])
+ ]);
+
+ log_and_die('/404', referrer: ($_SERVER['HTTP_REFERER'] ?? 'magic'));
+ }
+
+ switch($post['post_metadata']['type']) {
+ case 'directory':
+ if(preg_match('/^(.*[^\/])((?:#.*)?)$/', $SURI, $match)) {
+ header('Location: ' . $match[1] . '/' . $match[2]);
+
+ die();
+ }
+
+ echo render_twig('post_types/directory.html', [
+ "post" => $post,
+ "subposts" => $adapter->get_subposts_by_path($SURI)
+ ]);
+
+ break;
+
+ case 'text/markdown':
+ echo render_twig('post_types/markdown.html', [
+ "post" => $post
+ ]);
+ break;
+
+ case 'image':
+ echo render_twig('post_types/image.html', [
+ "post" => $post,
+ ]);
+ break;
+ }
+}
+
+function generate_website($SURI) {
+ global $adapter;
+ global $FONT_AWESOME_ARRAY;
+
+ if(preg_match('/^\/api\/admin/', $SURI)) {
+ header('Content-Type: application/json');
+
+ $user_api_key = '';
+ if(isset($_GET['api_key'])) {
+ $user_api_key = $_GET['api_key'];
+ }
+ if(isset($_POST['api_key'])) {
+ $user_api_key = $_POST['api_key'];
+ }
+
+ if($user_api_key != file_get_contents('secrets/api_admin_key')) {
+ http_response_code(401);
+
+ echo json_encode([
+ "authorized" => false
+ ]);
+
+ log_and_die('/api/401');
+ }
+
+ if($SURI = '/api/admin/upload') {
+ $adapter->handle_upload($_POST['post_path'], $_FILES['post_data']['tmp_name']);
+
+ echo json_encode(["ok" => true]);
+ }
+ } elseif(preg_match('/^\/api/', $SURI)) {
+ if($SURI == '/api/post_counters') {
+ header('Content-Type: application/json');
+ echo json_encode($adapter->get_post_access_counters());
+ } elseif($SURI == '/api/metrics') {
+ header('Content-Type: application/line');
+ echo $adapter->get_post_access_counters_line();
+ } elseif(preg_match('/^\/api\/posts(.*)$/', $SURI, $match)) {
+
+ header('Content-Type: application/json');
+ echo json_encode($adapter->get_post_by_path($match[1]));
+
+ } elseif(preg_match('/^\/api\/subposts(.*)$/', $SURI, $match)) {
+
+ header('Content-Type: application/json');
+ echo json_encode(get_subposts($match[1]));
+ } elseif($SURI == '/api/upload') {
+
+ echo $twig->render('upload.html');
+ }
+ } elseif(preg_match('/^\/feed(?:\/(rss|atom)(.*))?$/', $SURI, $match)) {
+ $feed = $adapter->get_laminas_feed($match[2] ?? '/', $match[1] ?? 'rss');
+
+ header('Content-Type: application/xml');
+ header('Cache-Control: max-age=1800');
+ header('Etag: W/"' . $SURI . '/' . strtotime($feed['feed_ts']) . '"');
+
+ echo $feed['feed'];
+ } elseif(preg_match('/^\s*image/', $_SERVER['HTTP_ACCEPT'])) {
+ header('Location: /raw' . $SURI);
+ exit(0);
+ } elseif(true) {
+ try_render_post($SURI);
+ }
+}
+
+generate_website($_SERVER['REQUEST_URI']);
+
+log_and_die($_SERVER['REQUEST_URI']);
+
?>
diff --git a/www/static/banner.js b/www/static/banner.js
index 256d683..7af1dc4 100644
--- a/www/static/banner.js
+++ b/www/static/banner.js
@@ -1,69 +1,160 @@
-const banner_show_time = 600 * 1000.0
-const banner_animated_style = "opacity 0.8s linear, transform 0.1s linear"
+const BANNER_TIME = 600 * 1000.0
+const BANNER_ANIMATION = "opacity 0.8s linear, transform 0.1s linear"
-var banner_current_src = localStorage.getItem('main_banner_img')
+class BannerHandler {
+ constructor(banner_container, banner_image, banner_link) {
+ this.bannerContainerDOM = banner_container
+ this.bannerDOM = banner_image
+ this.bannerLinkDOM = banner_link
-function getBannerTime() {
- return (new Date()).getTime() / banner_show_time
-}
-function getBannerSrc() {
- return "/static/banner/" + Math.floor(getBannerTime() + 1000/banner_show_time) % 2 + ".png"
-}
-function update_banner_top(banner, banner_container) {
- const banner_top_max = 0
- const banner_top_min = -banner.clientHeight + banner_container.clientHeight
+ this.bannerUpdateTimer = null
+ this.currentPhase = 0
- const banner_top = (1-(getBannerTime()%1)) * banner_top_min
- banner.style.transform = "translateY(" + banner_top + 'px' + ")"
-}
+ this.currentBannerData = null
+ try {
+ this.currentBannerData = JSON.parse(localStorage.getItem('main_banner_img'))
+ } catch(e) {}
-let banner_update_src = banner_current_src
-function update_banner(banner, banner_container) {
+ this.currentBannerData ||= {}
- image_select = getBannerSrc()
+ this.bannerDOM.onload=() => { this.onBannerLoaded() }
+ this.bannerDOM.onerror=() => {
+ this.fadeOut();
+ setTimeout(() => this.loadNextBanner(), 1000);
+ }
+ }
- update_banner_top(banner, banner_container)
+ startUpdateTick() {
+ if(this.bannerUpdateTimer !== null) {
+ return
+ }
- if(image_select != banner_update_src) {
- banner.style.opacity = 0
+ console.log("Starting tick")
+ this.bannerUpdateTimer = setInterval(() => { this.updateTick() }, 100);
+ }
+ stopUpdateTick() {
+ if(this.bannerUpdateTimer === null) {
+ return
+ }
+
+ console.log("Stopping tick!")
+
+ clearInterval(this.bannerUpdateTimer);
+ this.bannerUpdateTimer = null
+ }
+
+ getPhase() {
+ return (new Date()).getTime() / BANNER_TIME;
+ }
+
+ getTargetBanner() {
+ if(window.dergBannerOptions == null) {
+ return {}
+ }
+
+ var banner_index = Math.floor(this.getPhase()) % window.dergBannerOptions.length
+ var banner_choice = window.dergBannerOptions[banner_index]
+
+ return banner_choice
+ }
+
+ updateTranslation() {
+ const bannerTranslateMax = -this.bannerDOM.clientHeight + this.bannerContainerDOM.clientHeight
+
+ const bannerPercentageFrom = this.currentBannerData.from || 0;
+ const bannerPercentageTo = this.currentBannerData.to || 1;
+
+ const bannerPercentage = (bannerPercentageFrom + (bannerPercentageTo - bannerPercentageFrom) * this.currentPhase)
+
+ const banner_top = (1-bannerPercentage) * bannerTranslateMax
+ this.bannerDOM.style.transform = "translateY(" + banner_top + 'px' + ")"
+ }
+
+ fadeOut() {
+ this.bannerDOM.style.opacity = 0;
+ }
+ fadeIn() {
+ this.bannerDOM.style.opacity = 0.3;
+ }
+
+ loadNextBanner() {
+ this.currentBannerData = this.getTargetBanner()
+ this.currentBannerData.bannerTime = new Date()
+
+ this.loadBanner()
+ }
+
+ loadBanner() {
+ console.log("Target banner:");
+ console.log(this.currentBannerData);
+
+ localStorage.setItem("main_banner_img", JSON.stringify(this.currentBannerData))
+
+ this.currentPhase = 0
+
+ if((this.currentBannerData === null)
+ || (this.currentBannerData.src === undefined)) {
+
+ this.onBannerLoaded()
+ return
+ }
+
+ this.bannerDOM.src = this.currentBannerData.src
+ this.bannerLinkDOM.href = this.currentBannerData.href || this.currentBannerData.src
+ }
+ onBannerLoaded() {
+ console.log("Loaded?");
+
+ this.currentPhase = this.getPhase() % 1
+
+ this.updateTranslation()
+ this.fadeIn()
+
setTimeout(() => {
- banner.src = image_select
- }, 1000)
+ this.animateOn()
- banner_update_src = image_select
- localStorage.setItem('main_banner_img', image_select)
+ this.startUpdateTick()
+ }, 10)
+ }
- document.getElementById("main_banner_img_link").href = "/gallery/test"
+ updateTick() {
+ console.log("tick")
+
+ const nextPhase = this.getPhase() % 1;
+
+ if((nextPhase > this.currentPhase)
+ && (this.currentBannerData.src == this.getTargetBanner().src)) {
+
+ this.currentPhase = nextPhase;
+ this.updateTranslation();
+ } else {
+ this.fadeOut()
+ setTimeout(() => {
+ this.loadNextBanner()
+ }, 1000);
+
+ this.stopUpdateTick()
+ }
+ }
+
+ animateOn() {
+ this.bannerDOM.style.transition = BANNER_ANIMATION
+ }
+
+ start() {
+ this.fadeIn()
+
+ this.loadBanner()
}
}
-const banner_container = document.getElementById("main_header")
-const banner = document.getElementById("main_banner_img")
-
-
-banner.addEventListener('load', () => {
- update_banner_top(banner, banner_container)
-
- const next_banner_src = getBannerSrc()
-
- if(banner_current_src != next_banner_src) {
- banner.style.transition = banner_animated_style
- setTimeout(() => banner.style.opacity = 0.3, 1000)
- }
- else {
- banner.style.opacity = 0.3
- setTimeout(() => banner.style.transition = banner_animated_style, 0)
- }
-
- banner_current_src = next_banner_src
-})
+var bannerHandler = new BannerHandler(
+ document.getElementById("main_header"),
+ document.getElementById("main_banner_img"),
+ document.getElementById("main_banner_img_link"))
-document.addEventListener("DOMContentLoaded", function () {
- banner.src = getBannerSrc()
- document.getElementById("main_banner_img_link").href = "/gallery/test"
-})
+bannerHandler.start()
-setInterval(() => update_banner(banner, banner_container), 100)
-addEventListener("resize", () => update_banner(banner, banner_container));
+// addEventListener("resize", () => update_banner(banner, banner_container));
diff --git a/www/static/banner/1.png b/www/static/banner/1.png
deleted file mode 100644
index 6e5069f..0000000
Binary files a/www/static/banner/1.png and /dev/null differ
diff --git a/www/static/dergstyle.css b/www/static/dergstyle.css
index 810a46c..6051215 100644
--- a/www/static/dergstyle.css
+++ b/www/static/dergstyle.css
@@ -6,6 +6,11 @@
padding: 0;
}
+svg {
+ fill: var(--text_1);
+ padding-top: 0.1rem;
+}
+
body {
--bg_1: #0e0a2a;
--bg_2: #2c2943;
@@ -26,6 +31,13 @@ body {
padding-bottom: 4rem;
}
+@media only screen and (max-width: 600px) {
+ .hsmol_hide {
+ display: none !important;
+ visibility: hidden !important;
+ }
+}
+
:link {
color: var(--highlight_1);
font-style: italic;
@@ -153,6 +165,11 @@ a:hover {
display: flex;
flex-direction: row;
list-style-type: none;
+
+ overflow-x: scroll;
+ overflow-y: hidden;
+
+ white-space: nowrap;
}
#post_file_path a {
color: var(--text_1);
@@ -186,6 +203,17 @@ a:hover {
margin-left: auto;
}
+#content_footer {
+ display: block;
+ max-width: 100%;
+
+ margin-top: 0.5em;
+
+ font-size: 0.8em;
+ border-top: solid 1px darkgrey;
+ opacity: 0.7;
+}
+
#main_footer {
display: flex;
diff --git a/www/static/directorystyle.css b/www/static/directorystyle.css
index 79a8bfc..d762c6f 100644
--- a/www/static/directorystyle.css
+++ b/www/static/directorystyle.css
@@ -33,11 +33,4 @@ table.directory tr.entry .entry_title {
table.directory .entry_update_time {
display: block;
width: 12rem;
-}
-
-@media only screen and (max-width: 600px) {
- table.directory .entry_update_time {
- visibility: hidden;
- display: none;
- }
}
\ No newline at end of file
diff --git a/www/templates/fragments/filepath_bar.html b/www/templates/fragments/filepath_bar.html
index cff3d04..31b5265 100644
--- a/www/templates/fragments/filepath_bar.html
+++ b/www/templates/fragments/filepath_bar.html
@@ -8,12 +8,9 @@
{% set split_post = post.post_path |split('/') %}
{% for i in range(0, split_post|length - 1) %}