From f217c8a446ca6ac5bbbf744609a6cae7200297f4 Mon Sep 17 00:00:00 2001 From: fristi Date: Sat, 10 Jun 2023 13:44:50 +0200 Subject: [PATCH] Updated website to now use PHP. All content has been divided into pages, articles are now generated from markdown, index page will contain a list of the latest news articles. --- .gitignore | 6 +- 401.html | 27 -- 404.html | 27 -- 410.html | 27 -- 500.html | 31 -- 502.html | 31 -- README.md | 26 +- cmd.php | 3 + composer.json | 23 ++ config/application.php | 27 ++ index.html | 290 ------------------ libs/zap/Application.php | 106 +++++++ libs/zap/Config/Repository.php | 66 ++++ libs/zap/ErrorHandling/Handler.php | 31 ++ libs/zap/Http/Response.php | 235 ++++++++++++++ libs/zap/Routing/Repository.php | 82 +++++ libs/zap/Routing/Route.php | 190 ++++++++++++ libs/zap/Routing/Router.php | 230 ++++++++++++++ pages/faq.php | 34 -- public_html/.htaccess | 4 + public_html/assets/border.png | Bin 0 -> 396 bytes public_html/assets/border2.png | Bin 0 -> 545 bytes public_html/{ => assets/css}/style.css | 21 +- .../assets/{ => fonts}/yoster-web.woff | Bin .../assets/{ => fonts}/yoster-web.woff2 | Bin public_html/assets/js/app.js | 66 ++++ public_html/assets/js/gogo.js | 91 ++++++ public_html/index.php | 220 +------------ robots.txt => public_html/robots.txt | 0 .../articles/2022/introducing_funkwhale.md | 30 ++ ...ng_out_gitea_and_introducing_fossil_scm.md | 39 +++ .../2023/frontpage_updates_and_more.md | 41 +++ resources/articles/2023/plans_for_the_year.md | 42 +++ resources/views/_layout/layout.php | 49 +++ resources/views/errors/404.html | 25 ++ resources/views/pages/copyright.php | 25 ++ resources/views/pages/faq.php | 25 ++ resources/views/pages/index.php | 30 ++ resources/views/pages/news.php | 0 resources/views/pages/services.php | 34 ++ routes/routes.php | 15 + src/Controllers/FrontpageController.php | 106 +++++++ src/Models/Article.php | 259 ++++++++++++++++ style_errors.css | 50 --- 44 files changed, 1925 insertions(+), 739 deletions(-) delete mode 100644 401.html delete mode 100644 404.html delete mode 100644 410.html delete mode 100644 500.html delete mode 100644 502.html create mode 100644 cmd.php create mode 100644 composer.json create mode 100644 config/application.php delete mode 100644 index.html create mode 100644 libs/zap/Application.php create mode 100644 libs/zap/Config/Repository.php create mode 100644 libs/zap/ErrorHandling/Handler.php create mode 100644 libs/zap/Http/Response.php create mode 100644 libs/zap/Routing/Repository.php create mode 100644 libs/zap/Routing/Route.php create mode 100644 libs/zap/Routing/Router.php delete mode 100644 pages/faq.php create mode 100644 public_html/.htaccess create mode 100644 public_html/assets/border.png create mode 100644 public_html/assets/border2.png rename public_html/{ => assets/css}/style.css (89%) rename public_html/assets/{ => fonts}/yoster-web.woff (100%) rename public_html/assets/{ => fonts}/yoster-web.woff2 (100%) create mode 100644 public_html/assets/js/app.js create mode 100644 public_html/assets/js/gogo.js rename robots.txt => public_html/robots.txt (100%) create mode 100644 resources/articles/2022/introducing_funkwhale.md create mode 100644 resources/articles/2022/phasing_out_gitea_and_introducing_fossil_scm.md create mode 100644 resources/articles/2023/frontpage_updates_and_more.md create mode 100644 resources/articles/2023/plans_for_the_year.md create mode 100644 resources/views/_layout/layout.php create mode 100644 resources/views/errors/404.html create mode 100644 resources/views/pages/copyright.php create mode 100644 resources/views/pages/faq.php create mode 100644 resources/views/pages/index.php create mode 100644 resources/views/pages/news.php create mode 100644 resources/views/pages/services.php create mode 100644 routes/routes.php create mode 100644 src/Controllers/FrontpageController.php create mode 100644 src/Models/Article.php delete mode 100644 style_errors.css diff --git a/.gitignore b/.gitignore index f6f4e11..e08d18f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ deploy.sh -deploy-excludes \ No newline at end of file +deploy-excludes +composer.lock +.idea +vendor +cache/* \ No newline at end of file diff --git a/401.html b/401.html deleted file mode 100644 index 90e6c45..0000000 --- a/401.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - Subcon Town - 401 - - - - - - - - -
- -
- logo -
-
-

You are not authorized to access that page.

-
- - - - \ No newline at end of file diff --git a/404.html b/404.html deleted file mode 100644 index d1823fd..0000000 --- a/404.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - Subcon Town - 404 - - - - - - - - -
- -
- logo -
-
-

That page does not exist.

-
- - - - \ No newline at end of file diff --git a/410.html b/410.html deleted file mode 100644 index 7948b98..0000000 --- a/410.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - Subcon Town - 410 - - - - - - - - -
- -
- logo -
-
-

That page is gone. Forever.

-
- - - - \ No newline at end of file diff --git a/500.html b/500.html deleted file mode 100644 index 848edea..0000000 --- a/500.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - Subcon Town - 500 - - - - - - - - -
- -
- logo -
-
-

An error occurred on the server.

-

- The page you requested could not be served due to an internal server error. Please try again - later. -

-
- - - - \ No newline at end of file diff --git a/502.html b/502.html deleted file mode 100644 index 2a5710d..0000000 --- a/502.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - Subcon Town - 502 - - - - - - - - -
- -
- logo -
-
-

Bad Gateway.

-

- The page you requested could not be served due to an internal communication error. Please - try again later. -

-
- - - - \ No newline at end of file diff --git a/README.md b/README.md index 9b046d1..0778445 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,25 @@ -# subcon.town +# Subcon Town Frontpage -The subcon.town frontpage. \ No newline at end of file +This project contains the complete code and assets used by the Subcon Town frontpage. + +## Licensing + +All code is licensed under the BOML-1.0.0 license (Blue Oak Model License). +See the LICENSE file for more details. + +The "Subcon Town" branding and its associated assets (located in public_html/assets/subcon) +are licensed under the Creative Commons BY-NC-ND license. You may share and redistribute +these works, but please do not edit them or use them for commercial purposes. For more +details, read the LICENSE file in public_html/assets/subcon. + +### Third party works + +The fonts and background image used on subcon.town are not owned by Subcon Town. Subcon +Town does not claim any rights to these original works unless otherwise specified by their +respective licensing. + +### Complains + +If you feel any material is incorrectly used in this project, or otherwise infringes on +your rights, please file a complaint with fristi@subcon.town. Please allow up to 48 hours +for a response. \ No newline at end of file diff --git a/cmd.php b/cmd.php new file mode 100644 index 0000000..13b7be5 --- /dev/null +++ b/cmd.php @@ -0,0 +1,3 @@ + [ + 'name' => 'Subcon Town Frontpage', + 'url' => 'http://localhost', + 'debug' => false + ], + + 'encryption' => [ + 'key' => '' + ], + + 'frontpage' => [ + 'articles' => [ + '2023/frontpage_updates_and_more.md', + '2023/plans_for_the_year.md' + ] + ] +]; diff --git a/index.html b/index.html deleted file mode 100644 index 9ec2e0b..0000000 --- a/index.html +++ /dev/null @@ -1,290 +0,0 @@ - - - - - Subcon.town - Happy Dreams! - - - - - - - - - -
-
- Subcon Town - -
- -
-
-
-

Hello world!

-

- Welcome to subcon.town! Subcon town is a small private "pubnix" server, - hosted for friends and family. Subcon offers ssh access, webpage hosting and various other cloud services, - such as nextcloud and git hosting. -

-
-
-
-

News

-

Januari 2023: Plans for the year

-

- Happy new year! Subcon has come a long way since the change of the year. First off: we have moved to - new hardware! The server is now powered by an Odroid H3 board. With that, a number of other changes - have been made as well. -

-

- Storage on the server has been totally reworked; the old storage media have been replaced with a 2TB - NVMe drive that will, for one, house the user home directories, and a 4TB SATA SSD that will house all - variable data; user repositories, websites, databases, the lot. All of this has been set up with - convenient LVM partitions that can be easily resized if more space is required in the future. -

-

- Many services have been updated. The gitea server has been replaced with a new gogs server, now - hosted at factory.subcon.town. The old snikket server - has also been replaced with a new prosody setup, offering most of the same xmpp services at jab.subcon.town. - Note that old snikket accounts are thus gone. The fedora installation has also been updated to fedora - 37. -

-

- The most notable change however will be the decrease in processing power. While we still have about - 75% of the processing power, the upside is that the system as a whole is seriously more power efficient. - We are now running at a fraction of the power the old server needed, going from 60Wh on average to - a mere 6Wh. Serious power savings like that will lower upkeep cost, which I can in turn invest in - improving other things, such as my god awful internet bandwidth. -

-

- Remember that even with all these changes done, the server is, and will likely forever, be a work - in progress. There are still some rough edges needing work, further configuring of services, and - for the current while, a lot of work writing wiki pages at - library.subcon.town. -

- -

November 2022: Introducing Funkwhale

-

- After some fiddling around on the server, I have managed to finally get a Funkwhale instance - set up! Funkwhale is a piece of software that will let us host our music libraries. You can listen - to your and others' music from the web interface, or use any compatible app on your phone or tablet. - Besides that, Funkwhale federates, much like our Akkoma instance. You will be able to listen to the - libraries of people on other instances as well! -

-

- On a technical note, getting Funkwhale to work took a little bit of doing; it has been manually - set up according to the installation instructions for Arch Linux, adapted here and there to suit - Fedora. One remaining irk because of this, is that Funkwhale was not made with SELinux in mind. - One remaining problem that you might run into is that freshly uploaded music may not play initially; - this is caused by incorrect SELinux permissions. I have currently added a cronjob that will check - these permissions and fix them every few minutes. If your music does not play, please wait a few - minutes and try again. -

-

- For the time being, users will be limited to 5GB libraries. If needed, you may request to have this - limit changed. Please do check before uploading if your music has already been uploaded by others; - preventing duplicate uploads will help conserve space on the server. -

-

- Happy listening! -

- -

November 2022: Phasing out Gitea and introducing Fossil SCM

-

- Recently it has come to my attention that the owners of Gitea have been making some changes - in their project management, mainly in the form of providing support to businesses. For more - information, view - the Gitea blog. -

-

- Unfortunately, with the way things have gone, the community seems to be weary and distrustful of a - number of moves made by the current owners. My personal concerns are about one of the owners using - Gitea is a lucrative money-making scheme. To me, that's a deal breaker, as I don't know if I can - still trust the project after this. Unfortunate, since I know Gitea is supposed to be a community-run - effort. -

-

- As such, I have decided to deprecate the Gitea service on Subcon Town. It will no longer be updated, - but will be kept alive for a while until all projects have been migrated. I am additionally looking - into migrating the data to a Gogs installation, continuing a git hosting service that way. Meanwhile, - I have opted to set up Fossil as an alternative. Fossil SCM is a slightly different VCS, similar to git, - but more geared towards tightly nit development groups that work closer together. -

-

- While fossil may feel a bit more old-fashioned, I think it's also a bit more of a fit with Subcon's - pubnix feel. Users will be able to set up and manage their fossil repositories from their home - directories, in the new "public_fossil" directory. To create new repositories, you can use the - "mkfossil <filename>" command. This is a wrapper around "fossil init" that helps set the - correct file permissions. If done right, your new repo will pop up at museum.subcon.town - from where you'll be able to administrate it. -

-

- With the introduction of new commands and functions for users, I plan on writing a wiki article - that lists all available functions and how to use them. This should help new users settle in. - Note that the README files in your home directories will be kept updated as well. -

-

- Hope to see you soon on Subcon Town! -

- -

October 2022: Finalizing stuff

-

- It has been quite a while, but slowly the server has been getting ready to start functioning - as an actual pubnix community. The website has received a little touch-up, the copyright notice - has been updated, and a lot of work has been done under the hood. -

-

- The initial plan once everything is ready, is to allow a few people on the server for testing: - this is to iron out some issues before letting any more people in. Depending on how much trouble - we run into, this may take a little while. After that, I hope I can welcome more friends onto - my server, in hopes of making this another way of getting together and staying in touch. -

-

- The available services on subcon town are still being tweaked; however we do now have proper - http, gopher and gemini hosting for our users. Nextcloud and gitea will remain available as well, - as will the new Akko's Friture akkoma instance. However, after some consideration, I have - decided to also shutdown a few services: Bookstack, the wiki solution, will be removed as it - is simply not in active use. The Snikket xmpp service might be discontinued if I cannot find - a fix for the http issues it has; it might be replaced with a regular Prosody installation. -

-

- I hope to bring more news on a much sooner schedule. Please wait warmly! -

-
-
-
-

Services

- - - - - - - - - - - - -
Akkoma: akkos.fritu.re
Doku wiki: library.subcon.town
Funkwhale: odeon.subcon.town
Gemini: gemini://subcon.town
Gopher: gopher://subcon.town
Gogs: factory.subcon.town
Fossil SCM: museum.subcon.town
Nextcloud: cloud.subcon.town
- -

- Additionally, subcon.town also serves the webpages for comfitu.re - and its subdomains. -

-
-
-
-

Users

- - - - - -
fristi
-
-
-
-

FAQ

-

- Q: It's been a year, are you ever gonna get this show running? -
- A: Yes. I really want this to work. A lot of things are in place now and - quite frankly, it's time to invite people in here to see how things go. -

-

- Q: Is Subcon Town open for new users? -
- A: No. The intended purpose is an invite-only tilde community, for friends and - family. After the testing phase, I will start sending out invites to people interested. - You may ask for an invite as well at that point, but the plan is to only let comfy people in. -

-

- Q: Can I get an account on one of your services? -
- A: Only as an existing member. We are currently not open for - new users to join. If you're already a member, ask - @fristi for an account. -

-

- Q: Didn't you also run kartoffel.cafe? -
- A: Yes, but a recent change of plans made running an activitypub - instance not very feasible, so I made the difficult choice of closing - the instance, for good. I currently run a new instance called - Akko's Friture, but it's a single user instance for now. -

-

- Q: Is that the Yoshi's Island font? -
- A: Yes. It was good game. -

-
-
- -
-
- - -
- - - - - - \ No newline at end of file diff --git a/libs/zap/Application.php b/libs/zap/Application.php new file mode 100644 index 0000000..b62c59a --- /dev/null +++ b/libs/zap/Application.php @@ -0,0 +1,106 @@ +setBasePath($basePath); + $this->initConfiguration(); + $this->initErrorHandling(); + $this->initRouting(); + } + + public static function getInstance(): static + { + return static::$instance; + } + + public function getBasePath($path = '') + { + return $this->basePath . (!empty($path) ? DIRECTORY_SEPARATOR.$path : ''); + } + + public function getPublicPath($path = '') + { + return $this->basePath . DIRECTORY_SEPARATOR . 'public_html' . (!empty($path) ? DIRECTORY_SEPARATOR.$path : ''); + } + + public function getViewPath($path = '') + { + return $this->basePath . DIRECTORY_SEPARATOR . 'resources/views' . (!empty($path) ? DIRECTORY_SEPARATOR.$path : ''); + } + + public function getConfig() + { + return $this->config; + } + + public function run() + { + if(!$this->router) { + (new Response('Could not get router instance, aborting.', 500)) + ->send(); + } + + $this->router->run(); + } + + + protected function setBasePath($basePath): static + { + $this->basePath = rtrim($basePath, '\/'); + return $this; + } + + protected function initConfiguration() + { + $this->config = new Repository($this); + } + + protected function initErrorHandling() + { + set_error_handler(function (int $nr, string $error, ?string $file, ?int $line) { + (new Handler($this))->reportError($nr, $error, $file, $line); + $this->sendErrorResponse(); + }); + + set_exception_handler(function (\Throwable $e) { + (new Handler($this))->report($e); + $this->sendErrorResponse(); + }); + } + + protected function initRouting() + { + $this->router = new Router($this); + } + + protected function sendErrorResponse() + { + (new Response('Whoops, something went wrong.', 500)) + ->send(); + } +} \ No newline at end of file diff --git a/libs/zap/Config/Repository.php b/libs/zap/Config/Repository.php new file mode 100644 index 0000000..b59934d --- /dev/null +++ b/libs/zap/Config/Repository.php @@ -0,0 +1,66 @@ +loadFromCache(realpath($app->getBasePath('cache')))) { + return; + } + + //Otherwise, load from config files + $this->loadConfigurationFiles(realpath($app->getBasePath('config'))); + } + + public function get(string $key, $default = null) + { + $keys = explode('.', $key); + + $value = $this->items; + foreach($keys as $index) { + if(!key_exists($index, $value)) return $default; + $value = $value[$index]; + } + + return $value; + } + + public function buildCache() + { + + } + + protected function loadFromCache(string $cachePath): bool + { + if(!file_exists($cachePath) || !file_exists($cachePath . DIRECTORY_SEPARATOR . 'config-cache.php')) { + return false; + } + + $this->items = require $cachePath . DIRECTORY_SEPARATOR . 'config-cache.php'; + return true; + } + + protected function loadConfigurationFiles(string $configPath) + { + $files = glob($configPath . DIRECTORY_SEPARATOR . '*.php'); + + if($files === false || empty($files)) { + throw new \RuntimeException('Unable to load configuration, no configuration files found.'); + } elseif(!in_array($configPath . DIRECTORY_SEPARATOR . 'application.php', $files)) { + throw new \RuntimeException('Unable to load the application configuration, app configuration file is missing.'); + } + + $this->items = []; + foreach($files as $file) { + $items = require $file; + $this->items = array_merge($this->items, $items); + } + } +} \ No newline at end of file diff --git a/libs/zap/ErrorHandling/Handler.php b/libs/zap/ErrorHandling/Handler.php new file mode 100644 index 0000000..e596a36 --- /dev/null +++ b/libs/zap/ErrorHandling/Handler.php @@ -0,0 +1,31 @@ +app = $app; + } + + public function report(\Throwable $e) + { + $resource = fopen('php://stderr', 'w'); + fwrite($resource, "An error was thrown! {$e->getMessage()}"); + fwrite($resource, "{$e->getTraceAsString()}"); + //Log the error. + } + + public function reportError(int $error_nr, string $error, ?string $file, ?int $line) + { + $resource = fopen('php://stderr', 'w'); + fwrite($resource, "An error occurred! {$error}"); + fwrite($resource, "Occurred in {$file}, at line {$line}."); + //Log the error. + } +} \ No newline at end of file diff --git a/libs/zap/Http/Response.php b/libs/zap/Http/Response.php new file mode 100644 index 0000000..e722301 --- /dev/null +++ b/libs/zap/Http/Response.php @@ -0,0 +1,235 @@ + 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', // RFC2518 + 103 => 'Early Hints', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', // RFC4918 + 208 => 'Already Reported', // RFC5842 + 226 => 'IM Used', // RFC3229 + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 308 => 'Permanent Redirect', // RFC7238 + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Content Too Large', // RFC-ietf-httpbis-semantics + 414 => 'URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Range Not Satisfiable', + 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', // RFC2324 + 421 => 'Misdirected Request', // RFC7540 + 422 => 'Unprocessable Content', // RFC-ietf-httpbis-semantics + 423 => 'Locked', // RFC4918 + 424 => 'Failed Dependency', // RFC4918 + 425 => 'Too Early', // RFC-ietf-httpbis-replay-04 + 426 => 'Upgrade Required', // RFC2817 + 428 => 'Precondition Required', // RFC6585 + 429 => 'Too Many Requests', // RFC6585 + 431 => 'Request Header Fields Too Large', // RFC6585 + 451 => 'Unavailable For Legal Reasons', // RFC7725 + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 506 => 'Variant Also Negotiates', // RFC2295 + 507 => 'Insufficient Storage', // RFC4918 + 508 => 'Loop Detected', // RFC5842 + 510 => 'Not Extended', // RFC2774 + 511 => 'Network Authentication Required', // RFC6585 + ]; + + protected string $charset = 'UTF-8'; + protected string $protocol = '1.1'; + protected array $headers; + protected int $status; + protected string $content; + + public function __construct($content = '', int $status = 200, array $headers = []) + { + $this->headers = $headers; + $this->status = $status; + $this->setContent($content); + } + + public function setHeader($header, $content): static + { + $this->headers[$header] = $content; + return $this; + } + + public function setStatus(int $status): static + { + $this->status = $status; + return $this; + } + + public function setContent($content): static + { + //If our content is an array of data, convert it to a json response. + if($content instanceof \ArrayObject || is_array($content)) { + $this->setHeader('Content-Type', 'application/json'); + $json = json_encode($content); + + if($json === false) { + throw new \InvalidArgumentException(json_last_error_msg()); + } + + $content = $json; + } + + //Otherwise look if it can be rendered. + /*elseif($content instanceof RenderableInterface) { + $content = $content->render(); + }*/ + + $this->content = $content; + return $this; + } + + public function setProtocol(string $protocol): static + { + $this->protocol = $protocol; + return $this; + } + + + public function send() + { + $this->prepare(); + + //Send headers + if(!headers_sent()) { + foreach($this->headers as $header => $content) { + header("{$header}: $content", false, $this->status); + } + + //TODO: send cookies + + $statusText = static::$statusTexts[$this->status] ?? ''; + header("HTTP/{$this->protocol} {$this->status} {$statusText}", true, $this->status); + } + + //Send content (if any) + if(!empty($this->content)) { + echo $this->content; + } + } + + protected function prepare() + { + if(!key_exists('Content-Type', $this->headers)) { + $this->headers['Content-Type'] = 'text/html; charset='.$this->charset; + } elseif (stripos($this->headers['Content-Type'], 'text/') === 0 && stripos($this->headers['Content-Type'], 'charset') === false) { + $this->headers['Content-Type'] = $this->headers['Content-Type'] . '; charset='.$this->charset; + } + } +} \ No newline at end of file diff --git a/libs/zap/Routing/Repository.php b/libs/zap/Routing/Repository.php new file mode 100644 index 0000000..79c5ff5 --- /dev/null +++ b/libs/zap/Routing/Repository.php @@ -0,0 +1,82 @@ +routes = []; + } + + /** + * Register a new route. + * + * @param string $pattern The uri pattern to match + * @param array|string $methods One or more HTTP methods to match + * @param string|array|callable $callback A callback to run if this route is called + * @param array $middlewares List of extra middlewares to run for this route + * @return void + */ + public function register(string $pattern, array|string $methods, string|array|callable $callback, array $middlewares = []): void + { + $route = new Route($pattern, $methods, $callback, $middlewares); + + foreach($route->getMethods() as $method) { + $this->routes[$method][] = $route; + } + } + + public function get(string $pattern, string|array|callable $callback, array $middlewares = []): void + { + $this->register($pattern, 'GET', $callback, $middlewares); + } + + public function post(string $pattern, string|array|callable $callback, array $middlewares = []): void + { + $this->register($pattern, 'POST', $callback, $middlewares); + } + + public function put(string $pattern, string|array|callable $callback, array $middlewares = []): void + { + $this->register($pattern, 'PUT', $callback, $middlewares); + } + + public function delete(string $pattern, string|array|callable $callback, array $middlewares = []): void + { + $this->register($pattern, 'DELETE', $callback, $middlewares); + } + + public function options(string $pattern, string|array|callable $callback, array $middlewares = []): void + { + $this->register($pattern, 'OPTIONS', $callback, $middlewares); + } + + public function patch(string $pattern, string|array|callable $callback, array $middlewares = []): void + { + $this->register($pattern, 'PATCH', $callback, $middlewares); + } + + public function head(string $pattern, string|array|callable $callback, array $middlewares = []): void + { + $this->register($pattern, 'HEAD', $callback, $middlewares); + } + + /** + * Get the list of registered routes. + * + * @return array + */ + public function getRoutes(): array + { + return $this->routes; + } +} \ No newline at end of file diff --git a/libs/zap/Routing/Route.php b/libs/zap/Routing/Route.php new file mode 100644 index 0000000..0c170ba --- /dev/null +++ b/libs/zap/Routing/Route.php @@ -0,0 +1,190 @@ + + */ + private array $methods = []; + + /** + * @var array + */ + private array $middlewares = []; + + /** + * @var array + */ + private array $vars = []; + + /** + * Route constructor. + */ + public function __construct(string $pattern, string|array $methods, string|array|callable $callback, array $middlewares = []) + { + if ($methods === []) { + throw new \InvalidArgumentException('HTTP methods argument was empty; must contain at least one method'); + } + + if(is_string($methods)) $methods = [$methods]; + foreach($methods as $key => $method) { + $methods[$key] = strtoupper($method); + if (!in_array($methods[$key], ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH', 'HEAD'])) { + throw new \InvalidArgumentException("Invalid method '{$method}' specified."); + } + } + + $this->pattern = $pattern; + $this->callback = $callback; + $this->methods = $methods; + $this->middlewares = $middlewares; + } + + /** + * @param string $path + * @param string $method + * @return bool + */ + public function match(string $path, string $method): bool + { + $regex = $this->getPattern(); + foreach ($this->getVarsNames() as $variable) { + $varName = trim($variable, '{\}'); + $regex = str_replace($variable, '(?P<' . $varName . '>[^/]++)', $regex); + } + + if (in_array($method, $this->getMethods()) && preg_match('#^' . $regex . '$#sD', self::trimPath($path), $matches)) { + $values = array_filter($matches, static function ($key) { + return is_string($key); + }, ARRAY_FILTER_USE_KEY); + foreach ($values as $key => $value) { + $this->vars[$key] = $value; + } + return true; + } + return false; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @param string $name + * @return $this + */ + public function setName(string $name): static + { + $this->name = $name; + return $this; + } + + /** + * @return string + */ + public function getPattern(): string + { + return $this->pattern; + } + + /** + * @return string|array|callable + */ + public function getCallback(): string|array|callable + { + return $this->callback; + } + + /** + * @return array|string[] + */ + public function getMethods(): array + { + return $this->methods; + } + + /** + * @return array + */ + public function getVarsNames(): array + { + preg_match_all('/{[^}]*}/', $this->pattern, $matches); + return reset($matches) ?? []; + } + + /** + * @return bool + */ + public function hasVars(): bool + { + return $this->getVarsNames() !== []; + } + + /** + * @return string[] + */ + public function getVars(): array + { + return $this->vars; + } + + /** + * @return string[] + */ + public function getMiddlewares(): array + { + return $this->middlewares; + } + + /** + * @return mixed + */ + public function run() + { + //TODO: middlewares + //TODO: allow for automatically injecting dependencies for function arguments + + if(is_callable($this->callback)) { + return call_user_func_array($this->callback, $this->vars); + } elseif (is_array($this->callback) && [0,1] == array_keys($this->callback)) { + $class = $this->callback[0]; + $method = $this->callback[1]; + if(class_exists($class) && method_exists($class, $method)) { + $controller = new $class(); + return $controller->$method(...$this->vars); + } + } + + throw new \RuntimeException("Could not resolve callable function for route."); + } + + /** + * @param string $path + * @return string + */ + public static function trimPath(string $path): string + { + return '/' . rtrim(ltrim(trim($path), '/'), '/'); + } +} \ No newline at end of file diff --git a/libs/zap/Routing/Router.php b/libs/zap/Routing/Router.php new file mode 100644 index 0000000..10b5941 --- /dev/null +++ b/libs/zap/Routing/Router.php @@ -0,0 +1,230 @@ +app = $app; + + //Load routes from cache, or fall back to loading from file + if($this->loadFromCache(realpath($app->getBasePath('cache')))) { + return; + } + + $this->loadFromRouteFile(realpath($app->getBasePath('routes'))); + } + + /** + * @return void + */ + public function run(): void + { + //Parse request + $this->requestUri = $this->getRequestUri(); + $this->requestMethod = $this->getRequestMethod(); + $this->requestHeaders ??= $this->getRequestHeaders(); + + //Find the first matching route. Only one route will ever be executed. + $route = $this->findRoute($this->requestUri, $this->requestMethod); + + //If route was found, run it. Otherwise return a 404 response. + if($route) { + $response = $this->handleRoute($route); + } else { + $response = $this->handleNotFoundResponse(); + } + + //Send response. + if (!is_a($response, Response::class)) { + $response = new Response((string) $response); + } + $response->send(); + + //If the request was a HEAD request, empty the output buffer. + if ($_SERVER['REQUEST_METHOD'] == 'HEAD') { + ob_end_clean(); + } + } + + public function buildCache() + { + + } + + /** + * Define the current relative URI. + * + * @return string + */ + protected function getRequestUri(): string + { + $uri = rawurldecode($_SERVER['REQUEST_URI']); + + // Don't take query params into account on the URL + if (strstr($uri, '?')) { + $uri = substr($uri, 0, strpos($uri, '?')); + } + + // Remove trailing slash + enforce a slash at the start + return '/' . trim($uri, '/'); + } + + /** + * Get all request headers. + * + * @return array The request headers + */ + protected function getRequestHeaders(): array + { + if(!empty($this->requestHeaders)) return $this->requestHeaders; + + $headers = []; + + // If getallheaders() is available, use that + if (function_exists('getallheaders')) { + $headers = getallheaders(); + + // getallheaders() can return false if something went wrong + if ($headers !== false) { + $this->requestHeaders = $headers; + return $headers; + } + } + + // Method getallheaders() not available or went wrong: manually extract them + foreach ($_SERVER as $name => $value) { + if ((substr($name, 0, 5) == 'HTTP_') || ($name == 'CONTENT_TYPE') || ($name == 'CONTENT_LENGTH')) { + $headers[str_replace([' ', 'Http'], ['-', 'HTTP'], ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + } + } + + $this->requestHeaders = $headers; + return $headers; + } + + /** + * Get the request method used, taking overrides into account. + * + * @return string The request method to handle + */ + protected function getRequestMethod(): string + { + // Take the method as found in $_SERVER + $method = $_SERVER['REQUEST_METHOD']; + + // If it's a HEAD request override it to being GET and prevent any output, as per HTTP Specification + // @url http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4 + if ($_SERVER['REQUEST_METHOD'] == 'HEAD') { + ob_start(); + $method = 'GET'; + } + + // If it's a POST request, check for a method override header + elseif ($_SERVER['REQUEST_METHOD'] == 'POST') { + $headers = $this->getRequestHeaders(); + if (isset($headers['X-HTTP-Method-Override']) && in_array($headers['X-HTTP-Method-Override'], array('PUT', 'DELETE', 'PATCH'))) { + $method = $headers['X-HTTP-Method-Override']; + } + } + + return $method; + } + + protected function loadFromCache(string $cachePath): bool + { + if(!file_exists($cachePath) || !file_exists($cachePath . DIRECTORY_SEPARATOR . 'route-cache.php')) { + return false; + } + + $this->routes = require $cachePath . DIRECTORY_SEPARATOR . 'route-cache.php'; + return true; + } + + protected function loadFromRouteFile(string $routePath): void + { + if(!file_exists($routePath) || !file_exists($routePath . DIRECTORY_SEPARATOR . 'routes.php')) { + throw new \RuntimeException('Unable to initialize routes: route file not found.'); + } + + $repository = new Repository(); + $routes = require $routePath . DIRECTORY_SEPARATOR . 'routes.php'; + + if(!is_callable($routes)) { + throw new \RuntimeException('Unable to initialize routes: route file does not contain callable function.'); + } + + //Initialize routes + call_user_func($routes, $repository); + $this->routes = $repository->getRoutes(); + } + + protected function findRoute(string $path, string $method): ?Route + { + $method = strtoupper($method); + + if(key_exists($method, $this->routes)) { + foreach($this->routes[$method] as $route) { + if($route->match($path, $method)) { + return $route; + } + } + } + + return null; + } + + protected function handleRoute(Route $route) + { + //Todo: run route middleware and route handler. + + return $route->run(); + } + + protected function handleNotFoundResponse() + { + //Do we have an error template? + $template = $this->app->getViewPath('errors/404.html'); + if(file_exists($template)) { + ob_start(); + require $template; + $response = ob_get_clean(); + + return new Response($response, 404); + } else { + return new Response('Whoops! Could not find that file.', 404); + } + } +} \ No newline at end of file diff --git a/pages/faq.php b/pages/faq.php deleted file mode 100644 index 415f498..0000000 --- a/pages/faq.php +++ /dev/null @@ -1,34 +0,0 @@ -

FAQ

-

- Q: It's been a year, are you ever gonna get this show running? -
- A: Yes. I really want this to work. A lot of things are in place now and - quite frankly, it's time to invite people in here to see how things go. -

-

- Q: Is Subcon Town open for new users? -
- A: No. The intended purpose is an invite-only tilde community, for friends and - family. After the testing phase, I will start sending out invites to people interested. - You may ask for an invite as well at that point, but the plan is to only let comfy people in. -

-

- Q: Can I get an account on one of your services? -
- A: Only as an existing member. We are currently not open for - new users to join. If you're already a member, ask - @fristi for an account. -

-

- Q: Didn't you also run kartoffel.cafe? -
- A: Yes, but a recent change of plans made running an activitypub - instance not very feasible, so I made the difficult choice of closing - the instance, for good. I currently run a new instance called - Akko's Friture, but it's a single user instance for now. -

-

- Q: Is that the Yoshi's Island font? -
- A: Yes. It was good game. -

\ No newline at end of file diff --git a/public_html/.htaccess b/public_html/.htaccess new file mode 100644 index 0000000..48a7a3d --- /dev/null +++ b/public_html/.htaccess @@ -0,0 +1,4 @@ +RewriteEngine on +RewriteCond %{REQUEST_FILENAME} !-d +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule . index.php [L] \ No newline at end of file diff --git a/public_html/assets/border.png b/public_html/assets/border.png new file mode 100644 index 0000000000000000000000000000000000000000..393ade76be8ebb486d95a0b3db0e2ee9b0d2bb74 GIT binary patch literal 396 zcmV;70dxL|P)Px$MoC0LRA_gl(JWm#Agc<-Mkcr}iQ4*N^v zxqAAzA%p+`u-3jM=+pRGJ&A!_@b)D@x-#6ADiM#*89vg}l{${veg#KQ7k89W^=r6# zb_3ShY_OCeuAVN5xbc{(f5=KnPjMI8K3i*(wnxTsWKW!KgeqI zbT!w5)~7H~cU^JLnLXHhe<7mmz8*qozVb0#J;guqSx^JgB)8GrZ|>Dr*8;Ad(`P^p zNTUX%Q3KNK=Kv8EAK((tO0J&MXFv@|qXwi=1JdYwKpHh5jRCYq_Eoe-rZuukv_=+p q{6jSc&>ET6$cphXS|dAqjqD0Vau$Ohf)TO+0000Px$+et)0RA_2WePG89cNd5~XL7HmN;|F!3X~4*TyiKp!-ShL|#rE;+mXC(dYknRG0DKYD>FJQm z&fSeU#2~2g-efl57z255)h3%p(J7;AS!u+CgLF=I(dQ=q0WJtkbrvLK7mJJ z8xg1(EzoaapH1^?fp5DnHAW!7N=P9QpdE+^)z1NSD-ms3rDapwN<4;; zcGisqe!d3skZOT=Z`^_uQvDp*^T1Nr^FUPWd0;8*c_1pbA%G{c8o-y;ZA1X5CkisD z4I&U-gqD# { + if('error' in response) { + replaceContent(response.error); + } + + if('content' in response) { + replaceContent(response.content); + if(!ignoreHistory) { + window.history.pushState(null, '', href); + } + } + + loading = false; + }); + } + } + + function replaceContent(content) + { + if(elContent) { + elContent.innerHTML = content; + elMain.scrollTo(0, 0); + showAddresses() + } + } + + function showAddresses() { + let a = 'fristi'; + let b = 'subcon.town'; + let c = 'akkos.fritu.re'; + + gogo.getAll('.em').forEach( element => { + element.innerHTML = a + '@' + b; + element.setAttribute('href', 'mailto:' + a + '@' + b); + }); + + gogo.getAll('.fv').forEach( element => { + element.innerHTML = a + '@' + c; + }); + } + + window.addEventListener('load', function(){ + elContent = gogo.get('.content'); + elMain = gogo.get('main'); + gogo.get('.container').addEventListener('click', event => { + if(!event.target.classList.contains('page-link')) return; + loadContent(event.target.getAttribute('href')); + event.preventDefault(); + }); + showAddresses(); + }); + + window.addEventListener('popstate', event => { + loadContent(document.location, true); + }); +})(gogo); \ No newline at end of file diff --git a/public_html/assets/js/gogo.js b/public_html/assets/js/gogo.js new file mode 100644 index 0000000..2b6c954 --- /dev/null +++ b/public_html/assets/js/gogo.js @@ -0,0 +1,91 @@ +/*! + * Subcon GoGo Tools v1.0.0 + * Licensed under BOML-1.0.0 license. + */ +(function (root, factory) { + if (typeof define === "function" && define.amd) { //AMD + define(factory); + } else if (typeof exports === "object") { //CommonJS + module.exports = factory(); + } else { //Browser global + root.gogo = factory(); + } + + //Let everything know the library is ready + const event = new Event("gogoReady"); + document.dispatchEvent(event); +}(this, function () { + let gogo = {}; + + //Query selectors + gogo.get = function(selector) { + return document.querySelector(selector); + }; + + gogo.getAll = function(selector) { + return document.querySelectorAll(selector); + }; + + //Ajax + gogo.ajax = async function(url = '', method = 'GET', data = {}) { + //Set up fetch settings + let settings = { + method: method, // *GET, POST, PUT, DELETE, etc. + mode: 'cors', // no-cors, *cors, same-origin + cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached + credentials: 'same-origin', // include, *same-origin, omit + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + // 'Content-Type': 'application/x-www-form-urlencoded', + //'X-CSRF-TOKEN': qs('meta[name="csrf-token"]').getAttribute('content'), + 'X-Requested-With': 'XMLHttpRequest' //Allow laravel to properly detect this as an AJAX request + }, + redirect: 'follow', // manual, *follow, error + referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url + } + + //If method is get or head, prepare data as query string + if(method === 'GET' || method === 'HEAD') { + if(Object.keys(data).length !== 0) { + let query = new URLSearchParams(data).toString() + url = url + '?' + query; + } + } else { + settings['body'] = JSON.stringify(data); + } + + //Fetch + const response = await fetch(url, settings); + + if(!response.ok || response.headers.get('content-type').indexOf('application/json') === -1) { + return { + 'error': 'An error occurred while trying to fetch page data. Please refresh and try again.' + } + } + + //Return response (uses Promise API) + return response.json(); + } + + //Used to allow injecting html and javascript from ajax requests + gogo.setHTML = function (element, html) { + element.innerHTML = html; + + Array.from(element.querySelectorAll("script")) + .forEach( oldScriptEl => { + const newScriptEl = document.createElement("script"); + + Array.from(oldScriptEl.attributes).forEach( attr => { + newScriptEl.setAttribute(attr.name, attr.value) + }); + + const scriptText = document.createTextNode(oldScriptEl.innerHTML); + newScriptEl.appendChild(scriptText); + + oldScriptEl.parentNode.replaceChild(newScriptEl, oldScriptEl); + }); + } + + return gogo; +})); \ No newline at end of file diff --git a/public_html/index.php b/public_html/index.php index e00c401..a6fac97 100644 --- a/public_html/index.php +++ b/public_html/index.php @@ -1,218 +1,12 @@ - - - - - - Subcon.town - Happy Dreams! - - $content) { ?> - - - - - - - - - - -
-
- Subcon Town - -
- -
-
- -
-

Hello world!

-

- Welcome to Subcon Town! We are a small tilde community, hosted for friends and family. - We offer SSH access, hosting of websites, gopher holes and gemini pods, alongside - other services like cloud services, git and fossil hosting and an activitypub server. -

-

- Tilde communities are pubnixes, public access unix systems. We took our inspiration - from older such servers like tilde.club - and the tildes from tildeverse.org. -

-

- Users may find out more about this system by visiting the wiki. - Keep in mind that the wiki is still a work in progress. -

-
-
-
-

News

-

Januari 2023: Plans for the year

-

- Happy new year! Subcon has come a long way since the change of the year. First off: we have moved to - new hardware! The server is now powered by an Odroid H3 board. With that, a number of other changes - have been made as well. -

-

- Storage on the server has been totally reworked; the old storage media have been replaced with a 2TB - NVMe drive that will, for one, house the user home directories, and a 4TB SATA SSD that will house all - variable data; user repositories, websites, databases, the lot. All of this has been set up with - convenient LVM partitions that can be easily resized if more space is required in the future. -

-

- Many services have been updated. The gitea server has been replaced with a new gogs server, now - hosted at factory.subcon.town. The old snikket server - has also been replaced with a new prosody setup, offering most of the same xmpp services at jab.subcon.town. - Note that old snikket accounts are thus gone. The fedora installation has also been updated to fedora - 37. -

-

- The most notable change however will be the decrease in processing power. While we still have about - 75% of the processing power, the upside is that the system as a whole is seriously more power efficient. - We are now running at a fraction of the power the old server needed, going from 60Wh on average to - a mere 6Wh. Serious power savings like that will lower upkeep cost, which I can in turn invest in - improving other things, such as my god awful internet bandwidth. -

-

- Remember that even with all these changes done, the server is, and will likely forever, be a work - in progress. There are still some rough edges needing work, further configuring of services, and - for the current while, a lot of work writing wiki pages at - library.subcon.town. -

-
-
-
-

Services

- -

- We provide the following services for our members: -

- - - - - - - - - - -
Akkoma: akkos.fritu.re
Doku wiki: library.subcon.town
Funkwhale: odeon.subcon.town
Gogs: factory.subcon.town
Fossil SCM: museum.subcon.town
Nextcloud: cloud.subcon.town
- -

- We also host member websites, gopher holes and gemini pods. User websites are listed below, - for gopher holes and gemini pods, visit gopher://subcon.town and - gemini://subcon.town respectively using a compatible browser. -

- - - - - - -
fristi
-
-
-
-

FAQ

-

- Q: You're running on the Odroid H3 now, right? -
- A: Yes, all Subcon services have been moved to the new hardware - and are fully functional. -

-

- Q: Is Subcon Town open for new users? -
- A: We're still doing some work on some services, but in general - everything is ready. Note that Subcon Town is an invite-only community; you may certainly - ask for an invite, but generally we will only accept people that are known to us already. -

-

- Q: Can I get an account on one of your services? -
- A: Only as an existing member. If you're already a member, ask - @fristi for an account. -

-

- Q: Is that the Yoshi's Island font? -
- A: Yes. It was good game. -

-
-
- -
-
- -
-
- © 2021-2023 Subcon Town. -  License -
-
-
- - - - - - \ No newline at end of file +$app->run(); \ No newline at end of file diff --git a/robots.txt b/public_html/robots.txt similarity index 100% rename from robots.txt rename to public_html/robots.txt diff --git a/resources/articles/2022/introducing_funkwhale.md b/resources/articles/2022/introducing_funkwhale.md new file mode 100644 index 0000000..6e57416 --- /dev/null +++ b/resources/articles/2022/introducing_funkwhale.md @@ -0,0 +1,30 @@ +--- +title: Introducing Funkwhale +author: Fristi +date: 2022-11 +description: After some fiddling around on the server, I have managed to finally get a Funkwhale instance set up! +--- + +# Introducing Funkwhale + +*November 2022, by Fristi* + +After some fiddling around on the server, I have managed to finally get a Funkwhale instance +set up! Funkwhale is a piece of software that will let us host our music libraries. You can listen +to your and others' music from the web interface, or use any compatible app on your phone or tablet. +Besides that, Funkwhale federates, much like our Akkoma instance. You will be able to listen to the +libraries of people on other instances as well! + +On a technical note, getting Funkwhale to work took a little bit of doing; it has been manually +set up according to the installation instructions for Arch Linux, adapted here and there to suit +Fedora. One remaining irk because of this, is that Funkwhale was not made with SELinux in mind. +One remaining problem that you might run into is that freshly uploaded music may not play initially; +this is caused by incorrect SELinux permissions. I have currently added a cronjob that will check +these permissions and fix them every few minutes. If your music does not play, please wait a few +minutes and try again. + +For the time being, users will be limited to 5GB libraries. If needed, you may request to have this +limit changed. Please do check before uploading if your music has already been uploaded by others; +preventing duplicate uploads will help conserve space on the server. + +Happy listening! \ No newline at end of file diff --git a/resources/articles/2022/phasing_out_gitea_and_introducing_fossil_scm.md b/resources/articles/2022/phasing_out_gitea_and_introducing_fossil_scm.md new file mode 100644 index 0000000..dbf9ea2 --- /dev/null +++ b/resources/articles/2022/phasing_out_gitea_and_introducing_fossil_scm.md @@ -0,0 +1,39 @@ +--- +title: Phasing out Gitea and introducing Fossil SCM +author: Fristi +date: 2022-11 +description: Recently it has come to my attention that the owners of Gitea have been making some changes... +--- + +# Phasing out Gitea and introducing Fossil SCM + +*November 2022, by Fristi* + +Recently it has come to my attention that the owners of Gitea have been making some changes +in their project management, mainly in the form of providing support to businesses. For more +information, view [the Gitea blog](https://blog.gitea.io/2022/10/open-source-sustainment-and-the-future-of-gitea/). + +Unfortunately, with the way things have gone, the community seems to be weary and distrustful of a +number of moves made by the current owners. My personal concerns are about one of the owners using +Gitea is a lucrative money-making scheme. To me, that's a deal breaker, as I don't know if I can +still trust the project after this. Unfortunate, since I know Gitea is supposed to be a community-run +effort. + +As such, I have decided to deprecate the Gitea service on Subcon Town. It will no longer be updated, +but will be kept alive for a while until all projects have been migrated. I am additionally looking +into migrating the data to a Gogs installation, continuing a git hosting service that way. Meanwhile, +I have opted to set up Fossil as an alternative. Fossil SCM is a slightly different VCS, similar to git, +but more geared towards tightly nit development groups that work closer together. + +While fossil may feel a bit more old-fashioned, I think it's also a bit more of a fit with Subcon's +pubnix feel. Users will be able to set up and manage their fossil repositories from their home +directories, in the new "public_fossil" directory. To create new repositories, you can use the +"mkfossil <filename>" command. This is a wrapper around "fossil init" that helps set the +correct file permissions. If done right, your new repo will pop up at +[museum.subcon.town](https://museum.subcon.town) from where you'll be able to administrate it. + +With the introduction of new commands and functions for users, I plan on writing a wiki article +that lists all available functions and how to use them. This should help new users settle in. +Note that the README files in your home directories will be kept updated as well. + +Hope to see you soon on Subcon Town! \ No newline at end of file diff --git a/resources/articles/2023/frontpage_updates_and_more.md b/resources/articles/2023/frontpage_updates_and_more.md new file mode 100644 index 0000000..06858da --- /dev/null +++ b/resources/articles/2023/frontpage_updates_and_more.md @@ -0,0 +1,41 @@ +--- +title: Frontpage updates and more +author: fristi +date: 2023-06 +description: It's been quiet for half a year. Subcon has been chucking along + quite well with nothing special happening. Even the dreaded Fedora 38 update + did not prove too much of a headache. All has been moseying along easily. +--- + +# Frontpage updates and more + +*June 2023, by Fristi* + +It's been quiet for half a year. Subcon has been chucking along quite well with +nothing special happening. Even the dreaded Fedora 38 update did not prove too +much of a headache. All has been moseying along easily. + +Actually, I've just not been in the mood to work much on the server recently +because of life stuff. I've recently quit my job as a programmer and took a much +needed hiatus away from programming to clear my head of stuff. I think this was +good, because I've slowly regained my interest in programming and maintaining my +server again. + +And that means updates! The website has been updated; not much new on the front, +but it's now a PHP-powered website with actual pages. It even has fancy ajax +page loading now (but the website will also work without javascript). This means +people can now properly link to different pages as well as the news articles. + +Besides the website, I've also set up Radicale, a caldav and carddav server, as +a potential replacement for Nextcloud's calendars and addressbooks. With recent +Nextcloud updates it has become clear that updating it usually leads to downtime +and issues, which for me defeats the purpose of having such a fully-featured +groupware service. As such I will also be searching for a replacement for +Nextcloud's file hosting and syncing. + +Up on the todo list are to update Funkwhale, which I hope isn't gonna spit in my +face. Also I will be picking up work on the custom artwork for the subcon +frontpage; parts of it have already been finished, so I really wanna get it all +over and done with. Other than that, I'll be busy with just the usual. Keep tabs +on the [wiki](https://library.subcon.town) for any new guides and stuff; I will +be adding one for using Radicale. \ No newline at end of file diff --git a/resources/articles/2023/plans_for_the_year.md b/resources/articles/2023/plans_for_the_year.md new file mode 100644 index 0000000..7831d40 --- /dev/null +++ b/resources/articles/2023/plans_for_the_year.md @@ -0,0 +1,42 @@ +--- +title: Plans for the year +author: fristi +date: 2023-01 +description: Happy new year! Subcon has come a long way since the change of the + year. The server is now powered by an Odroid H3 board. With that, a number of + other changes have been made as well. +--- + +# Plans for the year + +*Januari 2023, by Fristi* + +Happy new year! Subcon has come a long way since the change of the year. First +off: we have moved to new hardware! The server is now powered by an Odroid H3 +board. With that, a number of other changes have been made as well. + +Storage on the server has been totally reworked; the old storage media have been +replaced with a 2TB NVMe drive that will, for one, house the user home +directories, and a 4TB SATA SSD that will house all variable data; user +repositories, websites, databases, the lot. All of this has been set up with +convenient LVM partitions that can be easily resized if more space is required +in the future. + +Many services have been updated. The gitea server has been replaced with a new +gogs server, now hosted at [factory.subcon.town](https://factory.subcon.town). +The old snikket server has also been replaced with a new prosody setup, offering +most of the same xmpp services at [jab.subcon.town](https://jab.subcon.town). +Note that old snikket accounts are thus gone. The fedora installation has also +been updated to fedora 37. + +The most notable change however will be the decrease in processing power. While +we still have about 75% of the processing power, the upside is that the system +as a whole is seriously more power efficient. We are now running at a fraction +of the power the old server needed, going from 60Wh on average to a mere 6Wh. +Serious power savings like that will lower upkeep cost, which I can in turn +invest in improving other things, such as my god awful internet bandwidth. + +Remember that even with all these changes done, the server is, and will likely +forever, be a work in progress. There are still some rough edges needing work, +further configuring of services, and for the current while, a lot of work +writing wiki pages at [library.subcon.town](https://library.subcon.town). \ No newline at end of file diff --git a/resources/views/_layout/layout.php b/resources/views/_layout/layout.php new file mode 100644 index 0000000..bf08645 --- /dev/null +++ b/resources/views/_layout/layout.php @@ -0,0 +1,49 @@ + + + + + + <?php if(isset($title)) echo "{$title} - "; ?>Welcome to Subcon Town! + + $value) { ?> + + + + + + + + + + + +
+
+ Subcon Town + +
+ +
+
+
+ +
+
+
+ +
+
+ © 2021- Subcon Town. +  License +
+
+
+ + + \ No newline at end of file diff --git a/resources/views/errors/404.html b/resources/views/errors/404.html new file mode 100644 index 0000000..9e18cc8 --- /dev/null +++ b/resources/views/errors/404.html @@ -0,0 +1,25 @@ + + + + + 404 - Not Found + + + +
+
+

404 - Not found!

+

+ Whoops! Looks like we don't have that thing you're looking for. Maybe it got + moved or deleted, we don't know. Sorry! +

+

+ << Back to the homepage +

+
+
+ Internet Explorer +
+
+ + diff --git a/resources/views/pages/copyright.php b/resources/views/pages/copyright.php new file mode 100644 index 0000000..69467c8 --- /dev/null +++ b/resources/views/pages/copyright.php @@ -0,0 +1,25 @@ +

Copyright notice

+

+ The "Subcon Town" branding, including logos and icons used on this website, are + licensed as CC BY-NC-ND. You may share and redistribute these works, but please do not edit them or use them for + commercial purposes. For more details, read the license. +

+

+ The source code of this website is licensed under the Blue Oak Model License 1.0.0. + You may use this source code as you wish. + For more details, read the license. + The source code of this website may be viewed here. +

+ +

Third party works

+

+ The fonts and background image used on this website are not owned by Subcon Town. Subcon Town does + not claim any rights to these original works unless otherwise specified by their respective licensing. +

+ +

Complaints

+

+ If you feel any material is incorrectly used on this website, or otherwise infringes on your rights, + feel free to file a complaint with fristi「AT」subcon.town. Please allow up + to 48 hours for a response. +

\ No newline at end of file diff --git a/resources/views/pages/faq.php b/resources/views/pages/faq.php new file mode 100644 index 0000000..1ea1dc7 --- /dev/null +++ b/resources/views/pages/faq.php @@ -0,0 +1,25 @@ +

FAQ

+

+ Q: You're running on the Odroid H3 now, right? +
+ A: Yes, all Subcon services have been moved to the new hardware + and are fully functional. +

+

+ Q: Is Subcon Town open for new users? +
+ A: We're still doing some work on some services, but in general + everything is ready. Note that Subcon Town is an invite-only community; you may certainly + ask for an invite, but generally we will only accept people that are known to us already. +

+

+ Q: Can I get an account on one of your services? +
+ A: Only as an existing member. If you're already a member, ask + @fristi for an account. +

+

+ Q: Is that the Yoshi's Island font? +
+ A: Yes. It was good game. +

\ No newline at end of file diff --git a/resources/views/pages/index.php b/resources/views/pages/index.php new file mode 100644 index 0000000..0698969 --- /dev/null +++ b/resources/views/pages/index.php @@ -0,0 +1,30 @@ +

Hello world!

+

+ Welcome to Subcon Town! We are a small tilde community, hosted for friends and family. + We offer SSH access, hosting of websites, gopher holes and gemini pods, alongside + other services like cloud services, git and fossil hosting and an activitypub server. +

+

+ Tilde communities are pubnixes, public access unix systems. We took our inspiration + from older such servers like tilde.club + and the tildes from tildeverse.org. +

+

+ Users may find out more about this system by visiting the wiki. + Keep in mind that the wiki is still a work in progress. +

+ +
+ +

News

+ + +

getTitle(); ?>

+
+

getDescription(); ?>

+ +
+ Read more +
+
+ \ No newline at end of file diff --git a/resources/views/pages/news.php b/resources/views/pages/news.php new file mode 100644 index 0000000..e69de29 diff --git a/resources/views/pages/services.php b/resources/views/pages/services.php new file mode 100644 index 0000000..ebaa8da --- /dev/null +++ b/resources/views/pages/services.php @@ -0,0 +1,34 @@ +

Services

+ +

+ We provide the following services for our members: +

+ + + + + + + + + + + +
Akkoma: akkos.fritu.re
Doku wiki: library.subcon.town
Funkwhale: odeon.subcon.town
Gogs: factory.subcon.town
Fossil SCM: museum.subcon.town
Nextcloud: cloud.subcon.town
Radicale: cal.subcon.town
+ +

+ We also host member websites, gopher holes and gemini pods. User websites are listed below, + for gopher holes and gemini pods, visit gopher://subcon.town and + gemini://subcon.town respectively using a compatible browser. +

+ +
+ +

User tildes:

+ + + + + + +
fristi
\ No newline at end of file diff --git a/routes/routes.php b/routes/routes.php new file mode 100644 index 0000000..006fcf8 --- /dev/null +++ b/routes/routes.php @@ -0,0 +1,15 @@ +get('/', [FrontpageController::class, 'index']); + $routes->get('/news/{slug}', [FrontpageController::class, 'article']); + $routes->get('/services', [FrontpageController::class, 'services']); + $routes->get('/faq', [FrontpageController::class, 'faq']); + $routes->get('/copyright', [FrontpageController::class, 'copyright']); +}; + + diff --git a/src/Controllers/FrontpageController.php b/src/Controllers/FrontpageController.php new file mode 100644 index 0000000..d9ee278 --- /dev/null +++ b/src/Controllers/FrontpageController.php @@ -0,0 +1,106 @@ +app = Application::getInstance(); + } + + public function index() + { + $articles = Article::getNewestArticles(3); + return $this->loadPage('index', ['articles' => $articles]); + } + + public function article(string $slug) + { + $article = Article::getArticle($slug); + if(!$article) { + return $this->notFoundError(); + } + + return $this->loadArticle($article); + } + + public function services() + { + return $this->loadPage('services'); + } + + public function faq() + { + return $this->loadPage('faq'); + } + + public function copyright() + { + return $this->loadPage('copyright'); + } + + private function loadPage(string $page, array $data = []) + { + if(isset($_SERVER['HTTP_ACCEPT']) && $_SERVER['HTTP_ACCEPT'] == 'application/json') { + //Only load content without the main template, return as array + $content = $this->toBuffer($this->app->getViewPath("pages/{$page}.php"), $data); + return new Response([ + 'content' => $content + ]); + } else { + $content = $this->toBuffer($this->app->getViewPath("pages/{$page}.php"), $data); + $view = $this->toBuffer($this->app->getViewPath('_layout/layout.php'), array_merge($data, ['content' => $content])); + return new Response($view); + } + } + + private function loadArticle(Article $article) + { + $data = [ + 'content' => $article->getRender()->getContent(), + 'meta' => [ + 'title' => $article->getTitle(), + 'author' => $article->getAuthor(), + 'date' => date('F Y', $article->getTimestamp()), + 'description' => $article->getDescription() + ] + ]; + + if(isset($_SERVER['HTTP_ACCEPT']) && $_SERVER['HTTP_ACCEPT'] == 'application/json') { + //Only load content without the main template, return as array + return new Response($data); + } else { + $view = $this->toBuffer($this->app->getViewPath('_layout/layout.php'), $data); + return new Response($view); + } + } + + private function toBuffer($path, array $data = []) + { + ob_start(); + if(!empty($data)) { + extract($data); + } + require $path; + return ob_get_clean(); + } + + private function notFoundError() + { + $html = $this->toBuffer($this->app->getViewPath('errors/404.html')); + return new Response($html, 404); + } + +} \ No newline at end of file diff --git a/src/Models/Article.php b/src/Models/Article.php new file mode 100644 index 0000000..5ee29f6 --- /dev/null +++ b/src/Models/Article.php @@ -0,0 +1,259 @@ +path = $path; + + if(!empty($cacheData)) { + foreach($cacheData as $property => $value) { + switch ($property) { + case 'title': + case 'author': + case 'description': + $this->$property = $value ?? ''; + break; + case 'date': + $this->timestamp = $value; + break; + } + } + } + } + + /** + * Get an article + * @param string $slug + * @return static|null + */ + public static function getArticle(string $slug): ?static + { + $app = Application::getInstance(); + if(!$app) throw new \RuntimeException('Unable to get articles; Application not yet initialized.'); + + if(!file_exists($app->getBasePath('cache/articles/cache.php'))) { + self::createArticleCache(); + } + + $cache = require $app->getBasePath('cache/articles/cache.php'); + if(key_exists($slug, $cache)) { + $article = $cache[$slug]; + return new Article($article['path'], $article); + } else { + return null; + } + } + + /** + * Get a list of Article instances for each article on file. + * + * @return array + */ + public static function getArticles(int $amount = -1): array + { + $app = Application::getInstance(); + if(!$app) throw new \RuntimeException('Unable to get articles; Application not yet initialized.'); + + if(!file_exists($app->getBasePath('cache/articles/cache.php'))) { + self::createArticleCache(); + } + + $articles = []; + $cache = require $app->getBasePath('cache/articles/cache.php'); + foreach($cache as $article) { + $articles[] = new Article($article['path'], $article); + $amount--; + + if($amount == 0) break; + } + + return $articles; + } + + /** + * Get a list of Article instances for the newest articles, optionally specifying + * the amount of articles. + * + * @param int $amount The amount of articles to load, defaults to 5. + * @return array + */ + public static function getNewestArticles(int $amount = 5): array + { + return self::getArticles($amount); + } + + /** + * Create an article cache containing a sorted list of article file paths and their metadata. + * + * @return void + */ + public static function createArticleCache() + { + $app = Application::getInstance(); + if(!$app) throw new \RuntimeException('Unable to cache articles; Application not yet initialized.'); + + $cache = []; + + //Init frontmatter parser + $parser = new FrontMatterParser(new SymfonyYamlFrontMatterParser()); + + $paths = glob(Application::getInstance()->getBasePath("resources/articles") . '/*/*.md'); + foreach ($paths as $path) { + $content = file_get_contents($path); + $result = $parser->parse($content); + $yaml = $result->getFrontMatter(); + + $article = [ + 'path' => $path, + 'slug' => str_replace('_', '-', basename($path, '.md')) + ]; + + foreach($yaml as $property => $value) { + switch ($property) { + case 'date': //Convert to timestamp for easier handling and sorting + $article['date'] = strtotime($value); + break; + default: + $article[$property] = $value; + } + } + + $cache[$article['slug']] = $article; + } + + //Sort cache by timestamp + uasort($cache, function ($a, $b) { + return $b['date'] <=> $a['date']; + }); + + //Check if directories exist + if(!file_exists($app->getBasePath('cache/articles'))) { + mkdir($app->getBasePath('cache/articles'), 0755, true); + } + + //Write to file + $php = 'getBasePath('cache/articles/cache.php'), $php); + } + + /** + * @return string + */ + public function getPath(): string + { + return $this->path; + } + + /** + * @return string + */ + public function getSlug(): string + { + return str_replace('_', '-', basename($this->path, '.md')); + } + + /** + * @return string + */ + public function getTitle(): string + { + return $this->title; + } + + /** + * @return int + */ + public function getTimestamp(): int + { + return $this->timestamp; + } + + /** + * @return string + */ + public function getDescription(): string + { + return $this->description; + } + + /** + * @return string + */ + public function getAuthor(): string + { + return $this->author; + } + + /** + * @return RenderedContentInterface + */ + public function getRender(): RenderedContentInterface + { + if(!isset($this->render)) { + $this->parseContent(); + } + + return $this->render; + } + + /** + * @return void + * @throws \League\CommonMark\Exception\CommonMarkException + */ + private function parseContent() + { + $environment = new Environment([]); + $environment->addExtension(new CommonMarkCoreExtension()); + $environment->addExtension(new FrontMatterExtension()); + + $converter = new MarkdownConverter($environment); + $markdown = file_get_contents($this->path); + $this->render = $converter->convert($markdown); + } +} \ No newline at end of file diff --git a/style_errors.css b/style_errors.css deleted file mode 100644 index f7740d6..0000000 --- a/style_errors.css +++ /dev/null @@ -1,50 +0,0 @@ -html, body { - margin:0; - padding:0; -} - -body { - width: 100%; - font-size: 16px; - color: white; - text-align: center; -} - -.background { - position:fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: url("assets/background.jpg"); - background-position: center center; - background-repeat: no-repeat; - background-size: cover; -} - -header, main, footer { - position: relative; - max-width: 600px; - box-sizing: border-box; - padding: 10px 0; -} - -header { - margin: 100px auto 10px auto; -} - -main { - margin: 0 auto 10px auto; -} - -footer { - margin: 0 auto 50px auto; - font-size: .8rem; -} - -h1 { - margin: 0; - padding: 0; - font-weight: bold; - font-size: 1.2rem; -} \ No newline at end of file