Compare commits

..

4 commits

54 changed files with 2083 additions and 505 deletions

4
.gitignore vendored
View file

@ -1,2 +1,6 @@
deploy.sh
deploy-excludes
composer.lock
.idea
vendor
cache/*

View file

@ -1,27 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Subcon Town - 401</title>
<link rel="shortcut icon" type="image/jpg" href="/assets/subcon/favicon_16.png" sizes="16x16"/>
<link rel="shortcut icon" type="image/jpg" href="/assets/subcon/favicon_32.png" sizes="32x32"/>
<link href="/style_errors.css?v=1.0.0" rel="stylesheet"/>
</head>
<body>
<div class="background"></div>
<header>
<img src="/assets/subcon/cloud_64.png" alt="logo"/>
</header>
<main>
<h1>You are not authorized to access that page.</h1>
</main>
<footer>
HTTP status: 401 - Unauthorized
</footer>
</body>
</html>

View file

@ -1,27 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Subcon Town - 404</title>
<link rel="shortcut icon" type="image/jpg" href="/assets/subcon/favicon_16.png" sizes="16x16"/>
<link rel="shortcut icon" type="image/jpg" href="/assets/subcon/favicon_32.png" sizes="32x32"/>
<link href="/style_errors.css?v=1.0.0" rel="stylesheet"/>
</head>
<body>
<div class="background"></div>
<header>
<img src="/assets/subcon/cloud_64.png" alt="logo"/>
</header>
<main>
<h1>That page does not exist.</h1>
</main>
<footer>
HTTP status: 404 - Not found
</footer>
</body>
</html>

View file

@ -1,27 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Subcon Town - 410</title>
<link rel="shortcut icon" type="image/jpg" href="/assets/subcon/favicon_16.png" sizes="16x16"/>
<link rel="shortcut icon" type="image/jpg" href="/assets/subcon/favicon_32.png" sizes="32x32"/>
<link href="/style_errors.css?v=1.0.0" rel="stylesheet"/>
</head>
<body>
<div class="background"></div>
<header>
<img src="/assets/subcon/cloud_64.png" alt="logo"/>
</header>
<main>
<h1>That page is gone. Forever.</h1>
</main>
<footer>
HTTP status: 410 - Gone
</footer>
</body>
</html>

View file

@ -1,31 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Subcon Town - 500</title>
<link rel="shortcut icon" type="image/jpg" href="/assets/subcon/favicon_16.png" sizes="16x16"/>
<link rel="shortcut icon" type="image/jpg" href="/assets/subcon/favicon_32.png" sizes="32x32"/>
<link href="/style_errors.css?v=1.0.0" rel="stylesheet"/>
</head>
<body>
<div class="background"></div>
<header>
<img src="/assets/subcon/cloud_64.png" alt="logo"/>
</header>
<main>
<h1>An error occurred on the server.</h1>
<p>
The page you requested could not be served due to an internal server error. Please try again
later.
</p>
</main>
<footer>
HTTP status: 500 - Internal Server Error
</footer>
</body>
</html>

View file

@ -1,31 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Subcon Town - 502</title>
<link rel="shortcut icon" type="image/jpg" href="/assets/subcon/favicon_16.png" sizes="16x16"/>
<link rel="shortcut icon" type="image/jpg" href="/assets/subcon/favicon_32.png" sizes="32x32"/>
<link href="/style_errors.css?v=1.0.0" rel="stylesheet"/>
</head>
<body>
<div class="background"></div>
<header>
<img src="/assets/subcon/cloud_64.png" alt="logo"/>
</header>
<main>
<h1>Bad Gateway.</h1>
<p>
The page you requested could not be served due to an internal communication error. Please
try again later.
</p>
</main>
<footer>
HTTP status: 502 - Bad gateway
</footer>
</body>
</html>

View file

@ -1,3 +1,25 @@
# subcon.town
# Subcon Town Frontpage
The subcon.town frontpage.
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.

3
cmd.php Normal file
View file

@ -0,0 +1,3 @@
<?php
//TODO: add CLI commands for handling certain tasks.

23
composer.json Normal file
View file

@ -0,0 +1,23 @@
{
"name": "subcon/frontpage",
"description": "Subcon town frontpage",
"type": "project",
"license": "BOML-1.0.0",
"autoload": {
"psr-4": {
"Subcon\\Frontpage\\": "src/",
"Subcon\\Zap\\": "libs/zap/"
}
},
"authors": [
{
"name": "Fristi",
"email": "fristi@subcon.town"
}
],
"minimum-stability": "stable",
"require": {
"league/commonmark": "^2.4",
"symfony/yaml": "^6.3"
}
}

27
config/application.php Normal file
View file

@ -0,0 +1,27 @@
<?php return [
/*
* Application configuration
*
* These settings contain the base configuration of your application.
* It is recommended that you set encryption.key as this key will be used
* to provide data encryption to your application.
*/
'application' => [
'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'
]
]
];

