Compare commits

...

2 commits

Author SHA1 Message Date
0dcf36052e ... unsure
Some checks reported warnings
/ phplint (push) Has been cancelled
2024-08-24 13:09:28 +02:00
76ca7b9c32 everything(everything): simply saving work 2024-08-15 22:53:55 +02:00
36 changed files with 1451 additions and 642 deletions

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
/vendor/
.docker_vols
sftp.json

0
Rakefile Normal file
View file

View file

@ -2,7 +2,7 @@ FROM composer
WORKDIR /app
COPY www/composer.* .
RUN composer install
COPY www/vendor/* vendor/
FROM php:apache
WORKDIR /var/www/html

View file

@ -24,7 +24,7 @@ services:
- ../.git
- mysql_schema.sql
volumes:
- website_datavolume:/var/www/html/raw
- ../.docker_vols/web:/var/www/html/raw
mysql:
build:
@ -42,7 +42,4 @@ services:
- path: mysql_schema.sql
action: rebuild
volumes:
- sqlvolume:/var/lib/mysql
volumes:
sqlvolume: {}
website_datavolume: {}
- ../.docker_vols/sql:/var/lib/mysql

View file

@ -3,35 +3,59 @@ CREATE DATABASE dragon_fire;
USE dragon_fire;
-- DROP TABLE posts;
-- DROP TABLE path_access_counts;
-- DROP TABLE path_errcodes;
-- DROP TABLE feed_cache;
CREATE TABLE posts (
post_id INTEGER AUTO_INCREMENT,
host VARCHAR(64) NOT NULL,
post_path VARCHAR(255) NOT NULL,
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,
post_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
post_updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
post_access_count INTEGER DEFAULT 0,
post_view_count INTEGER DEFAULT 0,
post_metadata JSON NOT NULL,
post_title VARCHAR(1024),
post_tags VARCHAR(1024),
post_brief TEXT(2048),
post_metadata JSON DEFAULT NULL,
post_settings_cache JSON DEFAULT NULL,
post_content MEDIUMTEXT,
PRIMARY KEY(post_id),
CONSTRAINT unique_post UNIQUE (host, post_path),
INDEX(host, post_path),
INDEX(post_path_depth, post_path),
INDEX(post_create_time),
INDEX(post_update_time)
INDEX(host, post_path_depth, post_path),
INDEX(host, post_created_at),
INDEX(host, post_updated_at),
FULLTEXT(post_tags)
);
CREATE TABLE post_markdown (
post_id INTEGER,
post_markdown TEXT,
PRIMARY KEY(post_id),
FOREIGN KEY(post_id) REFERENCES posts(post_id)
ON DELETE CASCADE,
FULLTEXT(post_markdown)
);
CREATE TABLE path_access_counts (
access_time DATETIME NOT NULL,
host VARCHAR(64) NOT NULL,
post_path VARCHAR(255),
agent VARCHAR(255),
referrer VARCHAR(255),
@ -42,6 +66,17 @@ CREATE TABLE path_access_counts (
PRIMARY KEY(access_time, host, post_path, agent, referrer)
);
CREATE TABLE path_errcodes (
access_timestamp DATETIME NOT NULL,
host VARCHAR(64) NOT NULL,
post_path VARCHAR(255),
agent VARCHAR(255),
referrer VARCHAR(255),
error VARCHAR(1024),
);
CREATE TABLE feed_cache (
host VARCHAR(64) NOT NULL,
search_path VARCHAR(255),

View file

@ -14,21 +14,20 @@ RewriteRule ^.*\.(flv|gif|ico|jpg|jpeg|mp4|mpeg|png|svg|swf|webp)$ raw/%{HTTP_HO
RewriteRule ^/?raw/(.*)$ raw/%{HTTP_HOST}/$1 [L,END]
RewriteEngine On
RewriteCond %{HTTPS} !on
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L,END]
# RewriteCond %{HTTPS} !on
# RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L,END]
RewriteCond %{REQUEST_URI} !^/?(static|raw|robots\.txt).*
RewriteRule (.*) router.php
RewriteCond %{REQUEST_URI} !^/?(src/dbtest\.php|static|raw|robots\.txt).*
RewriteRule (.*) src/router.php
Allow from all
Options +Indexes
<filesMatch ".(flv|gif|ico|jpg|jpeg|mp4|mpeg|png|svg|swf|webp)$">
Header set Cache-Control "max-age=315360, public"
Header set Cache-Control "max-age=60, public"
</filesMatch>
<filesMatch ".(js)$">
Header set Cache-Control "max-age=315360, public"
Header set Cache-Control "max-age=60, public"
</filesMatch>

273
www/composer.lock generated
View file

@ -8,16 +8,16 @@
"packages": [
{
"name": "dflydev/dot-access-data",
"version": "v3.0.2",
"version": "v3.0.3",
"source": {
"type": "git",
"url": "https://github.com/dflydev/dflydev-dot-access-data.git",
"reference": "f41715465d65213d644d3141a6a93081be5d3549"
"reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/f41715465d65213d644d3141a6a93081be5d3549",
"reference": "f41715465d65213d644d3141a6a93081be5d3549",
"url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f",
"reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f",
"shasum": ""
},
"require": {
@ -77,9 +77,9 @@
],
"support": {
"issues": "https://github.com/dflydev/dflydev-dot-access-data/issues",
"source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.2"
"source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3"
},
"time": "2022-10-27T11:44:00+00:00"
"time": "2024-07-08T12:26:09+00:00"
},
{
"name": "erusev/parsedown",
@ -519,16 +519,16 @@
},
{
"name": "league/commonmark",
"version": "2.4.1",
"version": "2.5.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
"reference": "3669d6d5f7a47a93c08ddff335e6d945481a1dd5"
"reference": "ac815920de0eff6de947eac0a6a94e5ed0fb147c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/3669d6d5f7a47a93c08ddff335e6d945481a1dd5",
"reference": "3669d6d5f7a47a93c08ddff335e6d945481a1dd5",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/ac815920de0eff6de947eac0a6a94e5ed0fb147c",
"reference": "ac815920de0eff6de947eac0a6a94e5ed0fb147c",
"shasum": ""
},
"require": {
@ -541,8 +541,8 @@
},
"require-dev": {
"cebe/markdown": "^1.0",
"commonmark/cmark": "0.30.0",
"commonmark/commonmark.js": "0.30.0",
"commonmark/cmark": "0.31.0",
"commonmark/commonmark.js": "0.31.0",
"composer/package-versions-deprecated": "^1.8",
"embed/embed": "^4.4",
"erusev/parsedown": "^1.0",
@ -551,10 +551,10 @@
"michelf/php-markdown": "^1.4 || ^2.0",
"nyholm/psr7": "^1.5",
"phpstan/phpstan": "^1.8.2",
"phpunit/phpunit": "^9.5.21",
"phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0",
"scrutinizer/ocular": "^1.8.1",
"symfony/finder": "^5.3 | ^6.0",
"symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0",
"symfony/finder": "^5.3 | ^6.0 || ^7.0",
"symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 || ^7.0",
"unleashedtech/php-coding-standard": "^3.1.1",
"vimeo/psalm": "^4.24.0 || ^5.0.0"
},
@ -564,7 +564,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "2.5-dev"
"dev-main": "2.6-dev"
}
},
"autoload": {
@ -621,7 +621,7 @@
"type": "tidelift"
}
],
"time": "2023-08-30T16:55:00+00:00"
"time": "2024-07-24T12:52:09+00:00"
},
{
"name": "league/config",
@ -707,31 +707,31 @@
},
{
"name": "nette/schema",
"version": "v1.2.5",
"version": "v1.3.0",
"source": {
"type": "git",
"url": "https://github.com/nette/schema.git",
"reference": "0462f0166e823aad657c9224d0f849ecac1ba10a"
"reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/schema/zipball/0462f0166e823aad657c9224d0f849ecac1ba10a",
"reference": "0462f0166e823aad657c9224d0f849ecac1ba10a",
"url": "https://api.github.com/repos/nette/schema/zipball/a6d3a6d1f545f01ef38e60f375d1cf1f4de98188",
"reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188",
"shasum": ""
},
"require": {
"nette/utils": "^2.5.7 || ^3.1.5 || ^4.0",
"php": "7.1 - 8.3"
"nette/utils": "^4.0",
"php": "8.1 - 8.3"
},
"require-dev": {
"nette/tester": "^2.3 || ^2.4",
"nette/tester": "^2.4",
"phpstan/phpstan-nette": "^1.0",
"tracy/tracy": "^2.7"
"tracy/tracy": "^2.8"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2-dev"
"dev-master": "1.3-dev"
}
},
"autoload": {
@ -763,26 +763,26 @@
],
"support": {
"issues": "https://github.com/nette/schema/issues",
"source": "https://github.com/nette/schema/tree/v1.2.5"
"source": "https://github.com/nette/schema/tree/v1.3.0"
},
"time": "2023-10-05T20:37:59+00:00"
"time": "2023-12-11T11:54:22+00:00"
},
{
"name": "nette/utils",
"version": "v4.0.3",
"version": "v4.0.5",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
"reference": "a9d127dd6a203ce6d255b2e2db49759f7506e015"
"reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/a9d127dd6a203ce6d255b2e2db49759f7506e015",
"reference": "a9d127dd6a203ce6d255b2e2db49759f7506e015",
"url": "https://api.github.com/repos/nette/utils/zipball/736c567e257dbe0fcf6ce81b4d6dbe05c6899f96",
"reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96",
"shasum": ""
},
"require": {
"php": ">=8.0 <8.4"
"php": "8.0 - 8.4"
},
"conflict": {
"nette/finder": "<3",
@ -849,9 +849,9 @@
],
"support": {
"issues": "https://github.com/nette/utils/issues",
"source": "https://github.com/nette/utils/tree/v4.0.3"
"source": "https://github.com/nette/utils/tree/v4.0.5"
},
"time": "2023-10-29T21:02:13+00:00"
"time": "2024-08-07T15:39:19+00:00"
},
{
"name": "psr/event-dispatcher",
@ -983,16 +983,16 @@
},
{
"name": "spatie/yaml-front-matter",
"version": "2.0.8",
"version": "2.0.9",
"source": {
"type": "git",
"url": "https://github.com/spatie/yaml-front-matter.git",
"reference": "f2f1f749a405fafc9d6337067c92c062d51a581c"
"reference": "cbe67e1cdd0a29a96d74ccab9400fe663e078392"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/yaml-front-matter/zipball/f2f1f749a405fafc9d6337067c92c062d51a581c",
"reference": "f2f1f749a405fafc9d6337067c92c062d51a581c",
"url": "https://api.github.com/repos/spatie/yaml-front-matter/zipball/cbe67e1cdd0a29a96d74ccab9400fe663e078392",
"reference": "cbe67e1cdd0a29a96d74ccab9400fe663e078392",
"shasum": ""
},
"require": {
@ -1029,7 +1029,7 @@
"yaml"
],
"support": {
"source": "https://github.com/spatie/yaml-front-matter/tree/2.0.8"
"source": "https://github.com/spatie/yaml-front-matter/tree/2.0.9"
},
"funding": [
{
@ -1041,20 +1041,20 @@
"type": "github"
}
],
"time": "2023-12-04T10:02:52+00:00"
"time": "2024-06-13T10:20:51+00:00"
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.4.0",
"version": "v3.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "7c3aff79d10325257a001fcf92d991f24fc967cf"
"reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf",
"reference": "7c3aff79d10325257a001fcf92d991f24fc967cf",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"shasum": ""
},
"require": {
@ -1063,7 +1063,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.4-dev"
"dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
@ -1092,7 +1092,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0"
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0"
},
"funding": [
{
@ -1108,20 +1108,20 @@
"type": "tidelift"
}
],
"time": "2023-05-23T14:45:45+00:00"
"time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.28.0",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb"
"reference": "0424dff1c58f028c451efff2045f5d92410bd540"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540",
"reference": "0424dff1c58f028c451efff2045f5d92410bd540",
"shasum": ""
},
"require": {
@ -1135,9 +1135,6 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@ -1174,7 +1171,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0"
},
"funding": [
{
@ -1190,20 +1187,20 @@
"type": "tidelift"
}
],
"time": "2023-01-26T09:26:14+00:00"
"time": "2024-05-31T15:07:36+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.28.0",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "42292d99c55abe617799667f454222c54c60e229"
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229",
"reference": "42292d99c55abe617799667f454222c54c60e229",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c",
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c",
"shasum": ""
},
"require": {
@ -1217,9 +1214,6 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@ -1257,7 +1251,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0"
},
"funding": [
{
@ -1273,20 +1267,20 @@
"type": "tidelift"
}
],
"time": "2023-07-28T09:04:16+00:00"
"time": "2024-06-19T12:30:46+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.28.0",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5"
"reference": "77fa7995ac1b21ab60769b7323d600a991a90433"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433",
"reference": "77fa7995ac1b21ab60769b7323d600a991a90433",
"shasum": ""
},
"require": {
@ -1294,9 +1288,6 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@ -1340,7 +1331,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0"
"source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0"
},
"funding": [
{
@ -1356,20 +1347,96 @@
"type": "tidelift"
}
],
"time": "2023-01-26T09:26:14+00:00"
"time": "2024-05-31T15:07:36+00:00"
},
{
"name": "symfony/yaml",
"version": "v7.0.0",
"name": "symfony/polyfill-php81",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "0055b230c408428b9b5cde7c55659555be5c0278"
"url": "https://github.com/symfony/polyfill-php81.git",
"reference": "3fb075789fb91f9ad9af537c4012d523085bd5af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/0055b230c408428b9b5cde7c55659555be5c0278",
"reference": "0055b230c408428b9b5cde7c55659555be5c0278",
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/3fb075789fb91f9ad9af537c4012d523085bd5af",
"reference": "3fb075789fb91f9ad9af537c4012d523085bd5af",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php81\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.30.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-06-19T12:30:46+00:00"
},
{
"name": "symfony/yaml",
"version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "fa34c77015aa6720469db7003567b9f772492bf2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/fa34c77015aa6720469db7003567b9f772492bf2",
"reference": "fa34c77015aa6720469db7003567b9f772492bf2",
"shasum": ""
},
"require": {
@ -1411,7 +1478,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/yaml/tree/v7.0.0"
"source": "https://github.com/symfony/yaml/tree/v7.1.1"
},
"funding": [
{
@ -1427,24 +1494,25 @@
"type": "tidelift"
}
],
"time": "2023-11-07T10:26:03+00:00"
"time": "2024-05-31T14:57:53+00:00"
},
{
"name": "twig/markdown-extra",
"version": "v3.8.0",
"version": "v3.11.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/markdown-extra.git",
"reference": "b6e4954ab60030233df5d293886b5404558daac8"
"reference": "504557d60d80478260ebd2221a2b3332a480865d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/b6e4954ab60030233df5d293886b5404558daac8",
"reference": "b6e4954ab60030233df5d293886b5404558daac8",
"url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/504557d60d80478260ebd2221a2b3332a480865d",
"reference": "504557d60d80478260ebd2221a2b3332a480865d",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/deprecation-contracts": "^2.5|^3",
"twig/twig": "^3.0"
},
"require-dev": {
@ -1456,6 +1524,9 @@
},
"type": "library",
"autoload": {
"files": [
"Resources/functions.php"
],
"psr-4": {
"Twig\\Extra\\Markdown\\": ""
},
@ -1483,7 +1554,7 @@
"twig"
],
"support": {
"source": "https://github.com/twigphp/markdown-extra/tree/v3.8.0"
"source": "https://github.com/twigphp/markdown-extra/tree/v3.11.0"
},
"funding": [
{
@ -1495,34 +1566,42 @@
"type": "tidelift"
}
],
"time": "2023-11-21T14:02:01+00:00"
"time": "2024-08-07T17:34:09+00:00"
},
{
"name": "twig/twig",
"version": "v3.8.0",
"version": "v3.11.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d"
"reference": "e80fb8ebba85c7341a97a9ebf825d7fd4b77708d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/9d15f0ac07f44dc4217883ec6ae02fd555c6f71d",
"reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/e80fb8ebba85c7341a97a9ebf825d7fd4b77708d",
"reference": "e80fb8ebba85c7341a97a9ebf825d7fd4b77708d",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "^1.3",
"symfony/polyfill-php80": "^1.22"
"symfony/polyfill-php80": "^1.22",
"symfony/polyfill-php81": "^1.29"
},
"require-dev": {
"psr/container": "^1.0|^2.0",
"symfony/phpunit-bridge": "^5.4.9|^6.3|^7.0"
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
},
"type": "library",
"autoload": {
"files": [
"src/Resources/core.php",
"src/Resources/debug.php",
"src/Resources/escaper.php",
"src/Resources/string_loader.php"
],
"psr-4": {
"Twig\\": "src/"
}
@ -1555,7 +1634,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.8.0"
"source": "https://github.com/twigphp/Twig/tree/v3.11.0"
},
"funding": [
{
@ -1567,7 +1646,7 @@
"type": "tidelift"
}
],
"time": "2023-11-21T18:54:41+00:00"
"time": "2024-08-08T16:15:16+00:00"
}
],
"packages-dev": [],

View file

@ -1,200 +0,0 @@
<?php
class PostData {
public static $default_banners = [];
public static $markdown_engine = null;
public $data;
public $html_data;
public $raw_data;
public static function _generate_404($sql_row) {
$sql_row ??= [
'post_id' => -1,
'post_path' => '/404.md',
'post_title' => '404 Page',
'post_metadata' => [
'type' => '404'
]
];
}
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, $sql_row) {
$this->handler = $post_handler;
$this->content_html = null;
$this->content_markdown = null;
$sql_meta = null;
if(!isset($sql_row) or !$sql_row['found']) {
$sql_row = $this->_generate_404($sql_row);
$sql_meta = $sql_row['post_metadata'];
}
$sql_meta = $sql_row['post_metadata'];
if(is_string($sql_meta)) {
$sql_meta = json_decode($sql_meta, true);
}
unset($sql_meta['settings']);
$this->sql_row = $sql_row;
$this->sql_meta = $sql_meta ?? [];
$sql_settings = json_decode($sql_row['post_settings_cache'], true)
?? $this->handler->get_settings_for_path($sql_row['post_path']);
$data = [
'id' => $sql_row['post_id'],
'path' => $sql_row['post_path'],
'url' => 'https://' . $sql_row['host'] . $sql_row['post_path'],
'created_at' => $sql_row['post_create_time'] ?? '',
'updated_at' => $sql_row['post_update_time'] ?? '',
'view_count' => $sql_row['post_access_count'] ?? 0
];
$data['title'] = $sql_meta['title'] ?? $sql_row['title'];
unset($sql_meta['title']);
$data['tags'] = $sql_meta['tags'] ?? [];
unset($sql_meta['tags']);
$data['type'] = $sql_meta['type']
?? self::_deduce_type($sql_row['post_path']);
unset($sql_meta['type']);
$data['icon'] = $sql_meta['icon']
?? self::_deduce_icon($data['type']);
unset($sql_meta['icon']);
if(isset($sql_meta['media_url'])) {
$data['media_url'] = $sql_meta['media_url'];
$data['thumb_url'] = $sql_meta['thumb_url'] ?? $data['media_url'];
unset($sql_meta['media_url']);
unset($sql_meta['thumb_url']);
}
$data['banners'] = $sql_meta['banners']
?? $sql_settings['banners']
?? self::$default_banners;
unset($sql_meta['banners']);
unset($sql_settings['banners']);
$data['preview_image'] = $sql_meta['preview_image'] ?? $data['banners'][0]['src'] ?? null;
unset($sql_meta['preview_image']);
$data['brief'] = $sql_meta['brief']
?? $data['title'];
unset($sql_meta['brief']);
$data['excerpt'] = $sql_meta['excerpt']
?? substr($sql_row['post_content'], 0, 256);
$data['metadata'] = $sql_meta;
$data['settings'] = $sql_settings;
$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();
}
return $this->data[$name];
}
public function get_html() {
$fn = self::$markdown_engine;
$this->content_html ??= $fn($this);
return $this->content_html;
}
public function get_markdown() {
$this->content_markdown ??=
$this->handler->get_markdown_for_id($this->data['id']);
return $this->content_markdown;
}
public function to_json($with_markdown = false, $with_html = false) {
$out_data = $this->data;
if($with_markdown) {
$out_data['markdown'] = $this->get_markdown();
}
if($with_html) {
$out_data['html'] = $this->get_html();
}
return json_encode($out_data);
}
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;
}
public function get_child_posts() {
if(isset($this->child_posts))
return $this->child_posts;
$child_data = $this->handler->get_subposts_by_path($this->data['path']);
$this->child_posts = array_map(function($data) {
return new PostData($this->handler, $data);
}, $child_data);
}
}
?>

View file

@ -1,221 +0,0 @@
<?php
require_once 'mysql_adapter.php';
use Spatie\YamlFrontMatter\YamlFrontMatter;
use Laminas\Feed\Writer\Feed;
class PostHandler extends MySQLAdapter {
public $data_directory;
function __construct($SITE_CONFIG) {
parent::__construct($SITE_CONFIG);
$this->data_directory = 'raw/' . $this->SITE_CONFIG['HTTP_HOST'];
}
function deduce_post_type($post_path) {
$ext = pathinfo($post_path, PATHINFO_EXTENSION);
if(preg_match("/\.(\w+)\.md$/", $post_path, $ext_match)) {
$ext = $ext_match[1];
}
$ext_mapping = [
'' => 'directory',
'md' => 'text/markdown',
'png' => 'image',
'jpg' => 'image',
'jpeg' => 'image'
];
return $ext_mapping[$ext] ?? '?';
}
function fill_in_post_meta($post_path, $meta) {
$icon_mapping = [
'' => 'question',
'text/markdown' => 'markdown',
'blog' => 'markdown',
'directory' => 'folder',
'gallery' => 'images',
'blog_list' => 'rectangle-list',
'image' => 'image'
];
$meta["title"] ??= basename($post_path);
if($meta["title"] == "") {
$meta["title"] = "root";
}
if(!isset($meta['media_file']) and preg_match("/\.(\w+)\.md$/", $post_path)) {
$meta['media_file'] = "https://" . $this->SITE_CONFIG['HTTP_HOST'] . chop($post_path, ".md");
}
$meta['tags'] ??= [];
$meta['type'] ??= $this->deduce_post_type($post_path);
$meta['icon'] ??= $icon_mapping[$meta['type']] ?? 'question';
return $meta;
}
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_data['post_metadata'] = $this->fill_in_post_meta(
$post_data['post_path'],
$post_meta);
$post_data["post_file_dir"] = '/raw' . $post_data["post_path"];
return $post_data;
}
function make_post_directory($directory) {
$data_directory = $this->data_directory . $directory;
is_dir($data_directory) || mkdir($data_directory, 0777, true);
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) {
move_uploaded_file($file_path, $this->data_directory . $post_path);
}
function save_markdown_post($post_path, $post_data) {
if(basename($post_path) == "README.md") {
$post_path = dirname($post_path);
}
$frontmatter_post = YamlFrontMatter::parse($post_data);
$post_path = $this->_sanitize_path($post_path);
$post_content = $frontmatter_post->body();
$post_metadata = $frontmatter_post->matter();
$post_metadata = $this->fill_in_post_meta(
$post_path,
$post_metadata);
$post_metadata['tags'][]= 'type:' . $post_metadata['type'];
$this->update_or_create_post($post_path, $post_metadata, $post_content);
}
function handle_upload($post_path, $file_path) {
$post_path = $this->_sanitize_path($post_path);
$ext = pathinfo($post_path, PATHINFO_EXTENSION);
switch($ext) {
case "md":
$this->save_markdown_post($post_path, file_get_contents($file_path));
$this->make_post_directory(dirname($post_path));
move_uploaded_file($file_path, $this->data_directory . $post_path);
break;
default:
$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 host=? AND search_path=? AND export_type=?",
"sss", $this->SITE_CONFIG['HTTP_HOST'], $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($this->SITE_CONFIG['uri_prefix'] . $path);
$feed->setFeedLink($this->SITE_CONFIG['uri_prefix'] . "/feeds/atom" . $path, "atom");
$feed->setDateModified(time());
$feed->setDescription("DergenFeed for all your " . $path . " needs");
$feed_posts = $this->_exec("SELECT
post_path,
post_create_time, post_update_time,
post_content,
post_metadata
FROM posts
WHERE (host = ?) AND ((post_path = ?) OR (post_path LIKE ?))
ORDER BY post_create_time DESC LIMIT 200",
"sss", $this->SITE_CONFIG['HTTP_HOST'], $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($this->SITE_CONFIG['uri_prefix'] . $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
(host, search_path, export_type, feed_content)
VALUES
(?, ?, 'atom', ?),
(?, ?, 'rss', ?)",
"ssssss",
$this->SITE_CONFIG['HTTP_HOST'], $path, $feed->export('atom'),
$this->SITE_CONFIG['HTTP_HOST'], $path, $feed->export('rss'));
return $this->try_get_cached_feed($path, $export_opt);
}
}
?>

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'];
}
}
?>

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

@ -0,0 +1,213 @@
<?php
class Post implements ArrayAccess {
public $handler;
private $content_html;
private $content_markdown;
private $site_defaults;
public $data;
public $html_data;
public $raw_data;
private $_child_posts;
private $_parent_post;
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, $site_defaults) {
$this->handler = $post_handler;
$this->content_html = null;
$this->content_markdown = null;
$this->site_defaults = $site_defaults;
if(!isset($post_data) or !isset($post_data['id'])) {
$post_data = $this->_generate_404($post_data);
}
$data = $post_data;
if($data['path'] == '') {
$data['path'] = '/';
$data['title'] ??= 'root';
$data['basename'] ??= 'root';
}
$post_data['host'] ??= 'localhost:8081';
$data['url'] ??= 'http://' . $post_data['host'] . $post_data['path'];
$data['basename'] ??= basename($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($name == 'parent') {
return $this->get_parent_post();
}
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 {
$this->data[$offset] = $value;
}
public function offsetUnset($offset) : void {
unset($this->data[$offset]);
}
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) {
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 ??= $this->handler->get_post($parent_path);
return $this->_parent_post;
}
}
?>

View file

@ -0,0 +1,61 @@
<?php
require_once 'db_interface.php';
require_once 'db_handler/post.php';
class PostHandler {
private $db;
private $posts;
public $markdown_engine;
public $site_defaults;
function __construct($db_adapter) {
$this->db = $db_adapter;
$this->posts = [];
$this->site_defaults = null;
$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->site_defaults);
}
$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, $this->site_defaults));
}
return $out_list;
}
}
?>

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

@ -0,0 +1,191 @@
<?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...',
'title' => "A Markdown Test"
]);
$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!
> Just checking in...
'
);
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";
?>

View file

@ -4,8 +4,8 @@ $data_time_start = microtime(true);
require_once 'vendor/autoload.php';
require_once 'post_adapter.php';
require_once 'db_handler/mysql_handler.php';
require_once 'db_handler/post_handler.php';
require_once 'post.php';
require_once 'fontawesome.php';
@ -24,7 +24,30 @@ $SITE_CONFIG = Yaml::parseFile('secrets/' . $SERVER_HOST . '.config.yml');
$SITE_CONFIG['uri_prefix'] = $SERVER_PREFIX;
$SITE_CONFIG['HTTP_HOST'] = $SERVER_HOST;
$adapter = new PostHandler($SITE_CONFIG);
$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']);

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

@ -0,0 +1,31 @@
<?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';
$REQUEST_URI = parse_url($_SERVER['REQUEST_URI']);
$REQUEST_PATH = $REQUEST_URI['path'];
parse_str($REQUEST_URI['query'] ?? '', $REQUEST_QUERY);
if(preg_match('/^\/api/', $REQUEST_PATH)) {
require_once 'serve/api.php';
}
elseif(preg_match('/^\/ajax/', $REQUEST_PATH)) {
require_once 'serve/ajax.php';
}
else {
require_once 'serve/post.php';
}
?>

26
www/src/serve/ajax.php Normal file
View file

@ -0,0 +1,26 @@
<?php
$match = null;
preg_match('/^\/ajax\/(.*)$/', $REQUEST_PATH, $match);
if(!isset($match)) {
die();
}
$AJAX_REQUEST_TEMPLATE = $match[1];
$ajax_args = [
];
if(isset($REQUEST_QUERY['page'])) {
$ajax_args['page'] = $adapter->get_post($REQUEST_QUERY['page']);
}
$ajax_args['fa'] = $FONT_AWESOME_ARRAY;
$ajax_args['page'] ??= $SITE_CONFIG['site_defaults'];
echo $twig->render('/ajax/' . $AJAX_REQUEST_TEMPLATE, $ajax_args);
?>

48
www/src/serve/post.php Normal file
View file

@ -0,0 +1,48 @@
<?php
require_once 'fontawesome.php';
$post = $adapter->get_post($REQUEST_PATH);
function render_root_template($template, $args = []) {
global $twig;
global $FONT_AWESOME_ARRAY;
global $SITE_CONFIG;
$args['fa'] = $FONT_AWESOME_ARRAY;
$args['page'] ??= $SITE_CONFIG['site_defaults'];
$page = $args['page'];
$page['base'] ??= $page['url'];
$args['opengraph'] = [
"site_name" => $page['site_name'] ?? 'Nameless Site',
"title" => $page['title'] ?? 'Titleless',
"url" => $page['url'] ?? $page['path'] ?? 'No URL set',
"description" => $page['description'] ?? 'No description set'
];
$args['banners'] = json_encode($page['banners'] ?? []);
$args['age_gate'] = (!isset($_COOKIE['AgeConfirmed']))
&& isset($SITE_CONFIG['age_gate']);
echo $twig->render($template, $args);
}
function render_pathed_content_template($template, $args = []) {
render_root_template($template, $args);
}
render_pathed_content_template('post_types/markdown.html', [
'page' => $post
]);
die();
if(!isset($post)) {
render_404();
die();
}
?>

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

@ -0,0 +1,45 @@
<?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";
$adapter->site_defaults = $SITE_CONFIG['site_defaults'];
?>

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

View file

@ -311,12 +311,21 @@ body.htmx-request::before {
.folder-listing input {
display: none;
}
.folder-listing input + ul {
.folder-listing input ~ ul {
display: none;
}
.folder-listing input:checked + ul {
.folder-listing input:checked ~ ul {
display: block;
}
.folder-listing label > :nth-child(2) {
display: none;
}
.folder-listing input:checked ~ label > :nth-child(1) {
display: none;
}
.folder-listing input:checked ~ label > :nth-child(2) {
display: inline-block;
}
#navbar-expand-label {
cursor: pointer;

View file

@ -1,13 +0,0 @@
<li class="folder-listing">
<span hx-boost="false"
hx-get="/ajax/open_folder_listing{{ post.post_path }}"
hx-target="closest li"
hx-swap="outerHTML">
{{ fa[post.post_metadata.icon] | raw }}
</span>
<a href={{post.post_path}}>
{{ post.post_metadata.title }}
</a>
</li>

View file

@ -0,0 +1,27 @@
<li class="folder-listing">
{% set folder_key = random() %}
<input type="checkbox" id="folder-open-{{folder_key}}" name="folder-open-{{folder_key}}">
<label for="folder-open-{{folder_key}}"
hx-trigger="click once queue:last, mouseenter once queue:all, intersect once queue:all"
hx-get="/ajax/compact_filelist/listing.html?page={{ post.path }}"
hx-target="next ul">
{{ fa['folder'] | raw }}
{{ fa['folder-open'] | raw }}
</label>
<a href={{post.path}}>
{{ post.basename }} - {{ post.title }}
</a>
<ul class="folder-listing">
<li>
<span>
Loading...
</span>
</li>
</ul>
</li>

View file

@ -0,0 +1,4 @@
{% for post in page.child_posts %}
{{ include('ajax/compact_filelist/entry.html') }}
{% endfor %}

View file

@ -1,4 +0,0 @@
{% for post in subposts %}
{{ include('ajax/folder_listing_entry.html') }}
{% endfor %}

View file

@ -1,26 +0,0 @@
<li class="folder-listing">
{% set folder_key = random() %}
<label for="folder-open-{{folder_key}}"
hx-trigger="click once queue:all"
hx-get="/ajax/folder_listing{{post.post_path}}"
hx-target="next ul">
{{ fa[post.post_metadata.icon] | raw }}
</label>
<a href={{post.post_path}}>
{{ post.post_metadata.title }}
</a>
<input type="checkbox" id="folder-open-{{folder_key}}">
<ul class="folder-listing">
<li>
<span>
Loading...
</span>
</li>
</ul>
</li>

View file

@ -1,20 +0,0 @@
<li class="folder-listing">
<span hx-boost="false"
hx-get="/ajax/closed_folder_listing{{ post.post_path }}"
hx-target="closest li"
hx-swap="outerHTML">
{{ fa[post.post_metadata.icon] | raw }}
</span>
<a href={{post.post_path}}>
{{ post.post_metadata.title }}
</a>
<ul class="folder-listing">
{% for post in subposts %}
{{ include('ajax/closed_folder_listing.html') }}
{% endfor %}
</ul>
</li>

View file

@ -1,11 +1,11 @@
<div id="post_file_bar">
<menu id="post_file_titles">
<li>
{{ post.post_metadata.title }}
{{ page.basename }}
</li>
</menu>
<menu id="post_file_path">
{% set split_post = post.post_path |split('/') %}
{% set split_post = page.path |split('/') %}
{% for i in range(0, split_post|length - 1) %}
<li>
{% if i != 0 %}
@ -22,7 +22,7 @@
<li style="margin-left: auto;">
<label for="navbar-expander" id="navbar-expand-label"> {{ fa['bars'] | raw }} </label>
<a rel="alternate" type="application/rss+xml" target="_blank"
style="padding-left: 0.3rem;" href="/feed/rss{{post.post_path}}">
style="padding-left: 0.3rem;" href="/feed/rss{{page.path}}">
{{ fa['rss']|raw }}
</a>
</li>
@ -32,7 +32,7 @@
<div class="navbar-expand">
Full path:
<ul class="navbar-full-path">
{% set split_post = post.post_path |split('/') %}
{% set split_post = page.path |split('/') %}
{% for i in range(0, split_post|length - 1) %}
<li>
{% if i != 0 %}
@ -47,8 +47,8 @@
</ul>
Dev links:
<a hx-boost="false" href="/raw{{post.post_path}}">raw</a>
<a hx-boost="false" href="/api/posts{{post.post_path}}">api</a>
<a hx-boost="false" href="/raw{{page.path}}">raw</a>
<a hx-boost="false" href="/api/posts{{page.path}}">api</a>
<h4>Folder browser: </h4>
@ -58,12 +58,10 @@
{{ fa['turn-up'] | raw}}
</span>
<a href={{post.parent_path}}>..</a>
<a href={{page.parent.path}}>..</a>
</li>
{% for post in subposts %}
{{ include('ajax/folder_listing_entry.html') }}
{% endfor %}
{{ include('ajax/compact_filelist/listing.html') }}
</ul>
</div>
</div>

View file

@ -6,11 +6,11 @@
{{ parent() }}
<link rel="alternate" type="application/atom+xml" title="{{og.site_name}} Feed for {{post.post_path}}" href="{{site_config.uri_prefix}}/feed/atom{{post.post_path}}">
<link rel="alternate" type="application/atom+xml" title="{{opengraph.site_name}} Feed for {{page.path}}" href="{{page.uri_prefix}}/feed/atom{{page.path}}">
{% endblock %}
{% block second_title %}
<h2> {{ post.post_metadata.title }} </h2>
<h2> {{ page.title }} </h2>
{% endblock %}
{%block main_content%}
@ -22,7 +22,7 @@
{%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~
This page was created on {{ page.created_at }}, last edited {{ page.updated_at }}, and was viewed {{ page.view_count }} times~
</span>
</article>

View file

@ -9,5 +9,9 @@
{%endblock %}
{%block content_article%}
{{ content_html|raw }}
{% if page.title %}
<h1 class="content-title"> {{ page.title }} </h1>
{% endif %}
{{ page.html|raw }}
{% endblock %}

View file

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>{{og.site_name}} - {{og.title}}</title>
<title>{{opengraph.site_name}} - {{opengraph.title}}</title>
<link rel="stylesheet" href="/static/dergstyle.css">
@ -12,7 +12,6 @@
<link rel="stylesheet" href="/static/article_blop.css">
<link rel="stylesheet" href="/static/gallerystyle.css">
<link rel="icon" type="image/x-icon" href="/static/icon.jpeg">
<meta name="viewport" content="width=device-width,initial-scale=1">
@ -20,27 +19,31 @@
<script src="/static/htmx.min.js"></script>
<script id="main_banner_script" src="/static/banner.js"></script>
{% if page.base %}
<base href="{{ page.base }}">
{% endif %}
{% block feed_links %}
<link rel="alternate" type="application/rss+xml" title="{{og.site_name}} Global Feed" href="{{site_config.uri_prefix}}/feed">
<link rel="alternate" type="application/rss+xml" title="{{opengraph.site_name}} Global Feed" href="{{site_config.uri_prefix}}/feed">
{% endblock %}
{% block extra_head %}{% endblock %}
{% block opengraph_tags %}
<meta property="og:site_name" content="{{og.site_name}}">
<meta property="og:site_name" content="{{opengraph.site_name}}">
<meta property="og:url" content="{{og.url}}" />
<meta property="og:url" content="{{opengraph.url}}" />
<meta property="og:title" content="{{ og.title|e }}" />
<meta name="twitter:title" content="{{ og.title|e }}" />
<meta property="og:title" content="{{ opengraph.title|e }}" />
<meta name="twitter:title" content="{{ opengraph.title|e }}" />
<meta property="og:type" content="{{og.type}}" />
<meta property="og:type" content="{{opengraph.type}}" />
<meta property="og:description" content="{{ og.description|e }}" />
<meta name="twitter:description" content="{{ og.description|e }}" />
<meta property="og:description" content="{{ opengraph.description|e }}" />
<meta name="twitter:description" content="{{ opengraph.description|e }}" />
<meta property="og:image" content="{{og.image}}" />
<meta name="twitter:image" content="{{og.image}}" />
<meta property="og:image" content="{{opengraph.image}}" />
<meta name="twitter:image" content="{{opengraph.image}}" />
<meta name="twitter:card" content="summary_large_image">
<meta name="robots" content="max-image-preview:large">
@ -71,25 +74,29 @@
<a id="main_banner_img_link" href="/gallery" hx-preserve> full picture</a>
<script type="text/javascript">
window.dergBannerOptions = JSON.parse('{{banner|raw}}');
window.dergBannerOptions = JSON.parse('{{banners|raw}}');
startBanner();
</script>
<h1 id="big_title">{% block big_title %}{{og.site_name}}{%endblock%}</h1>
<h1 id="big_title">{% block big_title %}{{opengraph.site_name}}{%endblock%}</h1>
{% block second_title %}{% endblock %}
<div id="title_separator"></div>
<menu id="nav_bar">
<li><a href="/about"> About </a></li>
<li><a href="/blog"> Blog </a></li>
<li><a href="/projects"> Projects </a></li>
<li><a href="/artwork"> Artworks </a></li>
{% for link in page.navbar_links %}
<li><a href="{{link.href}}"> {{link.text}} </a></li>
{% endfor %}
</menu>
</header>
<main id="main_content_wrapper">
{% block main_content %}<h3>Soon there shall be content!</h3>{% endblock %}
{% block main_content %}
<h3>This here should have been replaced by content.
</h3>
If you can see this, complain to your nearest dragon.
{% endblock %}
</main>
<footer id="main_footer">