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) %}
  • - {% if i != 0 %} - > - {% endif %} {% if i != 0 %} - {{ split_post[i] }} + > {{ split_post[i] }} {% else %} root @@ -22,9 +19,15 @@ {% endfor %} -
  • +
  • raw api
  • +
  • + + {{ fa['rss']|raw }} + +
  • \ No newline at end of file diff --git a/www/templates/pathed_content.html b/www/templates/pathed_content.html index df143f4..7ac1cdf 100644 --- a/www/templates/pathed_content.html +++ b/www/templates/pathed_content.html @@ -2,6 +2,11 @@ {% extends "root.html" %} +{% block feed_links %} + + +{% endblock %} + {% block second_title %}

    {{ post.post_metadata.title }}

    {% endblock %} @@ -13,6 +18,10 @@
    {%block content_article %} {%endblock%} + + + This article was created on {{ post.post_create_time }}, last edited {{ post.post_update_time }}, and was viewed {{ post.post_access_count }} times~ +
    {%endblock%} diff --git a/www/templates/post_types/directory.html b/www/templates/post_types/directory.html index 5fa62fc..2872ab4 100644 --- a/www/templates/post_types/directory.html +++ b/www/templates/post_types/directory.html @@ -14,12 +14,12 @@ Name Title - Modified + Modified {% for subpost in subposts %} - ICN + {{ fa[subpost.post_metadata.icon] | raw }} {{subpost.post_basename}} @@ -27,7 +27,7 @@ {{ subpost.post_metadata.title }} - + {{ subpost.post_update_time }} diff --git a/www/templates/post_types/image.html b/www/templates/post_types/image.html index 60ef590..00ea107 100644 --- a/www/templates/post_types/image.html +++ b/www/templates/post_types/image.html @@ -6,6 +6,14 @@ {%endblock%} +{% block opengraph_tags %} + {{ parent() }} + + + + +{%endblock %} + {%block content_article%}
    diff --git a/www/templates/post_types/markdown.html b/www/templates/post_types/markdown.html index d4e79a4..f874fbc 100644 --- a/www/templates/post_types/markdown.html +++ b/www/templates/post_types/markdown.html @@ -2,6 +2,12 @@ {% extends "pathed_content.html" %} +{% block opengraph_tags %} + {{ parent() }} + + +{%endblock %} + {%block content_article%} {{ post['post_content']|markdown_to_html }} {% endblock %} \ No newline at end of file diff --git a/www/templates/post_types/rrror.html b/www/templates/post_types/rrror.html new file mode 100644 index 0000000..0e258e1 --- /dev/null +++ b/www/templates/post_types/rrror.html @@ -0,0 +1,19 @@ + +{% extends "pathed_content.html" %} + +{% block extra_head %} + +{% endblock %} + +{% block second_title %} +

    (Broken)

    +{% endblock %} + +{% block content_article %} +

    The Dergs are confused:

    +

    {{ error_code }}

    + +
    + {{ error_description|markdown_to_html }} +
    +{% endblock %} \ No newline at end of file diff --git a/www/templates/root.html b/www/templates/root.html index 0bb44f5..13ba701 100644 --- a/www/templates/root.html +++ b/www/templates/root.html @@ -1,21 +1,44 @@ - Lucidragons' Fire + The Dergsite - {{og.title}} + {% block feed_links %} + + {% endblock %} + {% block extra_head %}{% endblock %} - - + {% block opengraph_tags %} + + + + + + + + + + + + + {% endblock %} + + +
    full picture + +

    {% block big_title %}The dergsite{%endblock%}

    {% block second_title %}{% endblock %}
    diff --git a/www/templates/rrror.html b/www/templates/rrror.html deleted file mode 100644 index f9f3ff9..0000000 --- a/www/templates/rrror.html +++ /dev/null @@ -1,21 +0,0 @@ - -{% extends "root.html" %} - -{% block extra_head %} - -{% endblock %} - -{% block second_title %} -

    (Broken)

    -{% endblock %} - -{% block main_content %} -
    -

    The Dergs are confused:

    -

    {{ error_code }}

    - -
    - {{ error_description|markdown_to_html }} -
    -
    -{% endblock %} \ No newline at end of file diff --git a/www/user_content/about.md b/www/user_content/about.md deleted file mode 100644 index b83b2e5..0000000 --- a/www/user_content/about.md +++ /dev/null @@ -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! \ No newline at end of file