Updated website to now use PHP. All content has been divided into pages, articles are now generated from markdown, index page will contain a list of the latest news articles.
This commit is contained in:
parent
a9995f8812
commit
f217c8a446
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -1,2 +1,6 @@
|
||||||
deploy.sh
|
deploy.sh
|
||||||
deploy-excludes
|
deploy-excludes
|
||||||
|
composer.lock
|
||||||
|
.idea
|
||||||
|
vendor
|
||||||
|
cache/*
|
||||||
27
401.html
27
401.html
|
|
@ -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>
|
|
||||||
27
404.html
27
404.html
|
|
@ -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>
|
|
||||||
27
410.html
27
410.html
|
|
@ -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>
|
|
||||||
31
500.html
31
500.html
|
|
@ -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>
|
|
||||||
31
502.html
31
502.html
|
|
@ -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>
|
|
||||||
26
README.md
26
README.md
|
|
@ -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
3
cmd.php
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
//TODO: add CLI commands for handling certain tasks.
|
||||||
23
composer.json
Normal file
23
composer.json
Normal 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
27
config/application.php
Normal 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'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
290
index.html
290
index.html
|
|
@ -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 <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 <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.
|
|
||||||
<a href="#copyright">License</a>
|
|
||||||
<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
106
libs/zap/Application.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
66
libs/zap/Config/Repository.php
Normal file
66
libs/zap/Config/Repository.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
libs/zap/ErrorHandling/Handler.php
Normal file
31
libs/zap/ErrorHandling/Handler.php
Normal 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
235
libs/zap/Http/Response.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
82
libs/zap/Routing/Repository.php
Normal file
82
libs/zap/Routing/Repository.php
Normal 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
190
libs/zap/Routing/Route.php
Normal 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
230
libs/zap/Routing/Router.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
<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>
|
|
||||||
4
public_html/.htaccess
Normal file
4
public_html/.htaccess
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
RewriteEngine on
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteRule . index.php [L]
|
||||||
BIN
public_html/assets/border.png
Normal file
BIN
public_html/assets/border.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 396 B |
BIN
public_html/assets/border2.png
Normal file
BIN
public_html/assets/border2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 545 B |
|
|
@ -1,7 +1,7 @@
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'yoster_islandregular';
|
font-family: 'yoster_islandregular';
|
||||||
src: url('assets/yoster-web.woff2') format('woff2'),
|
src: url('../fonts/yoster-web.woff2') format('woff2'),
|
||||||
url('assets/yoster-web.woff') format('woff');
|
url('../fonts/yoster-web.woff') format('woff');
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
@ -29,7 +29,7 @@ html, body {
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-image: url("assets/background3.png");
|
background-image: url("../background3.png");
|
||||||
background-position: center center;
|
background-position: center center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
|
|
@ -45,8 +45,7 @@ body {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left:0;
|
left:0;
|
||||||
right:0;
|
right:0;
|
||||||
/*height: 100%;*/
|
height: 100%;
|
||||||
max-height: 100%;
|
|
||||||
max-width: 640px;
|
max-width: 640px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
|
@ -105,9 +104,10 @@ header ul li a:hover:before {
|
||||||
|
|
||||||
main {
|
main {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
height: 488px;
|
||||||
padding: var(--spacing-large);
|
padding: var(--spacing-large);
|
||||||
border: 32px solid transparent;
|
border: 32px solid transparent;
|
||||||
border-image: url('assets/border2x2.png') 32 fill round;
|
border-image: url('../border2x2.png') 32 fill round;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
scrollbar-color: #fff transparent;
|
scrollbar-color: #fff transparent;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
|
|
@ -175,4 +175,13 @@ a:not(header a):after { content:"]"; margin-left: 6px; }
|
||||||
|
|
||||||
table tbody tr td:first-child {
|
table tbody tr td:first-child {
|
||||||
padding: 0 3em 0 0;
|
padding: 0 3em 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-stub p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-stub-end {
|
||||||
|
text-align: right;
|
||||||
|
margin-bottom: var(--spacing-large);
|
||||||
}
|
}
|
||||||
66
public_html/assets/js/app.js
Normal file
66
public_html/assets/js/app.js
Normal 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);
|
||||||
91
public_html/assets/js/gogo.js
Normal file
91
public_html/assets/js/gogo.js
Normal 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;
|
||||||
|
}));
|
||||||
|
|
@ -1,218 +1,12 @@
|
||||||
<?php
|
<?php
|
||||||
$meta = [];
|
define('APP_START', microtime(true));
|
||||||
$content = '';
|
|
||||||
|
|
||||||
//Router
|
// Autoload classes
|
||||||
|
require __DIR__.'/../vendor/autoload.php';
|
||||||
|
|
||||||
|
// Boot application
|
||||||
|
$app = new \Subcon\Zap\Application(dirname(__DIR__));
|
||||||
|
|
||||||
//Return as ajax or as page?
|
//TODO: Console mode compatibility.
|
||||||
|
|
||||||
|
$app->run();
|
||||||
?>
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Subcon.town - Happy Dreams!</title>
|
|
||||||
|
|
||||||
<?php foreach($meta as $name => $content) { ?>
|
|
||||||
<meta name="<?php echo $name; ?>" content="<?php echo $content; ?>">
|
|
||||||
<?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="style.css?v=2.0.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 class="menu-item" href="#home">Home</a></li>
|
|
||||||
<li><a class="menu-item" href="#news">News</a></li>
|
|
||||||
<li><a class="menu-item" href="#services">Services</a></li>
|
|
||||||
<li><a class="menu-item" href="#faq">FAQ</a></li>
|
|
||||||
</ul>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<div class="inner-main">
|
|
||||||
<?php echo $content; ?>
|
|
||||||
<div class="content" id="home">
|
|
||||||
<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>
|
|
||||||
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>.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Users may find out more about this system by visiting the <a target="_blank" href="https://library.subcon.town/">wiki</a>.
|
|
||||||
Keep in mind that the wiki is still a work in progress.
|
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
<div class="content" id="services">
|
|
||||||
<h1>Services</h1>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
We provide the following services for our members:
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<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>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>
|
|
||||||
We also host member websites, gopher holes and gemini pods. User websites are listed below,
|
|
||||||
for gopher holes and gemini pods, visit <a href="gopher://subcon.town">gopher://subcon.town</a> and
|
|
||||||
<a href="gemini://subcon.town">gemini://subcon.town</a> respectively using a compatible browser.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<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>You're running on the Odroid H3 now, right?</em>
|
|
||||||
<br/>
|
|
||||||
<strong>A</strong>: Yes, all Subcon services have been moved to the new hardware
|
|
||||||
and are fully functional.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Q</strong>: <em>Is Subcon Town open for new users?</em>
|
|
||||||
<br/>
|
|
||||||
<strong>A</strong>: 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.
|
|
||||||
</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 href="https://akkos.fritu.re/fristi">@fristi</a> for an account.
|
|
||||||
</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: center;">
|
|
||||||
© 2021-2023 Subcon Town.
|
|
||||||
<a class="menu-item" href="#copyright">License</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- load email addresses -->
|
|
||||||
<script>
|
|
||||||
showAddresses();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
30
resources/articles/2022/introducing_funkwhale.md
Normal file
30
resources/articles/2022/introducing_funkwhale.md
Normal 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!
|
||||||
|
|
@ -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!
|
||||||
41
resources/articles/2023/frontpage_updates_and_more.md
Normal file
41
resources/articles/2023/frontpage_updates_and_more.md
Normal 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.
|
||||||
42
resources/articles/2023/plans_for_the_year.md
Normal file
42
resources/articles/2023/plans_for_the_year.md
Normal 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).
|
||||||
49
resources/views/_layout/layout.php
Normal file
49
resources/views/_layout/layout.php
Normal 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.
|
||||||
|
<a class="page-link" href="/copyright">License</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
25
resources/views/errors/404.html
Normal file
25
resources/views/errors/404.html
Normal 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="/"><< 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>
|
||||||
25
resources/views/pages/copyright.php
Normal file
25
resources/views/pages/copyright.php
Normal 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>
|
||||||
25
resources/views/pages/faq.php
Normal file
25
resources/views/pages/faq.php
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<h1>FAQ</h1>
|
||||||
|
<p>
|
||||||
|
<strong>Q</strong>: <em>You're running on the Odroid H3 now, right?</em>
|
||||||
|
<br/>
|
||||||
|
<strong>A</strong>: Yes, all Subcon services have been moved to the new hardware
|
||||||
|
and are fully functional.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Q</strong>: <em>Is Subcon Town open for new users?</em>
|
||||||
|
<br/>
|
||||||
|
<strong>A</strong>: 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.
|
||||||
|
</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>
|
||||||
|
<p>
|
||||||
|
<strong>Q</strong>: <em>Is that the Yoshi's Island font?</em>
|
||||||
|
<br/>
|
||||||
|
<strong>A</strong>: Yes. It was good game.
|
||||||
|
</p>
|
||||||
30
resources/views/pages/index.php
Normal file
30
resources/views/pages/index.php
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
<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>
|
||||||
|
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>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Users may find out more about this system by visiting the <a target="_blank" href="https://library.subcon.town/">wiki</a>.
|
||||||
|
Keep in mind that the wiki is still a work in progress.
|
||||||
|
</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 } ?>
|
||||||
0
resources/views/pages/news.php
Normal file
0
resources/views/pages/news.php
Normal file
34
resources/views/pages/services.php
Normal file
34
resources/views/pages/services.php
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<h1>Services</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
We provide the following services for our members:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr> <td>Akkoma:</td> <td><a title="Our microblogging service." target="_blank" href="https://akkos.fritu.re">akkos.fritu.re</a></td> </tr>
|
||||||
|
<tr> <td>Doku wiki:</td> <td><a title="Our wiki hosting service." target="_blank" href="https://library.subcon.town">library.subcon.town</a></td> </tr>
|
||||||
|
<tr> <td>Funkwhale:</td> <td><a title="Our music hosting instance." target="_blank" href="https://odeon.subcon.town">odeon.subcon.town</a></td> </tr>
|
||||||
|
<tr> <td>Gogs:</td> <td><a title="Our git respository hosting service." target="_blank" href="https://factory.subcon.town">factory.subcon.town</a></td> </tr>
|
||||||
|
<tr> <td>Fossil SCM:</td> <td><a title="Our fossil repository hosting service." target="_blank" href="https://museum.subcon.town">museum.subcon.town</a></td> </tr>
|
||||||
|
<tr> <td>Nextcloud:</td> <td><a title="Our cloud file hosting and syncing service." target="_blank" href="https://cloud.subcon.town">cloud.subcon.town</a></td> </tr>
|
||||||
|
<tr> <td>Radicale:</td> <td><a title="Our caldav and carddav hosting service." target="_blank" href="https://cal.subcon.town/radicale">cal.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
15
routes/routes.php
Normal 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']);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
106
src/Controllers/FrontpageController.php
Normal file
106
src/Controllers/FrontpageController.php
Normal 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
259
src/Models/Article.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue