Compare commits

...

32 commits

Author SHA1 Message Date
f4824f8ca8 fix: 🩹 fix up wrong ordering
All checks were successful
/ phplint (push) Successful in 6s
2024-01-09 00:12:54 +01:00
5d1d4cdc32 refactor: move as many OG tags to root page as possible 2024-01-09 00:12:31 +01:00
454161e98a refactor: ♻️ break out page type handling code and opengraph tag generation 2024-01-09 00:11:52 +01:00
1b8bff401b feat: rewrite banner javascript system to accept custom banner data from PHP template 2024-01-08 16:42:29 +01:00
30019b4c47 refactor: remove long-unused "user-content" dir 2024-01-08 16:40:37 +01:00
d584a2c373 misc: prepare test entires for new banner system 2024-01-08 16:40:04 +01:00
d44b854673 refactor: remove static banner images in prep for new banner javascript 2024-01-08 16:39:29 +01:00
83b849fda8 fix: small naming fix to make writing directory README data easier 2024-01-04 16:07:29 +01:00
98e062611e feat: add dedicated class to hide contents on small-width screens 2024-01-04 16:06:52 +01:00
91224e634a feat: add OpenGraph tags 2024-01-04 16:06:06 +01:00
62089b9ee2 feat: add FontAwesome symbols for directories 2024-01-04 16:05:16 +01:00
87a14e0624 feat: missing tweaks for feed writing 2024-01-04 16:03:21 +01:00
596cc0e1a3 feat: add RSS and Atom feeds with caching and nice links 2023-12-25 20:13:00 +01:00
c6297fd81b feat: add robots.txt, google may monch 2023-12-23 10:52:47 +01:00
b089bfc551 feat: add wrapper function to log page access and exit 2023-12-23 10:52:29 +01:00
70a2c45509 fix: fix graphical desing of error page to remove double-nested article 2023-12-23 10:51:42 +01:00
e777ea5675 fix: small fix to post data normalization handling missing posts better 2023-12-23 10:51:07 +01:00
2add216157 feat: add line-format output for access counters 2023-12-23 10:50:33 +01:00
a4fe4c4489 feat: transition to time-block based access count storage, including referrer storage 2023-12-23 10:49:54 +01:00
35ff4951ad feat: add small article footer to show created/edited times and view count 2023-12-20 19:20:24 +01:00
edeecb8cf9 feat: re-add post access counter for get_post function 2023-12-20 19:19:35 +01:00
226d19e62c fix: 🩹 fix error on empty argtypes list (no bindings needed) 2023-12-20 18:55:12 +01:00
6cb60a6652 feat: add settings construct cache
This will avoid the awkward recursive query for
each post by fetching cached post settings if available, and
caching them nicely.
2023-12-20 18:54:31 +01:00
94b65aec8c feat: add post access usage metrics 2023-12-20 18:52:28 +01:00
eb87a78625 feat: use a proper path sanitization function for permitted paths 2023-12-20 18:50:07 +01:00
b552562f31 feat: add implicit README settings to directory settings copy 2023-12-19 10:18:34 +01:00
d2dc57a36a test: add test entires using the new settings 2023-12-19 10:14:31 +01:00
40a270059f feat: pull hierarchical settings dict from post metadata 2023-12-19 10:14:14 +01:00
2ee294b012 fix: 🐛 fix post metadata fill-out failing on non-found posts 2023-12-18 16:02:31 +01:00
37357da4b0 refactor: ♻️ move error page into proper post_type dir 2023-12-18 16:01:29 +01:00
b8ae2c5617 fix: properly use HTTP_ACCEPT to detect image rather than SEC_FETCH_DEST 2023-12-18 16:01:06 +01:00
f2f8a235c8 feat: properly utilise 404 page again 2023-12-18 16:00:28 +01:00
28 changed files with 829 additions and 219 deletions

View file

@ -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 (

8
test_entries/README.md Normal file
View file

@ -0,0 +1,8 @@
---
settings:
colourscheme: fun
post_style: generic
banners:
- src: /banner/0.png
- src: /banner/1.png
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 KiB

View file

@ -0,0 +1,10 @@
---
directory:
settings:
banners:
- src: /about/neira/Neira_Queen.png
from: 0.5
to: 0.95
---
# She is soft~<3

View file

@ -0,0 +1,5 @@
---
title: Oh, the pain
---
# AAA

View file

Before

Width:  |  Height:  |  Size: 527 KiB

After

Width:  |  Height:  |  Size: 527 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before After
Before After

View file

@ -1,5 +1,10 @@
---
title: A little image test idea
directory:
settings:
colourscheme: spicy
banners:
- src: /about/neira/Neira_Queen.png
---
# README concept

View file

@ -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

View file

@ -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"
}
}

