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 0000000..393ade7 Binary files /dev/null and b/public_html/assets/border.png differ diff --git a/public_html/assets/border2.png b/public_html/assets/border2.png new file mode 100644 index 0000000..f2a8dd9 Binary files /dev/null and b/public_html/assets/border2.png differ diff --git a/public_html/style.css b/public_html/assets/css/style.css similarity index 89% rename from public_html/style.css rename to public_html/assets/css/style.css index 258c80f..de2b759 100644 --- a/public_html/style.css +++ b/public_html/assets/css/style.css @@ -1,7 +1,7 @@ @font-face { font-family: 'yoster_islandregular'; - src: url('assets/yoster-web.woff2') format('woff2'), - url('assets/yoster-web.woff') format('woff'); + src: url('../fonts/yoster-web.woff2') format('woff2'), + url('../fonts/yoster-web.woff') format('woff'); font-weight: normal; font-style: normal; } @@ -29,7 +29,7 @@ html, body { } body { - background-image: url("assets/background3.png"); + background-image: url("../background3.png"); background-position: center center; background-size: cover; background-attachment: fixed; @@ -45,8 +45,7 @@ body { position: absolute; left:0; right:0; - /*height: 100%;*/ - max-height: 100%; + height: 100%; max-width: 640px; padding: 0 10px; margin: auto; @@ -105,9 +104,10 @@ header ul li a:hover:before { main { flex: 1 1 auto; + height: 488px; padding: var(--spacing-large); border: 32px solid transparent; - border-image: url('assets/border2x2.png') 32 fill round; + border-image: url('../border2x2.png') 32 fill round; overflow-y: auto; scrollbar-color: #fff transparent; scrollbar-width: thin; @@ -175,4 +175,13 @@ a:not(header a):after { content:"]"; margin-left: 6px; } table tbody tr td:first-child { padding: 0 3em 0 0; +} + +.article-stub p { + margin-bottom: 0; +} + +.article-stub-end { + text-align: right; + margin-bottom: var(--spacing-large); } \ No newline at end of file diff --git a/public_html/assets/yoster-web.woff b/public_html/assets/fonts/yoster-web.woff similarity index 100% rename from public_html/assets/yoster-web.woff rename to public_html/assets/fonts/yoster-web.woff diff --git a/public_html/assets/yoster-web.woff2 b/public_html/assets/fonts/yoster-web.woff2 similarity index 100% rename from public_html/assets/yoster-web.woff2 rename to public_html/assets/fonts/yoster-web.woff2 diff --git a/public_html/assets/js/app.js b/public_html/assets/js/app.js new file mode 100644 index 0000000..94b3c2f --- /dev/null +++ b/public_html/assets/js/app.js @@ -0,0 +1,66 @@ +(function(gogo){ + let loading = false; + let elContent; + let elMain; + + function loadContent(href, ignoreHistory = false) + { + if(!loading) { + loading = true; + gogo.ajax(href, 'GET') + .then((response) => { + 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