View file

@ -1,290 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Subcon.town - Happy Dreams!</title>
<link rel="shortcut icon" type="image/jpg" href="assets/subcon/favicon_16.png" sizes="16x16"/>
<link rel="shortcut icon" type="image/jpg" href="assets/subcon/favicon_32.png" sizes="32x32"/>
<link href="style.css?v=1.2.0" rel="stylesheet"/>
<script>
function showAddresses() {
let a = 'fristi';
let b = 'subcon.town';
let c = 'akkos.fritu.re';
document.querySelectorAll('.em').forEach( element => {
element.innerHTML = a + '@' + b;
element.setAttribute('href', 'mailto:' + a + '@' + b);
});
document.querySelectorAll('.fv').forEach( element => {
element.innerHTML = a + '@' + c;
});
}
</script>
</head>
<body>
<div class="container">
<header>
<img src="assets/subcon/logo_128.png" alt="Subcon Town" />
<ul>
<li><a href="#home">Home</a></li>
<li><a href="#news">News</a></li>
<li><a href="#services">Services</a></li>
<li><a href="#users">Users</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
</header>
<main>
<div class="inner-main">
<div class="content" id="home">
<h1>Hello world!</h1>
<p>
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.
</p>
</div>
<hr />
<div class="content" id="news">
<h1>News</h1>
<h2>Januari 2023: Plans for the year</h2>
<p>
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.
</p>
<p>
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.
</p>
<p>
Many services have been updated. The gitea server has been replaced with a new gogs server, now
hosted at <a href="https://factory.subcon.town">factory.subcon.town</a>. 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.
</p>
<p>
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.
</p>
<p>
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 <a href="https://library.subcon.town">
library.subcon.town</a>.
</p>
<h2>November 2022: Introducing Funkwhale</h2>
<p>
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!
</p>
<p>
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.
</p>
<p>
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.
</p>
<p>
Happy listening!
</p>
<h2>November 2022: Phasing out Gitea and introducing Fossil SCM</h2>
<p>
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
<a href="https://blog.gitea.io/2022/10/open-source-sustainment-and-the-future-of-gitea/">the Gitea blog</a>.
</p>
<p>
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.
</p>
<p>
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.
</p>
<p>
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 &lt;filename&gt;" 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 <a href="https://museum.subcon.town">museum.subcon.town</a>
from where you'll be able to administrate it.
</p>
<p>
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.
</p>
<p>
Hope to see you soon on Subcon Town!
</p>
<h2>October 2022: Finalizing stuff</h2>
<p>
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.
</p>
<p>
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.
</p>
<p>
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.
</p>
<p>
I hope to bring more news on a much sooner schedule. Please wait warmly!
</p>
</div>
<hr />
<div class="content" id="services">
<h1>Services</h1>
<table>
<tbody>
<tr> <td>Akkoma:</td> <td><a href="https://akkos.fritu.re">akkos.fritu.re</a></td> </tr>
<tr> <td>Doku wiki:</td> <td><a href="https://library.subcon.town">library.subcon.town</a></td> </tr>
<tr> <td>Funkwhale:</td> <td><a href="https://odeon.subcon.town">odeon.subcon.town</a></td> </tr>
<tr> <td>Gemini:</td> <td><a href="gemini://subcon.town">gemini://subcon.town</a></td> </tr>
<tr> <td>Gopher:</td> <td><a href="gopher://subcon.town">gopher://subcon.town</a></td> </tr>
<tr> <td>Gogs:</td> <td><a href="https://factory.subcon.town">factory.subcon.town</a></td> </tr>
<tr> <td>Fossil SCM:</td> <td><a href="https://museum.subcon.town">museum.subcon.town</a></td> </tr>
<tr> <td>Nextcloud:</td> <td><a href="https://cloud.subcon.town">cloud.subcon.town</a></td> </tr>
</tbody>
</table>
<p>
Additionally, subcon.town also serves the webpages for <a href="https://comfitu.re">comfitu.re</a>
and its subdomains.
</p>
</div>
<hr />
<div class="content" id="users">
<h1>Users</h1>
<table>
<tr>
<td width="50%"><a href="~fristi">fristi</a></td>
<td width="50%"></td>
</tr>
</table>
</div>
<hr />
<div class="content" id="faq">
<h1>FAQ</h1>
<p>
<strong>Q</strong>: <em>It's been a year, are you ever gonna get this show running?</em>
<br/>
<strong>A</strong>: 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.
</p>
<p>
<strong>Q</strong>: <em>Is Subcon Town open for new users?</em>
<br/>
<strong>A</strong>: 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.
</p>
<p>
<strong>Q</strong>: <em>Can I get an account on one of your services?</em>
<br/>
<strong>A</strong>: Only as an existing member. We are currently not open for
new users to join. If you're already a member, ask
<a href="https://akkos.fritu.re/fristi">@fristi</a> for an account.
</p>
<p>
<strong>Q</strong>: <em>Didn't you also run kartoffel.cafe?</em>
<br/>
<strong>A</strong>: 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
<a href="https://akkos.fritu.re">Akko's Friture</a>, but it's a single user instance for now.
</p>
<p>
<strong>Q</strong>: <em>Is that the Yoshi's Island font?</em>
<br/>
<strong>A</strong>: Yes. It was good game.
</p>
</div>
<hr />
<div class="content" id="copyright">
<h1>Copyright notice</h1>
<p>
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 <a target="_blank" href="./assets/subcon/LICENSE">license</a>.
</p>
<p>
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 <a target="_blank" href="./LICENSE">license</a>.
The source code of this website may be viewed <a target="_blank" href="https://factory.subcon.town/fristi/subcon.town">here</a>.
</p>
<h2>Third party works</h2>
<p>
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.
</p>
<h2>Complaints</h2>
<p>
If you feel any material is incorrectly used on this website, or otherwise infringes on your rights,
feel free to file a complaint with <a class="em" href="#">fristi「AT」subcon.town</a>. Please allow up
to 48 hours for a response.
</p>
</div>
</div>
</main>
<footer>
<div style="text-align: right">
© 2021-2022 Subcon Town.
&nbsp;<a href="#copyright">License</a>
&nbsp;<a target="_blank" href="https://factory.subcon.town/fristi/subcon.town">Source</a>
</div>
</footer>
</div>
<!-- load email addresses -->
<script> showAddresses(); </script>
</body>
</html>

