everything(everything): simply saving work

This commit is contained in:
David Bailey 2024-08-15 22:53:55 +02:00
parent 0f2761cd61
commit 76ca7b9c32
25 changed files with 1330 additions and 561 deletions

View file

@ -0,0 +1,17 @@
<?php
interface AnalyticsInterface {
public function log_path_access($path,
$agent,
$time,
$referrer);
public function log_path_errcode(
$path,
$agent,
$referrer,
$code);
}
?>

View file

@ -0,0 +1,86 @@
<?php
function sanitize_post_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 escape_tag($tag) {
return preg_replace_callback('/[\WZ]/', function($match) {
return "Z" . ord($match[0]);
}, strtolower($tag));
}
function escape_search_tag($tag) {
preg_match("/^([\+\-]?)(.*?)(\*?)$/", $tag, $matches);
if(!isset($matches[1])) {
echo "Problem with tag!";
var_dump($tag);
}
return $matches[1] . $this->escape_tag($matches[2]) . $matches[3];
}
interface PostdataInterface {
/* Postdata format:
*
* The Postdata array is a simple intermediate data format
* for the Post content and metadata.
* It is slightly abstracted from the SQL format itself but will
* only reformat keys, *not* do any alteration of the data itself.
*
* Any supported fields will be integrated into the database.
* Other fields will be saved in a JSON structure, and will
* be restored afterward.
*
* The following fields are mandatory for *writing*
* - path: String, must be sanitized to consist of just alphanumeric
* characters, `_-./`
* used to identify the post itself
*
* The following fields may be returned by the database:
* - id
* - created_at
* - updated_at
* - view_count
*
* The following fields may be supported by the database:
* - markdown: String, markdown of the post. May be
* stored separately and won't be returned by default!
* - type: String, defining the type of the post
* - title: String, self-explanatory
* - tags: Array of strings
* - settings: Hash, recursively merged settings (calculated by DB!)
*
* The following fields are *recommended*, but nothing more:
* - icon: String, optionally defining
*/
public function stub_postdata($path);
public function stub_postdata_tree($path);
public function set_postdata($data);
public function set_post_markdown($id, $markdown);
public function get_postdata($path);
// Returns a key-value pair of child paths => child data
public function get_post_children($path,
$limit = 50, $depth_start = 1, $depth_end = 1,
$order_by = 'path');
public function get_post_markdown($id);
}
?>

View file