10
www/fontawesome.php Normal file
View file

@ -0,0 +1,10 @@
<?php
$FONT_AWESOME_ARRAY=[
'markdown' => '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="20" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M593.8 59.1H46.2C20.7 59.1 0 79.8 0 105.2v301.5c0 25.5 20.7 46.2 46.2 46.2h547.7c25.5 0 46.2-20.7 46.1-46.1V105.2c0-25.4-20.7-46.1-46.2-46.1zM338.5 360.6H277v-120l-61.5 76.9-61.5-76.9v120H92.3V151.4h61.5l61.5 76.9 61.5-76.9h61.5v209.2zm135.3 3.1L381.5 256H443V151.4h61.5V256H566z"/></svg>',
'image' => '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M0 96C0 60.7 28.7 32 64 32H448c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6h96 32H424c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>',
'folder' => '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M64 480H448c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H288c-10.1 0-19.6-4.7-25.6-12.8L243.2 57.6C231.1 41.5 212.1 32 192 32H64C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64z"/></svg>',
'rss' => '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="14" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM96 136c0-13.3 10.7-24 24-24c137 0 248 111 248 248c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-110.5-89.5-200-200-200c-13.3 0-24-10.7-24-24zm0 96c0-13.3 10.7-24 24-24c83.9 0 152 68.1 152 152c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-57.4-46.6-104-104-104c-13.3 0-24-10.7-24-24zm0 120a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg>'
];
?>

View file

@ -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, "/");

View file

@ -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);
}
}
?>

2
www/robots.txt Normal file
View file

@ -0,0 +1,2 @@
User-agent: *
Allow: /

View file

@ -1,20 +1,14 @@
<?php
require_once 'vendor/autoload.php';
require_once 'post_adapter.php';
$data_time_start = microtime(true);
// $sql = mysqli_connect('mysql', 'root', 'example', 'dragon_fire');
require_once 'vendor/autoload.php';
require_once 'post_adapter.php';
require_once 'fontawesome.php';
$adapter = new PostHandler();
//if (!$sql)
// {
// echo 'Connection failed<br>';
// echo 'Error number: ' . mysqli_connect_errno() . '<br>';
// echo 'Error message: ' . mysqli_connect_error() . '<br>';
// 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']);
?>

View file

@ -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));

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

View file

@ -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;

View file

@ -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;
}
}

View file

@ -8,12 +8,9 @@
{% set split_post = post.post_path |split('/') %}
{% for i in range(0, split_post|length - 1) %}
<li>
{% if i != 0 %}
>
{% endif %}
{% if i != 0 %}
<a href="{{split_post|slice(0,i+1)|join('/')}}">
{{ split_post[i] }}
> {{ split_post[i] }}
</a>
{% else %}
<a href="/">root</a>
@ -22,9 +19,15 @@
{% endfor %}
<li style="margin-left: auto;">
<li class="hsmol_hide" style="margin-left: auto;">
<a href="/raw{{post.post_path}}">raw</a>
<a href="/api/posts{{post.post_path}}">api</a>
</li>
<li class="hsmol_hide">
<a rel="alternate" type="application/rss+xml" target="_blank"
style="padding-left: 0.3rem;" href="/feed/rss{{post.post_path}}">
{{ fa['rss']|raw }}
</a>
</li>
</menu>
</div>

View file

@ -2,6 +2,11 @@
{% extends "root.html" %}
{% block feed_links %}
<link rel="alternate" type="application/rss+xml" title="DergSite Global Feed" href="https://lucidragons.de/feed">
<link rel="alternate" type="application/atom+xml" title="DergSite Feed for {{post.post_path}}" href="https://lucidragons.de/feed/atom{{post.post_path}}">
{% endblock %}
{% block second_title %}
<h2> {{ post.post_metadata.title }} </h2>
{% endblock %}
@ -13,6 +18,10 @@
<article>
{%block content_article %}
{%endblock%}
<span id="content_footer">
This article was created on {{ post.post_create_time }}, last edited {{ post.post_update_time }}, and was viewed {{ post.post_access_count }} times~
</span>
</article>
{%endblock%}