106
libs/zap/Application.php Normal file
View file

@ -0,0 +1,106 @@
<?php
namespace Subcon\Zap;
use Subcon\Zap\Config\Repository;
use Subcon\Zap\ErrorHandling\Handler;
use Subcon\Zap\Http\Response;
use Subcon\Zap\Routing\Router;
class Application
{
const VERSION = '2.0.0';
protected static $instance;
protected string $basePath;
protected Repository $config;
protected Router $router;
public function __construct(string $basePath = null)
{
if(!$basePath || !file_exists($basePath) || !is_dir($basePath)) {
$basePath = implode('/', array_slice(explode('/', $_SERVER['SCRIPT_NAME']), 0, -2)) . '/';
}
//Initialize paths and load up environment and config files.
static::$instance = $this;
$this->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();
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace Subcon\Zap\Config;
use Subcon\Zap\Application;
class Repository
{
protected array $items;
public function __construct(Application $app)
{
//Load from cache if it exists
if($this->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);
}
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace Subcon\Zap\ErrorHandling;
use Subcon\Zap\Application;
class Handler
{
protected $app;
public function __construct(Application $app)
{
$this->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.
}
}

235
libs/zap/Http/Response.php Normal file
View file

@ -0,0 +1,235 @@
<?php
namespace Subcon\Zap\Http;
class Response
{
// HTTP Response codes (shamelessly stolen from Symfony's Response class)
public const HTTP_CONTINUE = 100;
public const HTTP_SWITCHING_PROTOCOLS = 101;
public const HTTP_PROCESSING = 102; // RFC2518
public const HTTP_EARLY_HINTS = 103; // RFC8297
public const HTTP_OK = 200;
public const HTTP_CREATED = 201;
public const HTTP_ACCEPTED = 202;
public const HTTP_NON_AUTHORITATIVE_INFORMATION = 203;
public const HTTP_NO_CONTENT = 204;
public const HTTP_RESET_CONTENT = 205;
public const HTTP_PARTIAL_CONTENT = 206;
public const HTTP_MULTI_STATUS = 207; // RFC4918
public const HTTP_ALREADY_REPORTED = 208; // RFC5842
public const HTTP_IM_USED = 226; // RFC3229
public const HTTP_MULTIPLE_CHOICES = 300;
public const HTTP_MOVED_PERMANENTLY = 301;
public const HTTP_FOUND = 302;
public const HTTP_SEE_OTHER = 303;
public const HTTP_NOT_MODIFIED = 304;
public const HTTP_USE_PROXY = 305;
public const HTTP_RESERVED = 306;
public const HTTP_TEMPORARY_REDIRECT = 307;
public const HTTP_PERMANENTLY_REDIRECT = 308; // RFC7238
public const HTTP_BAD_REQUEST = 400;
public const HTTP_UNAUTHORIZED = 401;
public const HTTP_PAYMENT_REQUIRED = 402;
public const HTTP_FORBIDDEN = 403;
public const HTTP_NOT_FOUND = 404;
public const HTTP_METHOD_NOT_ALLOWED = 405;
public const HTTP_NOT_ACCEPTABLE = 406;
public const HTTP_PROXY_AUTHENTICATION_REQUIRED = 407;
public const HTTP_REQUEST_TIMEOUT = 408;
public const HTTP_CONFLICT = 409;
public const HTTP_GONE = 410;
public const HTTP_LENGTH_REQUIRED = 411;
public const HTTP_PRECONDITION_FAILED = 412;
public const HTTP_REQUEST_ENTITY_TOO_LARGE = 413;
public const HTTP_REQUEST_URI_TOO_LONG = 414;
public const HTTP_UNSUPPORTED_MEDIA_TYPE = 415;
public const HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
public const HTTP_EXPECTATION_FAILED = 417;
public const HTTP_I_AM_A_TEAPOT = 418; // RFC2324
public const HTTP_MISDIRECTED_REQUEST = 421; // RFC7540
public const HTTP_UNPROCESSABLE_ENTITY = 422; // RFC4918
public const HTTP_LOCKED = 423; // RFC4918
public const HTTP_FAILED_DEPENDENCY = 424; // RFC4918
public const HTTP_TOO_EARLY = 425; // RFC-ietf-httpbis-replay-04
public const HTTP_UPGRADE_REQUIRED = 426; // RFC2817
public const HTTP_PRECONDITION_REQUIRED = 428; // RFC6585
public const HTTP_TOO_MANY_REQUESTS = 429; // RFC6585
public const HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431; // RFC6585
public const HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451; // RFC7725
public const HTTP_INTERNAL_SERVER_ERROR = 500;
public const HTTP_NOT_IMPLEMENTED = 501;
public const HTTP_BAD_GATEWAY = 502;
public const HTTP_SERVICE_UNAVAILABLE = 503;
public const HTTP_GATEWAY_TIMEOUT = 504;
public const HTTP_VERSION_NOT_SUPPORTED = 505;
public const HTTP_VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL = 506; // RFC2295
public const HTTP_INSUFFICIENT_STORAGE = 507; // RFC4918
public const HTTP_LOOP_DETECTED = 508; // RFC5842
public const HTTP_NOT_EXTENDED = 510; // RFC2774
public const HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511; // RFC6585
/**
* Status codes translation table.
*
* Shamelessly stolen from Symfony's Response class.
*
* The list of codes is complete according to the
* {@link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml Hypertext Transfer Protocol (HTTP) Status Code Registry}
* (last updated 2021-10-01).
*
* Unless otherwise noted, the status code is defined in RFC2616.
*
* @var array
*/
public static $statusTexts = [
100 => '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;
}
}
}

View file

@ -0,0 +1,82 @@
<?php
namespace Subcon\Zap\Routing;
class Repository
{
/**
* @var array
*/
private array $routes;
/**
* RoutingRepository constructor.
*/
public function __construct()
{
$this->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;
}
}

190
libs/zap/Routing/Route.php Normal file
View file

@ -0,0 +1,190 @@
<?php
namespace Subcon\Zap\Routing;
class Route
{
/**
* @var string
*/
private string $name;
/**
* @var string
*/
private string $pattern;
/**
* @var string|array|callable
*/
private $callback;
/**
* @var array<string>
*/
private array $methods = [];
/**
* @var array<string>
*/
private array $middlewares = [];
/**
* @var array<string>
*/
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), '/'), '/');
}
}