@ -0,0 +1,348 @@
<?php
require_once 'analytics_interface.php';
require_once 'db_interface.php';
function taglist_escape_tag($tag) {
return preg_replace_callback('/[\WZ]/', function($match) {
return "Z" . ord($match[0]);
}, strtolower($tag));
}
function taglist_to_sql_string($post_tags) {
$post_tags = array_unique($post_tags);
$post_tags = array_map(function($val) {
return taglist_escape_tag($val);
}, $post_tags);
asort($post_tags);
$post_tags = join(' ', $post_tags);
return $post_tags;
}
class MySQLHandler
implements PostdataInterface {
CONST SQL_READ_COLUMNS = [
'id', 'path', 'created_at', 'updated_at',
'title', 'view_count', 'brief'];
CONST SQL_WRITE_COLUMNS = ['path', 'title', 'brief'];
private $sql_connection;
public $hostname;
public $debugging;
function __construct($sql_connection, $hostname) {
$this->sql_connection = $sql_connection;
$this->hostname = $hostname;
$this->debugging = false;
}
private function _dbg($message) {
if($this->debugging) {
echo $message;
}
}
private function _exec($qery, $argtypes = '', ...$args) {
$stmt = $this->sql_connection->prepare($qery);
if($argtypes != ""){
$stmt->bind_param($argtypes, ...$args);
}
$stmt->execute();
return $stmt->get_result();
}
private function clear_post_settings_cache($post_path) {
$post_path = sanitize_post_path($post_path);
$this->_exec("
UPDATE posts
SET post_settings_cache=NULL
WHERE host = ? AND post_path LIKE ?;
", "ss", $this->hostname, $post_path . "%");
}
public function stub_postdata($path) {
$post_path = sanitize_post_path($path);
$path_depth = substr_count($post_path, "/");
$qry = "
INSERT INTO posts
(host, post_path, post_path_depth)
VALUES
( ?, ?, ?) AS new
ON DUPLICATE KEY UPDATE post_path=new.post_path;";
$this->_exec($qry, "ssi",
$this->hostname,
$post_path,
$path_depth);
}
public function stub_postdata_tree($path) {
$post_path = sanitize_post_path($path);
while(true) {
if($post_path == '/') {
$post_path = '';
}
try {
$this->stub_postdata($post_path);
}
catch(Exception $e) {
}
$post_path = dirname($post_path);
if(strlen($post_path) == 0) {
break;
}
}
}
public function set_postdata($data) {
$data['path'] = sanitize_post_path($data['path']);
$post_path = $data['path'];
unset($data['path']);
$this->stub_postdata_tree($post_path);
$data['title'] ??= basename($post_path);
$post_tags = $data['tags'] ?? [];
array_push($post_tags,
'path:' . $post_path
);
$sql_args = [
$this->hostname,
$post_path,
substr_count($post_path, "/"),
$data['title'],
taglist_to_sql_string($post_tags),
$data['brief'] ?? null
];
unset($data['title']);
unset($data['brief']);
$post_markdown = $data['markdown'] ?? null;
unset($data['markdown']);
unset($data['html']);
array_push($sql_args, json_encode($data));
$qry =
"INSERT INTO posts
(host,
post_path, post_path_depth,
post_title, post_tags, post_brief,
post_metadata, post_settings_cache)
VALUES
( ?, ?, ?, ?, ?, ?, ?, null) AS new
ON DUPLICATE KEY
UPDATE post_title=new.post_title,
post_tags=new.post_tags,
post_brief=new.post_brief,
post_metadata=new.post_metadata,
post_updated_at=CURRENT_TIMESTAMP;
";
$this->_exec($qry, "ssissss", ...$sql_args);
if(isset($post_markdown)) {
$this->set_post_markdown($this->sql_connection->insert_id, $post_markdown);
}
$this->clear_post_settings_cache($post_path);
}
public function set_post_markdown($id, $markdown) {
$qry =
"INSERT INTO post_markdown ( post_id, post_markdown )
VALUES (?, ?) AS new
ON DUPLICATE KEY UPDATE post_markdown=new.post_markdown;
";
$this->_exec($qry, "is", $id, $markdown);
}
private function get_post_settings($post_path) {
$post_path = sanitize_post_path($post_path);
$this->_dbg("-> gps: getting path " . $post_path . "\n");
$post_settings = $this->_exec("
SELECT post_settings_cache
FROM posts
WHERE post_path = ? AND host = ?
", "ss", $post_path, $this->hostname)->fetch_assoc();
if(!isset($post_settings)) {
$this->_dbg("-> gps: Returning because of no result\n");
return [];
}
if(isset($post_settings['post_settings_cache'])) {
$result = json_decode($post_settings['post_settings_cache'], true);
if($this->debugging) {
echo "-> gps: Returning because of cached result:\n";
echo "--> " . json_encode($result) . "\n";
}
return $result;
}
$parent_settings = [];
if($post_path != "") {
$parent_settings = $this->get_post_settings(dirname($post_path));
}
$post_settings = [];
$post_metadata = $this->_exec("
SELECT post_path, post_metadata
FROM posts
WHERE post_path = ? AND host = ?
", "ss", $post_path, $this->hostname)->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->_dbg("-> gps: Merged post settings are " . json_encode($post_settings) . ", saving...\n");
$this->_exec("
UPDATE posts SET post_settings_cache=? WHERE post_path=? AND host=?
", "sss",
json_encode($post_settings), $post_path, $this->hostname);
return $post_settings;
}
private function process_postdata($data) {
if(!isset($data)) {
return null;
}
if(!isset($data['post_path'])) {
echo "ERROR, trying to get a post data package without path!";
die();
}
$outdata = [];
foreach($this::SQL_READ_COLUMNS as $key) {
if(isset($data['post_' . $key])) {
$outdata[$key] = $data['post_' . $key];
}
}
$post_metadata = json_decode($data['post_metadata'] ?? '{}', true);
$post_settings = [];
if(isset($data['post_settings_cache'])) {
$post_settings = json_decode($data['post_settings_cache'], true);
}
else {
$post_settings = $this->get_post_settings($data['post_path']);
}
$outdata = array_merge($post_settings, $post_metadata, $outdata);
return $outdata;
}
public function get_postdata($path) {
$path = sanitize_post_path($path);
$qry = "
SELECT *
FROM posts
WHERE post_path = ? AND host = ?;
";
$data = $this->_exec($qry, "ss", $path, $this->hostname)->fetch_assoc();
return $this->process_postdata($data);
}
public function get_post_children($path,
$limit = 50, $depth_start = 1, $depth_end = 1,
$order_by = 'path') {
$path = sanitize_post_path($path);
$path_depth = substr_count($path, "/");
$allowed_ordering = [
'path' => true,
'path DESC' => true,
'created_at' => true,
'created_at DESC' => true,
'modified_at' => true,
'modified_at DESC' => true
];
if(!isset($allowed_ordering[$order_by])) {
throw new Exception('Children ordering not allowed');
}
$order_by = 'post_' . $order_by;
if($this->debugging) {
echo "-> GPC: Getting children for path " . $path;
}
$qry = "
SELECT *
FROM posts
WHERE post_path_depth BETWEEN ? AND ?
AND post_path LIKE ?
ORDER BY " . $order_by .
" LIMIT ?";
$data = $this->_exec($qry, "iisi",
$path_depth + $depth_start, $path_depth + $depth_end,
$path.'/%', $limit
)->fetch_all(MYSQLI_ASSOC);
$outdata = [];
foreach($data AS $post_element) {
$outdata[$post_element['post_path']] =
$this->process_postdata($post_element);
}
return $outdata;
}
public function get_post_markdown($id) {
$qry =
"SELECT post_markdown
FROM post_markdown
WHERE post_id = ?
";
$data = $this->_exec($qry, "i", $id)->fetch_assoc();
if(!isset($data)) {
return "";
}
return $data['post_markdown'];
}
}
?>

205
www/src/db_handler/post.php Normal file
View file

@ -0,0 +1,205 @@
<?php
class Post implements ArrayAccess {
public $handler;
private $content_html;
private $content_markdown;
public $site_defaults;
public $data;
public $html_data;
public $raw_data;
private $_child_posts;
public static function _generate_404($post_data) {
$post_data ??= [
'id' => -1,
'path' => '/404.md',
'title' => '404 Page',
'metadata' => [
'type' => '404'
]
];
return $post_data;
}
public static function _deduce_type($path) {
$ext = pathinfo($path, PATHINFO_EXTENSION);
if(preg_match("/\.(\w+)\.md$/", $path, $ext_match)) {
$ext = $ext_match[1];
}
$ext_mapping = [
'' => 'directory',
'md' => 'text/markdown',
'png' => 'image',
'jpg' => 'image',
'jpeg' => 'image'
];
return $ext_mapping[$ext] ?? '?';
}
public static function _deduce_icon($type) {
$icon_mapping = [
'' => 'question',
'text/markdown' => 'markdown',
'blog' => 'markdown',
'blog_list' => 'rectangle-list',
'directory' => 'folder',
'gallery' => 'images',
'image' => 'image'
];
return $icon_mapping[$type] ?? 'unknown';
}
function __construct($post_handler, $post_data) {
$this->handler = $post_handler;
$this->content_html = null;
$this->content_markdown = null;
$this->site_defaults = null;
if(!isset($post_data) or !isset($post_data['id'])) {
$post_data = $this->_generate_404($post_data);
}
$data = $post_data;
$post_data['host'] ??= 'localhost:8081';
$data['url'] ??= 'https://' . $post_data['host'] . $post_data['path'];
$data['title'] ??= basename($data['path']);
$data['tags'] ??= [];
$data['type'] ??= self::_deduce_type($post_data['path']);
$data['icon'] ??= self::_deduce_icon($data['type']);
if(isset($sql_meta['media_url'])) {
$data['thumb_url'] ??= $data['media_url'];
}
$data['preview_image'] ??= $data['banners'][0]['src'] ?? null;
$data['brief'] ??= $data['title'];
$this->data = $data;
}
public function __get($name) {
if($name == 'html') {
return $this->get_html();
}
if($name == 'markdown') {
return $this->get_markdown();
}
if($name == 'json') {
return $this->to_json();
}
if($name == 'child_posts') {
return $this->get_child_posts();
}
if(isset($this->data[$name])) {
return $this->data[$name];
}
if(is_null($this->site_defaults)) {
throw new RuntimeException("Post site defaults have not been set properly!");
}
return $this->site_defaults[$name] ?? null;
}
public function offsetGet($offset) : mixed {
return $this->__get($offset) ?? null;
}
public function offsetExists($offset) : bool {
if(isset($this->data[$offset])) {
return true;
}
if(isset($this->site_defaults[$offset])) {
return true;
}
return !is_null($this->offsetGet($offset));
}
public function offsetSet($offset, $value) : void {
throw RuntimeError("Setting of post data is not allowed!");
}
public function offsetUnset($offset) : void {
throw RuntimeError("Unsetting of post data is not allowed!");
}
public function get_html() {
$this->content_html ??= $this->handler->render_post($this);
return $this->content_html;
}
public function get_markdown() {
$this->content_markdown ??=
$this->handler->get_markdown_for($this);
return $this->content_markdown;
}
public function get_child_posts(...$search_args) {
var_dump($search_args);
if(count($search_args) == 0) {
$this->child_posts ??=
$this->handler->get_children_for($this);
return $this->child_posts;
}
else {
return $this->handler->get_children_for($this, ...$search_args);
}
}
public function to_array($options = []) {
$out_data = $this->data;
if(isset($options['markdown'])) {
$out_data['markdown'] = $this->get_markdown();
}
if(isset($options['html'])) {
$out_data['html'] = $this->get_html();
}
if(isset($options['children'])) {
die();
}
return $out_data;
}
public function to_json($options = []) {
return json_encode($this->to_array($options));
}
public function get_parent_post() {
$parent_path = dirname($this->data['path']);
if($parent_path == '')
return null;
$this->parent_post ??= new PostData($this->handler,
$this->handler->get_post_by_path($parent_path));
return $this->parent_post;
}
}
?>

View file

@ -0,0 +1,57 @@
<?php
require_once 'db_interface.php';
require_once 'db_handler/post.php';
class PostHandler {
private $db;
private $posts;
public $markdown_engine;
function __construct($db_adapter) {
$this->db = $db_adapter;
$this->posts = [];
$this->markdown_engine = null;
}
public function get_post($key) {
$key = sanitize_post_path($key);
if(isset($this->posts[$key])) {
return $this->posts[$key];
}
$post_data = $this->db->get_postdata($key);
$post = null;
if(isset($post_data)) {
$post = new Post($this, $post_data);
}
$this->posts[$key] = $post;
return $post;
}
public function get_markdown_for($post) {
return $this->db->get_post_markdown($post->id);
}
public function render_post($post) {
return $this->markdown_engine($post);
}
public function get_children_for($post, ...$search_opts) {
$child_list = $this->db->get_post_children($post->path, ...$search_opts);
$out_list = [];
foreach($child_list as $child_data) {
array_push($out_list, new Post($this, $child_data));
}
return $out_list;
}
}
?>

187
www/src/dbtest.php Normal file
View file

@ -0,0 +1,187 @@
<?php
header("content-type: text/plain; charset=UTF-8; imeanit=yes");
header("X-Content-Type-Options: nosniff");
header('Content-Disposition: inline');
// Reporting E_NOTICE can be good too (to report uninitialized
// variables or catch variable name misspellings ...)
error_reporting(E_ERROR | E_WARNING | E_PARSE | E_NOTICE);
$data_time_start = microtime(true);
require_once '../vendor/autoload.php';
require_once 'db_handler/mysql_handler.php';
require_once 'db_handler/post_handler.php';
require_once 'fontawesome.php';
require_once 'dergdown.php';
use Symfony\Component\Yaml\Yaml;
$SERVER_HOST = $_SERVER['HTTP_HOST'];
if(!preg_match('/^[\w\.\:]+$/', $SERVER_HOST)) {
http_response_code(500);
echo "Not a valid server host (was " . $SERVER_HOST . ")";
die();
}
$SERVER_PREFIX = "https://" . $SERVER_HOST;
$SITE_CONFIG = Yaml::parseFile('../secrets/' . $SERVER_HOST . '.config.yml');
$SITE_CONFIG['uri_prefix'] = $SERVER_PREFIX;
$SITE_CONFIG['HTTP_HOST'] = $SERVER_HOST;
$db_params = $SITE_CONFIG['db'];
$db_connection = null;
try {
if(false !== getenv('MYSQL_HOST')) {
$db_connection = mysqli_connect(getenv('MYSQL_HOST'),
getenv('MYSQL_USER'), getenv('MYSQL_PASSWORD'),
getenv('MYSQL_DATABASE'),
getenv('MYSQL_PORT'));
}
else {
$db_connection = mysqli_connect($db_params['host'],
$db_params['user'], $db_params['password'],
$db_params['database'],
$db_params['port']);
}
} catch (\Throwable $th) {
echo 'Connection failed<br>';
echo 'Error number: ' . mysqli_connect_errno() . '<br>';
echo 'Error message: ' . mysqli_connect_error() . '<br>';
die();
}
$db_connection->execute_query("DELETE FROM posts;");
$sql_adapter = new MySQLHandler($db_connection, $SERVER_HOST);
$adapter = new PostHandler($sql_adapter);
$sql_adapter->debugging = true;
function test_accounce($title) {
echo "\n\n===========================================
_______ ______ _____ _______
|__ __| ____|/ ____|__ __|
| | | |__ | (___ | | (_)
| | | __| \___ \ | |
| | | |____ ____) | | | _
|_| |______|_____/ |_| (_)
";
echo "==== " . $title . "\n";
echo "===========================================\n";
}
function adapter_fetch($post_path) {
global $db_connection;
global $sql_adapter;
echo "-> Fetching path " . $post_path . "\n";
echo json_encode($db_connection->execute_query("SELECT * FROM posts WHERE post_path=?", [
$post_path
])->fetch_assoc(), JSON_PRETTY_PRINT);
echo "\n-> Adapter output:\n";
echo json_encode($sql_adapter->get_postdata($post_path), JSON_PRETTY_PRINT) . "\n";
}
echo "Starting test...\n";
echo "Trying just a stub...\n";
$sql_adapter->stub_postdata_tree('/testing/stubtest/1/2/3.md');
echo "Stubbed~\n\n";
echo "Getting the stub post...\n";
echo json_encode($sql_adapter->get_postdata('/testing'), JSON_PRETTY_PRINT);
echo "\n\n";
test_accounce("Basic postdata setting");
$sql_adapter->set_postdata([
'path' => '/testing/settest/test.md',
'title' => 'One heck of a test!',
'type' => 'text/markdown',
'tags' => [
'test',
'type:text'
],
'overridetest' => 'metadata'
]);
echo "\nDone!";
adapter_fetch('/testing/settest/test.md');
echo "Done!\n\n";
test_accounce("Setting post markdown...");
$sql_adapter->set_postdata([
'path' => '/testing/markdowntest',
'markdown' => 'Inline markdown test should work...'
]);
$post = $sql_adapter->get_postdata('/testing/markdowntest');
var_dump($sql_adapter->get_post_markdown($post['id']));
$sql_adapter->set_post_markdown($post['id'],
'This is one hell of a cute test!'
);
var_dump($sql_adapter->get_post_markdown($post['id']));
unset($post);
test_accounce("Settings inheritance test...");
echo "Setting on a parent file...\n";
$sql_adapter->set_postdata([
'path' => '/testing/settest',
'settings' => [
'nom' => true,
'type' => 'frame',
'overridetest' => 'settings'
]
]);
echo "\nAnd checking if that held!\n";
adapter_fetch('/testing/settest');
adapter_fetch('/testing/settest/test.md');
test_accounce("Testing getting child posts");
echo json_encode($sql_adapter->get_post_children('/testing'), JSON_PRETTY_PRINT);
echo "\n\n------------------------------------------------------\n";
echo "TEST PHASE: Adapter testing";
echo "\n------------------------------------------------------\n\n";
$post = $adapter->get_post('/testing/markdowntest');
echo "Post path is " . $post->path . "\n";
echo "Post markdown is " . $post->markdown . "\n";
echo $post->to_json();
echo "\n\n";
echo $post->to_json([
'markdown' => true
]);
test_accounce("Fetching child posts");
echo "Root children:\n". json_encode(array_map(function($data) {
return $data->to_array();
}, $adapter->get_post('/')->child_posts), JSON_PRETTY_PRINT);
echo "\n\n";
echo "Root children, extended:\n" . json_encode(array_map(function($data) {
return $data->to_array();
}, $adapter->get_post('/')->get_child_posts(depth_end: 3)), JSON_PRETTY_PRINT);
echo "\n\n";
?>

39
www/src/dergdown.php Normal file
View file

@ -0,0 +1,39 @@
<?php
use Highlight\Highlighter;
class Dergdown extends ParsedownExtra
{
protected $highlighter;
public function __construct()
{
$this->highlighter = new Highlighter();
}
protected function blockFencedCodeComplete($block)
{
if (! isset($block['element']['text']['attributes'])) {
return $block;
}
$code = $block['element']['text']['text'];
$languageClass = $block['element']['text']['attributes']['class'];
$language = explode('-', $languageClass);
try {
$highlighted = $this->highlighter->highlight($language[1], $code);
$block['element']['text']['attributes']['class'] = vsprintf('%s hljs %s', [
$languageClass,
$highlighted->language,
]);
$block['element']['text']['rawHtml'] = $highlighted->value;
unset($block['element']['text']['text']);
} catch (DomainException $e) {
}
return $block;
}
}

16
www/src/fontawesome.php Normal file
View file

@ -0,0 +1,16 @@
<?php
$FONT_AWESOME_ARRAY=[
'bars' => '<svg xmlns="http://www.w3.org/2000/svg" class="fa-icn" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M0 96C0 78.3 14.3 64 32 64H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z"/></svg>',
'markdown' => '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="20" class="fa-icn" 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" class="fa-icn" 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>',
'images' => '<svg xmlns="http://www.w3.org/2000/svg" class="fa-icn" viewBox="0 0 576 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M160 32c-35.3 0-64 28.7-64 64V320c0 35.3 28.7 64 64 64H512c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H160zM396 138.7l96 144c4.9 7.4 5.4 16.8 1.2 24.6S480.9 320 472 320H328 280 200c-9.2 0-17.6-5.3-21.6-13.6s-2.9-18.2 2.9-25.4l64-80c4.6-5.7 11.4-9 18.7-9s14.2 3.3 18.7 9l17.3 21.6 56-84C360.5 132 368 128 376 128s15.5 4 20 10.7zM192 128a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zM48 120c0-13.3-10.7-24-24-24S0 106.7 0 120V344c0 75.1 60.9 136 136 136H456c13.3 0 24-10.7 24-24s-10.7-24-24-24H136c-48.6 0-88-39.4-88-88V120z"/></svg>',
'turn-up' => '<svg xmlns="http://www.w3.org/2000/svg" class="fa-icn" viewBox="0 0 384 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M350 177.5c3.8-8.8 2-19-4.6-26l-136-144C204.9 2.7 198.6 0 192 0s-12.9 2.7-17.4 7.5l-136 144c-6.6 7-8.4 17.2-4.6 26s12.5 14.5 22 14.5h88l0 192c0 17.7-14.3 32-32 32H32c-17.7 0-32 14.3-32 32v32c0 17.7 14.3 32 32 32l80 0c70.7 0 128-57.3 128-128l0-192h88c9.6 0 18.2-5.7 22-14.5z"/></svg>',
'rectangle-list' => '<svg xmlns="http://www.w3.org/2000/svg" class="fa-icn" viewBox="0 0 576 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M64 80c-8.8 0-16 7.2-16 16V416c0 8.8 7.2 16 16 16H512c8.8 0 16-7.2 16-16V96c0-8.8-7.2-16-16-16H64zM0 96C0 60.7 28.7 32 64 32H512c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96zm96 64a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zm104 0c0-13.3 10.7-24 24-24H448c13.3 0 24 10.7 24 24s-10.7 24-24 24H224c-13.3 0-24-10.7-24-24zm0 96c0-13.3 10.7-24 24-24H448c13.3 0 24 10.7 24 24s-10.7 24-24 24H224c-13.3 0-24-10.7-24-24zm0 96c0-13.3 10.7-24 24-24H448c13.3 0 24 10.7 24 24s-10.7 24-24 24H224c-13.3 0-24-10.7-24-24zm-72-64a32 32 0 1 1 0-64 32 32 0 1 1 0 64zM96 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg>',
'folder-tree' => '<svg xmlns="http://www.w3.org/2000/svg" class="fa-icn" viewBox="0 0 576 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M64 32C64 14.3 49.7 0 32 0S0 14.3 0 32v96V384c0 35.3 28.7 64 64 64H256V384H64V160H256V96H64V32zM288 192c0 17.7 14.3 32 32 32H544c17.7 0 32-14.3 32-32V64c0-17.7-14.3-32-32-32H445.3c-8.5 0-16.6-3.4-22.6-9.4L409.4 9.4c-6-6-14.1-9.4-22.6-9.4H320c-17.7 0-32 14.3-32 32V192zm0 288c0 17.7 14.3 32 32 32H544c17.7 0 32-14.3 32-32V352c0-17.7-14.3-32-32-32H445.3c-8.5 0-16.6-3.4-22.6-9.4l-13.3-13.3c-6-6-14.1-9.4-22.6-9.4H320c-17.7 0-32 14.3-32 32V480z"/></svg>',
'folder' => '<svg xmlns="http://www.w3.org/2000/svg" class="fa-icn" 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>',
'folder-open' => '<svg xmlns="http://www.w3.org/2000/svg" class="fa-icn" viewBox="0 0 576 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M88.7 223.8L0 375.8V96C0 60.7 28.7 32 64 32H181.5c17 0 33.3 6.7 45.3 18.7l26.5 26.5c12 12 28.3 18.7 45.3 18.7H416c35.3 0 64 28.7 64 64v32H144c-22.8 0-43.8 12.1-55.3 31.8zm27.6 16.1C122.1 230 132.6 224 144 224H544c11.5 0 22 6.1 27.7 16.1s5.7 22.2-.1 32.1l-112 192C453.9 474 443.4 480 432 480H32c-11.5 0-22-6.1-27.7-16.1s-5.7-22.2 .1-32.1l112-192z"/></svg>',
'rss' => '<svg xmlns="http://www.w3.org/2000/svg" height="16" class="fa-icn" 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

@ -0,0 +1,445 @@
<?php
class MySQLAdapter {
public $raw;
function __construct($SITE_CONFIG) {
$this->SITE_CONFIG = $SITE_CONFIG;
$db_params = $SITE_CONFIG['db'];
try {
if(false !== getenv('MYSQL_HOST')) {
$this->raw = mysqli_connect(getenv('MYSQL_HOST'),
getenv('MYSQL_USER'), getenv('MYSQL_PASSWORD'),
getenv('MYSQL_DATABASE'),
getenv('MYSQL_PORT'));
}
else {
$this->raw = mysqli_connect($db_params['host'],
$db_params['user'], $db_params['password'],
$db_params['database'],
$db_params['port']);
}
} catch (\Throwable $th) {
echo 'Connection failed<br>';
echo 'Error number: ' . mysqli_connect_errno() . '<br>';
echo 'Error message: ' . mysqli_connect_error() . '<br>';
die();
}
}
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);
if($argtypes != ""){
$stmt->bind_param($argtypes, ...$args);
}
$stmt->execute();
return $stmt->get_result();
}
function _normalize_post_data($post_data) {
$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) ?? [];
$post_data["post_content"] ??= '';
return $post_data;
}
function _normalize_post_array($post_data) {
$post_data ??= [];
return array_map(function($post) {
return $this->_normalize_post_data($post);
}, $post_data);
}
function bump_post($post_path, $post_metadata = [], $create_dirs = true) {
$post_path = $this->_sanitize_path($post_path);
$path_depth = substr_count($post_path, "/");
if($create_dirs) {
$this->make_post_directory(dirname($post_path));
}
$qry = "
INSERT INTO posts
(host, post_path, post_path_depth, post_metadata, post_content)
VALUES
( ?, ?, ?, ?, ?) AS new
ON DUPLICATE KEY UPDATE post_path=new.post_path;";
$this->_exec($qry, "ssiss",
$this->SITE_CONFIG['HTTP_HOST'],
$post_path,
$path_depth,
json_encode($post_metadata),
'');
}
function make_post_directory($directory) {
$json_metadata = ["type" => 'directory'];
while(strlen($directory) > 1) {
try {
$this->bump_post($directory, $json_metadata, false);
}
catch(Exception $e) {
}
$directory = dirname($directory);
}
}
function log_post_access($post_path, $agent, $referrer, $time) {
$post_path = $this->_sanitize_path($post_path);
$qry = "INSERT INTO path_access_counts
(access_time,
host, 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, "ssssd", $this->SITE_CONFIG['HTTP_HOST'], $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=? AND host=?", "ss",
$post_path, $this->SITE_CONFIG['HTTP_HOST']);
}
}
function get_post_access_counters() {
$qry = "
SELECT host, 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 host, 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";
$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 . ",host=" . $post_data['host'] . ",agent=".$post_data['agent'];
$out_data .= ",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 host = ? AND post_path LIKE ?;
", "ss", $this->SITE_CONFIG['HTTP_HOST'], $post_path . "%");
}
function escape_tag($tag) {
return preg_replace_callback('/[\WZ]/', function($match) {
return "Z" . ord($match[0]);
}, strtolower($tag));
}
function escape_search_tag($tag) {
preg_match("/^([\+\-]?)(.*?)(\*?)$/", $tag, $matches);
if(!isset($matches[1])) {
echo "Problem with tag!";
var_dump($tag);
}
return $matches[1] . $this->escape_tag($matches[2]) . $matches[3];
}
function update_post_search_data($post_path, $post_tags) {
$post_tags []= "path:" . $post_path;
$post_tags []= "host:" . $this->SITE_CONFIG['HTTP_HOST'];
$post_tags = array_unique($post_tags);
$post_tags = array_map(function($val) {
return $this->escape_tag($val);
}, $post_tags);
asort($post_tags);
$post_tags = join(' ', $post_tags);
$qry = "
INSERT INTO posts
( host, post_path, post_tags )
VALUES
( ?, ?, ? ) AS new
ON DUPLICATE KEY
UPDATE post_tags=new.post_tags;
";
$this->_exec($qry, "sss",
$this->SITE_CONFIG['HTTP_HOST'], $post_path, $post_tags);
}
function perform_post_search($taglist, $order = null, $limit = 20, $page = 0) {
$allowed_ordering = [
"post_create_time"
];
$qry = "
SELECT *
FROM posts
WHERE MATCH(post_tags) AGAINST (? IN BOOLEAN MODE)
";
if(!is_array($taglist)) {
$taglist = explode(' ', $taglist);
}
$taglist []= '+host:' . $this->SITE_CONFIG['HTTP_HOST'];
$taglist = array_unique($taglist);
$taglist = array_map(function($key) {
return $this->escape_search_tag($key);
}, $taglist);
$taglist = implode(' ', $taglist);
if(isset($order) and in_array($order, $allowed_ordering)) {
$qry = $qry . " ORDER BY " . $order;
}
$qry = $qry . " LIMIT ? OFFSET ?";
$search_results = $this->_exec($qry, "sii", $taglist, $limit, $limit * $page)->fetch_all(MYSQLI_ASSOC);
$search_results = [
"query_string" => $taglist,
"results" => $this->_normalize_post_array($search_results)
];
return $search_results;
}
function update_or_create_post($post_path, $post_metadata, $post_content) {
$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
(host, 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,
post_update_time=CURRENT_TIMESTAMP;";
$this->_exec($qry, "ssiss",
$this->SITE_CONFIG['HTTP_HOST'],
$post_path,
$path_depth,
json_encode($post_metadata),
$post_content);
$this->update_post_search_data($post_path, $post_metadata['tags'] ?? []);
}
function get_settings_for_path($post_path) {
$post_path = $this->_sanitize_path($post_path);
$post_settings = $this->_exec("
SELECT post_path, post_settings_cache
FROM posts
WHERE post_path = ? AND host = ?
", "ss", $post_path, $this->SITE_CONFIG['HTTP_HOST'])->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 = ? AND host = ?
", "ss", $post_path, $this->SITE_CONFIG['HTTP_HOST'])->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 = ? AND host = ?
";
$post_path = $this->_sanitize_path($post_path);
$post_data = $this->_exec($qry, "ss", $post_path, $this->SITE_CONFIG['HTTP_HOST'])->fetch_assoc();
$post_data ??= ['found' => false];
$post_data['post_path'] = $post_path;
$post_data['parent_path'] = dirname($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;
}
function get_markdown_for_id($id) {
$qry = "SELECT post_content
FROM posts
WHERE post_id = ? AND host = ?
";
$post_data = $this->_exec($qry, "is", $id,
$this->SITE_CONFIG['HTTP_HOST']
)->fetch_assoc();
return $post_data['post_content'];
}
function get_subposts_by_path($path) {
global $sql;
$path = $this->_sanitize_path($path);
$path_depth = substr_count($path, "/");
$qry = "SELECT post_path, post_metadata, post_update_time
FROM posts
WHERE
host = ?
AND (post_path LIKE CONCAT(?,'/%'))
AND post_path_depth = ?
ORDER BY post_path ASC
LIMIT 50";
$post_data = $this->_exec($qry, "ssi", $this->SITE_CONFIG['HTTP_HOST'],
$path, $path_depth+1)->fetch_all(MYSQLI_ASSOC);
$post_data = $this->_normalize_post_array($post_data);
return $post_data;
}
}
?>

346
www/src/old_router.php Normal file
View file

@ -0,0 +1,346 @@
<?php
$data_time_start = microtime(true);
require_once 'vendor/autoload.php';
require_once 'db_handler/mysql_handler.php';
require_once 'db_handler/post_handler.php';
require_once 'post.php';
require_once 'fontawesome.php';
require_once 'dergdown.php';
use Symfony\Component\Yaml\Yaml;
$SERVER_HOST = $_SERVER['HTTP_HOST'];
if(!preg_match('/^[\w\.]+$/', $SERVER_HOST)) {
die();
}
$SERVER_PREFIX = "https://" . $SERVER_HOST;
$SITE_CONFIG = Yaml::parseFile('secrets/' . $SERVER_HOST . '.config.yml');
$SITE_CONFIG['uri_prefix'] = $SERVER_PREFIX;
$SITE_CONFIG['HTTP_HOST'] = $SERVER_HOST;
$db_params = $SITE_CONFIG['db'];
$db_connection = null;
try {
if(false !== getenv('MYSQL_HOST')) {
$db_connection = mysqli_connect(getenv('MYSQL_HOST'),
getenv('MYSQL_USER'), getenv('MYSQL_PASSWORD'),
getenv('MYSQL_DATABASE'),
getenv('MYSQL_PORT'));
}
else {
$db_connection = mysqli_connect($db_params['host'],
$db_params['user'], $db_params['password'],
$db_params['database'],
$db_params['port']);
}
} catch (\Throwable $th) {
echo 'Connection failed<br>';
echo 'Error number: ' . mysqli_connect_errno() . '<br>';
echo 'Error message: ' . mysqli_connect_error() . '<br>';
die();
}
$sql_adapter = new MySQLHandler($db_connection, $SERVER_HOST);
$adapter = new PostHandler($sql_adapter);
$loader = new \Twig\Loader\FilesystemLoader(['./templates', './user_content']);
$twig = new \Twig\Environment($loader,[
'debug' => true,
'cache' => 'twig_cache'
]);
$twig->addExtension(new Twig\Extra\Markdown\MarkdownExtension());
use Twig\Extra\Markdown\DefaultMarkdown;
use Twig\Extra\Markdown\MarkdownRuntime;
use Twig\RuntimeLoader\RuntimeLoaderInterface;
function dergdown_to_html($text) {
$Parsedown = new Dergdown();
return $Parsedown->text($text);
}
function post_to_html($post) {
return dergdown_to_html($post->markdown);
}
PostData::$markdown_engine = "post_to_html";
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) {
return new MarkdownRuntime(new DefaultMarkdown());
}
}
});
function render_twig($template, $args = []) {
global $twig;
global $FONT_AWESOME_ARRAY;
global $SITE_CONFIG;
$args['fa'] = $FONT_AWESOME_ARRAY;
$post = $args['post'] ?? [];
$settings = $post['settings'] ?? [];
$meta = $post['post_metadata'] ?? [];
$args['banner'] ??= $settings['banners'] ?? $SITE_CONFIG['banners'];
$args['og'] = array_merge([
"site_name" => $SITE_CONFIG['opengraph']['site_name'],
"title" => $meta['title'] ?? $SITE_CONFIG['opengraph']['site_name'],
"url" => $_SERVER['REQUEST_URI'],
"type" => "article",
"description" => $meta['description']
?? $settings['description']
?? $SITE_CONFIG['opengraph']['description']
], $args['og'] ?? []);
if(($meta['type'] ?? '') == 'image') {
$args['og']['image'] ??= $meta['media_file'];
$args['og']['type'] = "image";
}
$args['og']['image'] ??= $args['banner'][0]["src"];
$args['banner'] = json_encode($args['banner']);
$args['site_config'] = $SITE_CONFIG;
$args['age_gate'] = (!isset($_COOKIE['AgeConfirmed'])) && isset($SITE_CONFIG['age_gate']);
$args['content_html'] ??= dergdown_to_html($post['post_content'] ?? '');
echo $twig->render($template, $args);
}
function try_render_ajax($SURI) {
global $adapter;
$match = null;
preg_match('/^\/ajax\/([^\/]+)(.*)$/', $SURI, $match);
if(!isset($match)) {
die();
}
$post = $adapter->get_post_by_path($match[2]);
$subposts = $adapter->get_subposts_by_path($match[2]);
echo render_twig('ajax/' . $match[1] . '.html', [
"post" => $post,
"subposts" => $subposts
]);
}
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 'blog':
case 'text/markdown':
echo render_twig('post_types/markdown.html', [
"post" => $post
]);
break;
case 'gallery':
if(preg_match('/^(.*[^\/])((?:#.*)?)$/', $SURI, $match)) {
header('Location: ' . $match[1] . '/' . $match[2]);
die();
}
$search_query = $post['post_metadata']['search_tags'] ??
('+type:image +path:' . $post['post_path'] . '/*');
$search_result = $adapter->perform_post_search($search_query);
echo render_twig('post_types/gallery.html', [
"post" => $post,
"subposts" => $adapter->get_subposts_by_path($SURI),
"gallery_images" => $search_result['results']
]);
break;
case 'blog_list':
if(preg_match('/^(.*[^\/])((?:#.*)?)$/', $SURI, $match)) {
header('Location: ' . $match[1] . '/' . $match[2]);
die();
}
$search_query = $post['post_metadata']['search_tags'] ??
('+type:blog +path:' . $post['post_path'] . '/*');
$search_result = $adapter->perform_post_search($search_query);
$search_result = array_map(function($key) {
$post = new PostData($adapter, $key);
return $post->data;
}, $search_result['results']);
echo render_twig('post_types/blog_list.html', [
"post" => $post,
"subposts" => $adapter->get_subposts_by_path($SURI),
"blog_posts" => $search_result
]);
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');
$post = new PostData($adapter, $adapter->get_post_by_path($match[1]));
echo $post->to_json(with_markdown: true, with_html: true);
} 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($SURI == '/api/search') {
header('Content-Type: application/json');
echo json_encode($adapter->perform_post_search($_GET['search_query']));
}
} elseif(preg_match('/^\/ajax\//', $SURI)) {
try_render_ajax($SURI);
} 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'];
} else {
try_render_post($SURI);
}
}
$URL_PATH = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
generate_website($URL_PATH);
log_and_die($URL_PATH);
?>