View file

@ -14,12 +14,12 @@
<th></th>
<th>Name</th>
<th>Title</th>
<th class="entry_update_time">Modified</th>
<th class="entry_update_time hsmol_hide">Modified</th>
</tr>
{% for subpost in subposts %}
<tr class="entry">
<td>
ICN
{{ fa[subpost.post_metadata.icon] | raw }}
</td>
<td>
<a href={{subpost.post_path}}>{{subpost.post_basename}}</a>
@ -27,7 +27,7 @@
<td class="entry_title">
{{ subpost.post_metadata.title }}
</td>
<td class="entry_update_time">
<td class="entry_update_time hsmol_hide">
{{ subpost.post_update_time }}
</td>
</tr>

View file

@ -6,6 +6,14 @@
<link rel="stylesheet" href="/static/imagestyle.css">
{%endblock%}
{% block opengraph_tags %}
{{ parent() }}
<meta property="og:type" content="image" />
<meta name="twitter:card" content="summary_large_image">
<meta name="robots" content="max-image-preview:large">
{%endblock %}
{%block content_article%}
<figure>
<a target="_blank" href="{{post['post_file_dir']}}">

View file

@ -2,6 +2,12 @@
{% extends "pathed_content.html" %}
{% block opengraph_tags %}
{{ parent() }}
<meta property="og:type" content="article" />
{%endblock %}
{%block content_article%}
{{ post['post_content']|markdown_to_html }}
{% endblock %}

View file

@ -0,0 +1,19 @@
{% extends "pathed_content.html" %}
{% block extra_head %}
<link rel="stylesheet" href="/static/rrrorstyle.css">
{% endblock %}
{% block second_title %}
<h2> (Broken) </h2>
{% endblock %}
{% block content_article %}
<h1 id="rrr_header"> The Dergs are confused:</h2>
<h2 id="rrr_code"> {{ error_code }}</h1>
<div>
{{ error_description|markdown_to_html }}
</div>
{% endblock %}

View file

@ -1,21 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<title>Lucidragons' Fire</title>
<title>The Dergsite - {{og.title}}</title>
<link rel="stylesheet" href="/static/dergstyle.css">
<link rel="icon" type="image/x-icon" href="/static/icon.jpeg">
<meta name="viewport" content="width=device-width,initial-scale=1">
{% block feed_links %}
<link rel="alternate" type="application/rss+xml" title="DergSite Global Feed" href="https://lucidragons.de/feed">
{% endblock %}
{% block extra_head %}{% endblock %}
<script src="/static/banner.js" defer></script>
</head>
{% block opengraph_tags %}
<meta property="og:site_name" content="The Dergsite">
<meta property="og:url" content="{{og.url}}" />
<meta property="og:title" content="{{ og.title|e }}" />
<meta property="twitter:title" content="{{ og.title|e }}" />
<meta property="og:description" content="{{ og.description|e }}" />
<meta property="twitter:description" content="{{ og.description|e }}" />
<meta property="og:image" content="{{og.image}}" />
<meta name="twitter:image" content="{{og.image}}" />
{% endblock %}
<script type="text/javascript">
window.dergBannerOptions = JSON.parse('{{banner|raw}}');
</script>
</head>
<body>
<header id="main_header">
<img id="main_banner_img"></img>
<a id="main_banner_img_link" href="/gallery"> full picture</a>
<script src="/static/banner.js"></script>
<h1 id="big_title">{% block big_title %}The dergsite{%endblock%}</h1>
{% block second_title %}{% endblock %}
<div id="title_separator"></div>

View file

@ -1,21 +0,0 @@
{% extends "root.html" %}
{% block extra_head %}
<link rel="stylesheet" href="/static/rrrorstyle.css">
{% endblock %}
{% block second_title %}
<h2> (Broken) </h2>
{% endblock %}
{% block main_content %}
<article>
<h1 id="rrr_header"> The Dergs are confused:</h2>
<h2 id="rrr_code"> {{ error_code }}</h1>
<div>
{{ error_description|markdown_to_html }}
</div>
</article>
{% endblock %}

View file

@ -1,15 +0,0 @@
# About the dergens
### Who we are
Just little things, but it does feel good.
No spacing, huh...
### What we like to do
nom Nom Nom test?
Though I don't understand why this is not quite functional...
Markdown truly is a delight to work with!