230
libs/zap/Routing/Router.php Normal file
View file

@ -0,0 +1,230 @@
<?php
namespace Subcon\Zap\Routing;
use Subcon\Zap\Application;
use Subcon\Zap\Http\Response;
class Router
{
/**
* @var Application
*/
private Application $app;
/**
* @var array
*/
private array $routes;
/**
* @var string
*/
private string $requestUri;
/**
* @var string
*/
private string $requestMethod;
/**
* @var array
*/
private array $requestHeaders;
/**
* @param Application $app
*/
public function __construct(Application $app)
{
$this->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);
}
}
}

4
public_html/.htaccess Normal file
View file

@ -0,0 +1,4 @@
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule . index.php [L]

55
public_html/LICENSE Normal file
View file

@ -0,0 +1,55 @@
# Blue Oak Model License
Version 1.0.0
## Purpose
This license gives everyone as much permission to work with
this software as possible, while protecting contributors
from liability.
## Acceptance
In order to receive this license, you must agree to its
rules. The rules of this license are both obligations
under that agreement and conditions to your license.
You must not do anything with this software that triggers
a rule that you cannot or will not follow.
## Copyright
Each contributor licenses you to do everything with this
software that would otherwise infringe that contributor's
copyright in it.
## Notices
You must ensure that everyone who gets a copy of
any part of this software from you, with or without
changes, also gets the text of this license or a link to
<https://blueoakcouncil.org/license/1.0.0>.
## Excuse
If anyone notifies you in writing that you have not
complied with [Notices](#notices), you can keep your
license by taking all practical steps to comply within 30
days after the notice. If you do not do so, your license
ends immediately.
## Patent
Each contributor licenses you to do everything with this
software that would otherwise infringe any patent claims
they can license or become able to license.
## Reliability
No contributor can revoke this license.
## No Liability
***As far as the law allows, this software comes as is,
without any warranty or condition, and no contributor
will be liable to anyone for any damages related to this
software or this license, under any kind of legal claim.***

View file

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 922 B

View file

@ -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;
@ -43,24 +43,25 @@ body {
.container {
position: absolute;
box-sizing: border-box;
left:0;
right:0;
height: 100%;
max-width: 600px;
padding: 0 24px;
margin:auto;
max-width: 640px;
padding: 0 10px;
margin: auto;
display:flex;
flex-direction: column;
flex-wrap: nowrap;
overflow: hidden;
background-color: var(--bg-color);
backdrop-filter: var(--bg-backdrop);
/*backdrop-filter: var(--bg-backdrop);*/
}
header {
display: flex;
margin: var(--spacing-large);
flex: 0 0 auto;
align-items: flex-end;
}
header img {
@ -70,15 +71,22 @@ header img {
}
header ul {
display: flex;
position: relative;
padding: 0;
height: 80px;
list-style: none;
font-size: 0;
flex: 1 1 auto;
text-align: right;
flex-wrap: wrap;
flex-direction: column;
justify-content: flex-end;
}
header ul li {
position: relative;
width: 50%;
flex: 0 0 auto;
}
header ul li a{
@ -86,7 +94,8 @@ header ul li a{
color: #fff;
font-size: 20px;
font-weight: normal;
line-height: 28px;
line-height: 36px;
text-align: center;
}
header ul li a:hover:before {
@ -95,16 +104,16 @@ header ul li a:hover:before {
main {
flex: 1 1 auto;
overflow: hidden;
padding: var(--spacing-large) 0 var(--spacing-large) var(--spacing-large);
}
main .inner-main {
max-height: 100%;
height: 488px;
padding: var(--spacing-large);
border: 32px solid transparent;
border-image: url('../border2x2.png') 32 fill round;
overflow-y: auto;
scrollbar-color: #fff transparent;
scrollbar-width: thin;
padding-right: var(--spacing-large);
}
main .inner-main {
text-align: justify;
}
@ -152,12 +161,12 @@ h2:before {
}
a {
color: #fff;
color: var(--link-color);
text-decoration: none;
}
a:hover {
color: var(--link-color);
color: #fff;
}
a:not(header a):before, a:not(header a):after { color: var(--link-color); }
@ -167,3 +176,12 @@ 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);
}

View file

@ -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);

View file

@ -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;
}));

View file

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

12
public_html/index.php Normal file
View file

@ -0,0 +1,12 @@
<?php
define('APP_START', microtime(true));
// Autoload classes
require __DIR__.'/../vendor/autoload.php';
// Boot application
$app = new \Subcon\Zap\Application(dirname(__DIR__));
//TODO: Console mode compatibility.
$app->run();

View file

@ -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!

View file

@ -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 &lt;filename&gt;" 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!

View file

@ -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.

View file

@ -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).

View file

@ -0,0 +1,41 @@
---
title: Service Updates
author: fristi
date: 2023-11
description: Hello villagers! Just a short update regarding some chances made on the server
recently. Upcoming will also be the upgrade to Fedora 39; if all goes well, we
should not be having any issues with this update.
---
# Service Updates
*November 2023, by Fristi*
Hello villagers! Just a short update regarding some chances made on the server
recently. Upcoming will also be the upgrade to Fedora 39; if all goes well, we
should not be having any issues with this update.
First off, I unfortunately had to shut down the *Funkwhale* service. This is mainly
due to technical issues in keeping the instance updated; it seems Funkwhale is just
not compatible enough with Fedora to keep it running without issues. As such, the
service is offline and will not be back for the foreseeable future. In the meantime
I will see if I can find a suitable alternative.
Additionally, the gogs service has been replaced with [Forgejo](https://forgejo.org/).
This is a fork of the popular (but now infamous) Gitea project that we used in the
past. I have decided to host this new instance on a different subdomain,
[forge.subcon.town](https://forge.subcon.town). The gogs service will no longer be
available.
Near the end of the month I will start migrating the server to *Fedora 39*. Preparations
have already been made, so that everything will hopefully go without any issues.
Fingers crossed.
Some more plans are currently in the works after the migration has been dealt with.
I've currently been testing a *web IRC client* that has been working very favorably.
Together with that, I've been planning to set up an *IRC server* of our own to pair
it with. To further boost the options we have for communication, I've also been
looking into hosting a *Matrix* server. However this one is still in the works and
will depend a lot on if *Conduit* or *Dendrite* end up working as advertised.
That is all for now, happy hacking!

View file

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php if(isset($title)) echo "{$title} - "; ?>Welcome to Subcon Town!</title>
<?php if(isset($meta) && is_array($meta)) {
foreach($meta as $key => $value) { ?>
<meta name="<?php echo $key; ?>" content="<?php echo $value; ?>">
<?php }} ?>
<link rel="shortcut icon" type="image/jpg" href="/assets/subcon/favicon_16.png" sizes="16x16"/>
<link rel="shortcut icon" type="image/jpg" href="/assets/subcon/favicon_32.png" sizes="32x32"/>
<link href="/assets/css/style.css?v=<?php echo \Subcon\Zap\Application::VERSION ?>" rel="stylesheet"/>
<script src="/assets/js/gogo.js" type="text/javascript"></script>
<script src="/assets/js/app.js?v=<?php echo \Subcon\Zap\Application::VERSION ?>" type="text/javascript"></script>
</head>
<body>
<div class="container">
<header>
<img src="/assets/subcon/logo_128.png" alt="Subcon Town" />
<ul>
<li><a class="page-link" href="/">Home</a></li>
<li><a target="_blank" href="https://library.subcon.town/">Wiki</a></li>
<li><a class="page-link" href="/services">Services</a></li>
<li><a class="page-link" href="/faq">FAQ</a></li>
</ul>
</header>
<main>
<div class="inner-main">
<div class="content">
<?php echo $content; ?>
</div>
</div>
</main>
<footer>
<div style="text-align: right">
© 2021-<?php echo date("Y"); ?> Subcon Town.
&nbsp;<a class="page-link" href="/copyright">License</a>
</div>
</footer>
</div>
</body>
</html>

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>404 - Not Found</title>
<link type="text/css" rel="stylesheet" href="https://cdn.subcon.town/system/page.css" />
</head>
<body>
<div class="container">
<div class="col">
<h1>404 - Not found!</h1>
<p>
Whoops! Looks like we don't have that thing you're looking for. Maybe it got
moved or deleted, we don't know. Sorry!
</p>
<p>
<a href="/">&lt;&lt; Back to the homepage</a>
</p>
</div>
<div class="col right">
<img style="max-height: 300px;" src="https://cdn.subcon.town/system/img/404.jpg" alt="Internet Explorer" />
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,25 @@
<h1>Copyright notice</h1>
<p>
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 <a target="_blank" href="./assets/subcon/LICENSE">license</a>.
</p>
<p>
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 <a target="_blank" href="./LICENSE">license</a>.
The source code of this website may be viewed <a target="_blank" href="https://factory.subcon.town/fristi/subcon.town">here</a>.
</p>
<h2>Third party works</h2>
<p>
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.
</p>
<h2>Complaints</h2>
<p>
If you feel any material is incorrectly used on this website, or otherwise infringes on your rights,
feel free to file a complaint with <a class="em" href="#">fristi「AT」subcon.town</a>. Please allow up
to 48 hours for a response.
</p>

View file

@ -0,0 +1,37 @@
<h1>FAQ</h1>
<p>
<strong>Q</strong>: <em>What is a tilde community?</em>
<br/>
<strong>A</strong>: Tilde communities are pubnixes, <i>public access unix systems</i>. We took our inspiration
from older such servers like <a target="_blank" href="http://tilde.club">tilde.club</a>
and the tildes from <a target="_blank" href="https://tildeverse.org">tildeverse.org</a>.
Put simply, a pubnix is a public server that is being shared by a number of people, that
use it simply for fun, or for purpose.
</p>
<p>
<strong>Q</strong>: <em>Why would I join a tilde community?</em>
<br/>
<strong>A</strong>: Good question! Ultimately that depends on you. Do you like
doing computer stuff? Do you like bonding with people over computers? Or do you
just like having a little space on the web to do stuff? Then you might like it.
</p>
<p>
<strong>Q</strong>: <em>Is Subcon Town open for new users?</em>
<br/>
<strong>A</strong>: Yes, but keep in mind we're an invite-only community. You can always ask
for an invite, and we don't mind giving people a chance, but we do like to run a tight ship.
</p>
<p>
<strong>Q</strong>: <em>Are there more open and public tilde communities?</em>
<br/>
<strong>A</strong>: Yes, many of the tilde communities we took inspiration from are open to new
users. For example, <a target="_blank" href="http://tilde.club">tilde.club</a> or some of the
communities on <a target="_blank" href="https://tildeverse.org">tildeverse.org</a> will readily
accept new members. Some communities are themed around a subject, so you might find one that fits you.
</p>
<p>
<strong>Q</strong>: <em>Can I get an account on one of your services?</em>
<br/>
<strong>A</strong>: Only as an existing member. If you're already a member, ask
<a target="_blank" href="https://akkos.fritu.re/fristi">@fristi</a> for an account.
</p>

View file

@ -0,0 +1,25 @@
<h1>Hello world!</h1>
<p>
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.
</p>
<p>
To find out more about this system and its services, view the <a class="page-link" href="/faq">faq</a> or visit
the <a target="_blank" href="https://library.subcon.town/">wiki</a>!
</p>
<hr />
<h1>News</h1>
<!-- List of article stubs -->
<?php foreach($articles as $article) { ?>
<h2><?php echo $article->getTitle(); ?></h2>
<div class="article-stub">
<p><?php echo $article->getDescription(); ?></p>
<div class="article-stub-end">
<a class="page-link" href="/news/<?php echo $article->getSlug(); ?>">Read more</a>
</div>
</div>
<?php } ?>

View file

View file

@ -0,0 +1,62 @@
<h1>Services</h1>
<p>
We provide the following services for our members:
</p>
<table>
<tbody>
<tr>
<td>Akkoma:</td>
<td><a title="A federated microblogging server powered by Akkoma." target="_blank" href="https://akkos.fritu.re">akkos.fritu.re</a></td>
</tr>
<tr>
<td>Fossil SCM:</td>
<td><a title="A Fossil repository hosting server, powered by... Fossil." target="_blank" href="https://museum.subcon.town">museum.subcon.town</a></td>
</tr>
<tr>
<td>Git:</td>
<td><a title="A Git respository hosting server powered by Forgejo." target="_blank" href="https://forge.subcon.town">forge.subcon.town</a></td>
</tr>
<tr>
<td>IRC:</td>
<td><a title="Our internet relay chat server powered by Ergo." target="_blank" href="https://cal.subcon.town/radicale">lounge.subcon.town</a></td>
</tr>
<tr>
<td>Matrix:</td>
<td><a title="A Matrix IM chat server powered by Conduit." target="_blank" href="https://cal.subcon.town/radicale">matrix.subcon.town</a></td>
</tr>
<tr>
<td>Nextcloud:</td>
<td><a title="A cloud file hosting server powered by Nextcloud." target="_blank" href="https://cloud.subcon.town">cloud.subcon.town</a></td>
</tr>
<tr>
<td>Radicale:</td>
<td><a title="A caldav and carddav hosting server powered by Radicale." target="_blank" href="https://cal.subcon.town/radicale">cal.subcon.town</a></td>
</tr>
<tr>
<td>Wiki:</td>
<td><a title="A wiki hosting server powered by DokuWiki." target="_blank" href="https://library.subcon.town">library.subcon.town</a></td>
</tr>
<tr>
<td>XMPP:</td>
<td><a title="A Jabber/XMPP IM chat server powered by Prosody." target="_blank" href="https://jab.subcon.town">jab.subcon.town</a></td>
</tr>
</tbody>
</table>
<p>
We also host member websites, gopher holes and gemini pods. User websites are listed below,
for gopher holes and gemini pods, visit <a target="_blank" href="gopher://subcon.town">gopher://subcon.town</a> and
<a target="_blank" href="gemini://subcon.town">gemini://subcon.town</a> respectively using a compatible browser.
</p>
<hr />
<h1>User tildes:</h1>
<table>
<tr>
<td width="50%"><a target="_blank" href="/~fristi">fristi</a></td>
<td width="50%"></td>
</tr>
</table>

15
routes/routes.php Normal file
View file

@ -0,0 +1,15 @@
<?php
use Subcon\Frontpage\Controllers\FrontpageController;
use Subcon\Zap\Routing\Repository;
return function (Repository $routes) {
//Frontpage routes
$routes->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']);
};

View file

@ -0,0 +1,106 @@
<?php
namespace Subcon\Frontpage\Controllers;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\FrontMatter\Data\SymfonyYamlFrontMatterParser;
use League\CommonMark\Extension\FrontMatter\FrontMatterExtension;
use League\CommonMark\Extension\FrontMatter\FrontMatterParser;
use League\CommonMark\MarkdownConverter;
use Subcon\Frontpage\Models\Article;
use Subcon\Zap\Application;
use Subcon\Zap\Http\Response;
class FrontpageController
{
private Application $app;
public function __construct()
{
$this->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);
}
}

259
src/Models/Article.php Normal file
View file

@ -0,0 +1,259 @@
<?php
namespace Subcon\Frontpage\Models;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\FrontMatter\Data\SymfonyYamlFrontMatterParser;
use League\CommonMark\Extension\FrontMatter\FrontMatterExtension;
use League\CommonMark\Extension\FrontMatter\FrontMatterParser;
use League\CommonMark\MarkdownConverter;
use League\CommonMark\Output\RenderedContentInterface;
use Subcon\Zap\Application;
class Article
{
/**
* @var string
*/
private string $path;
/**
* @var string
*/
private string $title;
/**
* @var int
*/
private int $timestamp;
/**
* @var string
*/
private string $description;
/**
* @var string
*/
private string $author;
/**
* @var RenderedContentInterface
*/
private RenderedContentInterface $render;
/**
* Initialize reference to main application instance.
* @param string $path Full path of the article this model will represent.
*/
public function __construct(string $path, array $cacheData = [])
{
$this->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 = '<?php return ' . var_export($cache, true) . ';' ;
file_put_contents($app->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);
}
}

View file

@ -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;
}