19
www/src/router.php Normal file
View file

@ -0,0 +1,19 @@
<?php
$data_time_start = microtime(true);
require_once '../vendor/autoload.php';
require_once 'setup_site_config.php';
require_once 'setup_db.php';
require_once 'fontawesome.php';
require_once 'dergdown.php';
require_once 'setup_twig.php';
$PARSED_URL = parse_url($_SERVER['REQUEST_URI']);
require_once 'serve_post.php';
?>

46
www/src/serve_post.php Normal file
View file

@ -0,0 +1,46 @@
<?php
require_once 'fontawesome.php';
$post = $adapter->get_post($PARSED_URL['path']);
if(isset($post)) {
$post->site_defaults = $SITE_CONFIG['site_defaults'];
}
function render_root_template($template, $args = []) {
global $twig;
global $FONT_AWESOME_ARRAY;
global $SITE_CONFIG;
$args['fontawesome'] = $FONT_AWESOME_ARRAY;
$args['page'] ??= $SITE_CONFIG['site_defaults'];
$page = $args['page'];
$args['opengraph'] = [
"site_name" => $page['site_name'] ?? 'UNSET SITE NAME',
"title" => $page['title'] ?? 'UNSET TITLE',
"url" => $page['url'] ?? 'UNSET URL',
"description" => $page['description'] ?? 'UNSET DESCRIPTION'
];
$args['banners'] = json_encode($page['banners'] ?? []);
$args['age_gate'] = (!isset($_COOKIE['AgeConfirmed']))
&& isset($SITE_CONFIG['age_gate']);
echo $twig->render($template, $args);
}
render_root_template('root.html', [
'page' => $post
]);
die();
if(!isset($post)) {
render_404();
die();
}
?>

43
www/src/setup_db.php Normal file
View file

@ -0,0 +1,43 @@
<?php
require_once 'db_handler/mysql_handler.php';
require_once 'db_handler/post_handler.php';
$db_params = $SITE_CONFIG['db'];
$db_connection = null;
try {
if(false !== getenv('MYSQL_HOST')) {
$db_connection = mysqli_connect(getenv('MYSQL_HOST'),
getenv('MYSQL_USER'), getenv('MYSQL_PASSWORD'),
getenv('MYSQL_DATABASE'),
getenv('MYSQL_PORT'));
}
else {
$db_connection = mysqli_connect($db_params['host'],
$db_params['user'], $db_params['password'],
$db_params['database'],
$db_params['port']);
}
} catch (\Throwable $th) {
echo 'Connection failed<br>';
echo 'Error number: ' . mysqli_connect_errno() . '<br>';
echo 'Error message: ' . mysqli_connect_error() . '<br>';
die();
}
$sql_adapter = new MySQLHandler($db_connection, $SERVER_HOST);
$adapter = new PostHandler($sql_adapter);
require_once 'dergdown.php';
function dergdown_to_html($text) {
$Parsedown = new Dergdown();
return $Parsedown->text($text);
}
function post_to_html($post) {
return dergdown_to_html($post->markdown);
}
$adapter->markdown_engine = "post_to_html";
?>

View file

@ -0,0 +1,19 @@
<?php
use Symfony\Component\Yaml\Yaml;
$SERVER_HOST = $_SERVER['HTTP_HOST'];
if(!preg_match('/^[\w\.\:]+$/', $SERVER_HOST)) {
http_response_code(500);
echo "Not a valid server host (was " . $SERVER_HOST . ")";
die();
}
$SERVER_PREFIX = "https://" . $SERVER_HOST;
$SITE_CONFIG = Yaml::parseFile('../secrets/' . $SERVER_HOST . '.config.yml');
$SITE_CONFIG['uri_prefix'] = $SERVER_PREFIX;
$SITE_CONFIG['HTTP_HOST'] = $SERVER_HOST;
?>

24
www/src/setup_twig.php Normal file
View file

@ -0,0 +1,24 @@
<?php
$loader = new \Twig\Loader\FilesystemLoader(['../templates']);
$twig = new \Twig\Environment($loader,[
'debug' => true,
'cache' => 'twig_cache'
]);
$twig->addExtension(new Twig\Extra\Markdown\MarkdownExtension());
use Twig\Extra\Markdown\DefaultMarkdown;
use Twig\Extra\Markdown\MarkdownRuntime;
use Twig\RuntimeLoader\RuntimeLoaderInterface;
$twig->addRuntimeLoader(new class implements RuntimeLoaderInterface {
public function load($class) {
if (MarkdownRuntime::class === $class) {
return new MarkdownRuntime(new DefaultMarkdown());
}
}
});
?>