Compare commits
4 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
444d7ec3d2 | ||
|
|
7fb4dbd806 | ||
|
|
f217c8a446 | ||
|
|
a9995f8812 |
4
.gitignore
vendored
|
|
@ -1,2 +1,6 @@
|
|||
deploy.sh
|
||||
deploy-excludes
|
||||
composer.lock
|
||||
.idea
|
||||
vendor
|
||||
cache/*
|
||||
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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
//TODO: add CLI commands for handling certain tasks.
|
||||
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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,230 @@
|
|||
<?php
|
||||
|
||||
namespace Subcon\Zap\Routing;
|
||||
|
||||
use Subcon\Zap\Application;
|
||||
use Subcon\Zap\Http\Response;
|
||||
|
||||
class Router
|
||||
{
|
||||
/**
|
||||
* @var Application
|
||||
*/
|
||||
private Application $app;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private array $routes;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private string $requestUri;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private string $requestMethod;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private array $requestHeaders;
|
||||
|
||||
/**
|
||||
* @param Application $app
|
||||
*/
|
||||
public function __construct(Application $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
|
||||
//Load routes from cache, or fall back to loading from file
|
||||
if($this->loadFromCache(realpath($app->getBasePath('cache')))) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->loadFromRouteFile(realpath($app->getBasePath('routes')));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
//Parse request
|
||||
$this->requestUri = $this->getRequestUri();
|
||||
$this->requestMethod = $this->getRequestMethod();
|
||||
$this->requestHeaders ??= $this->getRequestHeaders();
|
||||
|
||||
//Find the first matching route. Only one route will ever be executed.
|
||||
$route = $this->findRoute($this->requestUri, $this->requestMethod);
|
||||
|
||||
//If route was found, run it. Otherwise return a 404 response.
|
||||
if($route) {
|
||||
$response = $this->handleRoute($route);
|
||||
} else {
|
||||
$response = $this->handleNotFoundResponse();
|
||||
}
|
||||
|
||||
//Send response.
|
||||
if (!is_a($response, Response::class)) {
|
||||
$response = new Response((string) $response);
|
||||
}
|
||||
$response->send();
|
||||
|
||||
//If the request was a HEAD request, empty the output buffer.
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'HEAD') {
|
||||
ob_end_clean();
|
||||
}
|
||||
}
|
||||
|
||||
public function buildCache()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the current relative URI.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getRequestUri(): string
|
||||
{
|
||||
$uri = rawurldecode($_SERVER['REQUEST_URI']);
|
||||
|
||||
// Don't take query params into account on the URL
|
||||
if (strstr($uri, '?')) {
|
||||
$uri = substr($uri, 0, strpos($uri, '?'));
|
||||
}
|
||||
|
||||
// Remove trailing slash + enforce a slash at the start
|
||||
return '/' . trim($uri, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all request headers.
|
||||
*
|
||||
* @return array The request headers
|
||||
*/
|
||||
protected function getRequestHeaders(): array
|
||||
{
|
||||
if(!empty($this->requestHeaders)) return $this->requestHeaders;
|
||||
|
||||
$headers = [];
|
||||
|
||||
// If getallheaders() is available, use that
|
||||
if (function_exists('getallheaders')) {
|
||||
$headers = getallheaders();
|
||||
|
||||
// getallheaders() can return false if something went wrong
|
||||
if ($headers !== false) {
|
||||
$this->requestHeaders = $headers;
|
||||
return $headers;
|
||||
}
|
||||
}
|
||||
|
||||
// Method getallheaders() not available or went wrong: manually extract them
|
||||
foreach ($_SERVER as $name => $value) {
|
||||
if ((substr($name, 0, 5) == 'HTTP_') || ($name == 'CONTENT_TYPE') || ($name == 'CONTENT_LENGTH')) {
|
||||
$headers[str_replace([' ', 'Http'], ['-', 'HTTP'], ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$this->requestHeaders = $headers;
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the request method used, taking overrides into account.
|
||||
*
|
||||
* @return string The request method to handle
|
||||
*/
|
||||
protected function getRequestMethod(): string
|
||||
{
|
||||
// Take the method as found in $_SERVER
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
// If it's a HEAD request override it to being GET and prevent any output, as per HTTP Specification
|
||||
// @url http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'HEAD') {
|
||||
ob_start();
|
||||
$method = 'GET';
|
||||
}
|
||||
|
||||
// If it's a POST request, check for a method override header
|
||||
elseif ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
$headers = $this->getRequestHeaders();
|
||||
if (isset($headers['X-HTTP-Method-Override']) && in_array($headers['X-HTTP-Method-Override'], array('PUT', 'DELETE', 'PATCH'))) {
|
||||
$method = $headers['X-HTTP-Method-Override'];
|
||||
}
|
||||
}
|
||||
|
||||
return $method;
|
||||
}
|
||||
|
||||
protected function loadFromCache(string $cachePath): bool
|
||||
{
|
||||
if(!file_exists($cachePath) || !file_exists($cachePath . DIRECTORY_SEPARATOR . 'route-cache.php')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->routes = require $cachePath . DIRECTORY_SEPARATOR . 'route-cache.php';
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function loadFromRouteFile(string $routePath): void
|
||||
{
|
||||
if(!file_exists($routePath) || !file_exists($routePath . DIRECTORY_SEPARATOR . 'routes.php')) {
|
||||
throw new \RuntimeException('Unable to initialize routes: route file not found.');
|
||||
}
|
||||
|
||||
$repository = new Repository();
|
||||
$routes = require $routePath . DIRECTORY_SEPARATOR . 'routes.php';
|
||||
|
||||
if(!is_callable($routes)) {
|
||||
throw new \RuntimeException('Unable to initialize routes: route file does not contain callable function.');
|
||||
}
|
||||
|
||||
//Initialize routes
|
||||
call_user_func($routes, $repository);
|
||||
$this->routes = $repository->getRoutes();
|
||||
}
|
||||
|
||||
protected function findRoute(string $path, string $method): ?Route
|
||||
{
|
||||
$method = strtoupper($method);
|
||||
|
||||
if(key_exists($method, $this->routes)) {
|
||||
foreach($this->routes[$method] as $route) {
|
||||
if($route->match($path, $method)) {
|
||||
return $route;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function handleRoute(Route $route)
|
||||
{
|
||||
//Todo: run route middleware and route handler.
|
||||
|
||||
return $route->run();
|
||||
}
|
||||
|
||||
protected function handleNotFoundResponse()
|
||||
{
|
||||
//Do we have an error template?
|
||||
$template = $this->app->getViewPath('errors/404.html');
|
||||
if(file_exists($template)) {
|
||||
ob_start();
|
||||
require $template;
|
||||
$response = ob_get_clean();
|
||||
|
||||
return new Response($response, 404);
|
||||
} else {
|
||||
return new Response('Whoops! Could not find that file.', 404);
|
||||
}
|
||||
}
|
||||
}
|
||||
4
public_html/.htaccess
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
RewriteEngine on
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule . index.php [L]
|
||||
55
public_html/LICENSE
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Blue Oak Model License
|
||||
|
||||
Version 1.0.0
|
||||
|
||||
## Purpose
|
||||
|
||||
This license gives everyone as much permission to work with
|
||||
this software as possible, while protecting contributors
|
||||
from liability.
|
||||
|
||||
## Acceptance
|
||||
|
||||
In order to receive this license, you must agree to its
|
||||
rules. The rules of this license are both obligations
|
||||
under that agreement and conditions to your license.
|
||||
You must not do anything with this software that triggers
|
||||
a rule that you cannot or will not follow.
|
||||
|
||||
## Copyright
|
||||
|
||||
Each contributor licenses you to do everything with this
|
||||
software that would otherwise infringe that contributor's
|
||||
copyright in it.
|
||||
|
||||
## Notices
|
||||
|
||||
You must ensure that everyone who gets a copy of
|
||||
any part of this software from you, with or without
|
||||
changes, also gets the text of this license or a link to
|
||||
<https://blueoakcouncil.org/license/1.0.0>.
|
||||
|
||||
## Excuse
|
||||
|
||||
If anyone notifies you in writing that you have not
|
||||
complied with [Notices](#notices), you can keep your
|
||||
license by taking all practical steps to comply within 30
|
||||
days after the notice. If you do not do so, your license
|
||||
ends immediately.
|
||||
|
||||
## Patent
|
||||
|
||||
Each contributor licenses you to do everything with this
|
||||
software that would otherwise infringe any patent claims
|
||||
they can license or become able to license.
|
||||
|
||||
## Reliability
|
||||
|
||||
No contributor can revoke this license.
|
||||
|
||||
## No Liability
|
||||
|
||||
***As far as the law allows, this software comes as is,
|
||||
without any warranty or condition, and no contributor
|
||||
will be liable to anyone for any damages related to this
|
||||
software or this license, under any kind of legal claim.***
|
||||
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 178 KiB |
BIN
public_html/assets/border.png
Normal file
|
After Width: | Height: | Size: 396 B |
BIN
public_html/assets/border2.png
Normal file
|
After Width: | Height: | Size: 545 B |
BIN
public_html/assets/border2x2.png
Normal file
|
After Width: | Height: | Size: 922 B |
|
|
@ -1,7 +1,7 @@
|
|||
@font-face {
|
||||
font-family: 'yoster_islandregular';
|
||||
src: url('assets/yoster-web.woff2') format('woff2'),
|
||||
url('assets/yoster-web.woff') format('woff');
|
||||
src: url('../fonts/yoster-web.woff2') format('woff2'),
|
||||
url('../fonts/yoster-web.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ html, body {
|
|||
}
|
||||
|
||||
body {
|
||||
background-image: url("assets/background3.png");
|
||||
background-image: url("../background3.png");
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
background-attachment: fixed;
|
||||
|
|
@ -43,24 +43,25 @@ body {
|
|||
|
||||
.container {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
left:0;
|
||||
right:0;
|
||||
height: 100%;
|
||||
max-width: 600px;
|
||||
padding: 0 24px;
|
||||
max-width: 640px;
|
||||
padding: 0 10px;
|
||||
margin: auto;
|
||||
display:flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
background-color: var(--bg-color);
|
||||
backdrop-filter: var(--bg-backdrop);
|
||||
/*backdrop-filter: var(--bg-backdrop);*/
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
margin: var(--spacing-large);
|
||||
flex: 0 0 auto;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
header img {
|
||||
|
|
@ -70,15 +71,22 @@ header img {
|
|||
}
|
||||
|
||||
header ul {
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
height: 80px;
|
||||
list-style: none;
|
||||
font-size: 0;
|
||||
flex: 1 1 auto;
|
||||
text-align: right;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
header ul li {
|
||||
position: relative;
|
||||
width: 50%;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
header ul li a{
|
||||
|
|
@ -86,7 +94,8 @@ header ul li a{
|
|||
color: #fff;
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
line-height: 28px;
|
||||
line-height: 36px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
header ul li a:hover:before {
|
||||
|
|
@ -95,16 +104,16 @@ header ul li a:hover:before {
|
|||
|
||||
main {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
padding: var(--spacing-large) 0 var(--spacing-large) var(--spacing-large);
|
||||
}
|
||||
|
||||
main .inner-main {
|
||||
max-height: 100%;
|
||||
height: 488px;
|
||||
padding: var(--spacing-large);
|
||||
border: 32px solid transparent;
|
||||
border-image: url('../border2x2.png') 32 fill round;
|
||||
overflow-y: auto;
|
||||
scrollbar-color: #fff transparent;
|
||||
scrollbar-width: thin;
|
||||
padding-right: var(--spacing-large);
|
||||
}
|
||||
|
||||
main .inner-main {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
|
|
@ -152,12 +161,12 @@ h2:before {
|
|||
}
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
color: var(--link-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--link-color);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
a:not(header a):before, a:not(header a):after { color: var(--link-color); }
|
||||
|
|
@ -167,3 +176,12 @@ a:not(header a):after { content:"]"; margin-left: 6px; }
|
|||
table tbody tr td:first-child {
|
||||
padding: 0 3em 0 0;
|
||||
}
|
||||
|
||||
.article-stub p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.article-stub-end {
|
||||
text-align: right;
|
||||
margin-bottom: var(--spacing-large);
|
||||
}
|
||||
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
|
|
@ -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;
|
||||
}));
|
||||
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.6 KiB |
12
public_html/index.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
define('APP_START', microtime(true));
|
||||
|
||||
// Autoload classes
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
// Boot application
|
||||
$app = new \Subcon\Zap\Application(dirname(__DIR__));
|
||||
|
||||
//TODO: Console mode compatibility.
|
||||
|
||||
$app->run();
|
||||
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
|
|
@ -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
|
|
@ -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).
|
||||
41
resources/articles/2023/service_updates_november.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
title: Service Updates
|
||||
author: fristi
|
||||
date: 2023-11
|
||||
description: Hello villagers! Just a short update regarding some chances made on the server
|
||||
recently. Upcoming will also be the upgrade to Fedora 39; if all goes well, we
|
||||
should not be having any issues with this update.
|
||||
---
|
||||
|
||||
# Service Updates
|
||||
|
||||
*November 2023, by Fristi*
|
||||
|
||||
Hello villagers! Just a short update regarding some chances made on the server
|
||||
recently. Upcoming will also be the upgrade to Fedora 39; if all goes well, we
|
||||
should not be having any issues with this update.
|
||||
|
||||
First off, I unfortunately had to shut down the *Funkwhale* service. This is mainly
|
||||
due to technical issues in keeping the instance updated; it seems Funkwhale is just
|
||||
not compatible enough with Fedora to keep it running without issues. As such, the
|
||||
service is offline and will not be back for the foreseeable future. In the meantime
|
||||
I will see if I can find a suitable alternative.
|
||||
|
||||
Additionally, the gogs service has been replaced with [Forgejo](https://forgejo.org/).
|
||||
This is a fork of the popular (but now infamous) Gitea project that we used in the
|
||||
past. I have decided to host this new instance on a different subdomain,
|
||||
[forge.subcon.town](https://forge.subcon.town). The gogs service will no longer be
|
||||
available.
|
||||
|
||||
Near the end of the month I will start migrating the server to *Fedora 39*. Preparations
|
||||
have already been made, so that everything will hopefully go without any issues.
|
||||
Fingers crossed.
|
||||
|
||||
Some more plans are currently in the works after the migration has been dealt with.
|
||||
I've currently been testing a *web IRC client* that has been working very favorably.
|
||||
Together with that, I've been planning to set up an *IRC server* of our own to pair
|
||||
it with. To further boost the options we have for communication, I've also been
|
||||
looking into hosting a *Matrix* server. However this one is still in the works and
|
||||
will depend a lot on if *Conduit* or *Dendrite* end up working as advertised.
|
||||
|
||||
That is all for now, happy hacking!
|
||||
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
|
|
@ -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
|
|
@ -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>
|
||||
37
resources/views/pages/faq.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<h1>FAQ</h1>
|
||||
<p>
|
||||
<strong>Q</strong>: <em>What is a tilde community?</em>
|
||||
<br/>
|
||||
<strong>A</strong>: Tilde communities are pubnixes, <i>public access unix systems</i>. We took our inspiration
|
||||
from older such servers like <a target="_blank" href="http://tilde.club">tilde.club</a>
|
||||
and the tildes from <a target="_blank" href="https://tildeverse.org">tildeverse.org</a>.
|
||||
Put simply, a pubnix is a public server that is being shared by a number of people, that
|
||||
use it simply for fun, or for purpose.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Q</strong>: <em>Why would I join a tilde community?</em>
|
||||
<br/>
|
||||
<strong>A</strong>: Good question! Ultimately that depends on you. Do you like
|
||||
doing computer stuff? Do you like bonding with people over computers? Or do you
|
||||
just like having a little space on the web to do stuff? Then you might like it.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Q</strong>: <em>Is Subcon Town open for new users?</em>
|
||||
<br/>
|
||||
<strong>A</strong>: Yes, but keep in mind we're an invite-only community. You can always ask
|
||||
for an invite, and we don't mind giving people a chance, but we do like to run a tight ship.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Q</strong>: <em>Are there more open and public tilde communities?</em>
|
||||
<br/>
|
||||
<strong>A</strong>: Yes, many of the tilde communities we took inspiration from are open to new
|
||||
users. For example, <a target="_blank" href="http://tilde.club">tilde.club</a> or some of the
|
||||
communities on <a target="_blank" href="https://tildeverse.org">tildeverse.org</a> will readily
|
||||
accept new members. Some communities are themed around a subject, so you might find one that fits you.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Q</strong>: <em>Can I get an account on one of your services?</em>
|
||||
<br/>
|
||||
<strong>A</strong>: Only as an existing member. If you're already a member, ask
|
||||
<a target="_blank" href="https://akkos.fritu.re/fristi">@fristi</a> for an account.
|
||||
</p>
|
||||
25
resources/views/pages/index.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<h1>Hello world!</h1>
|
||||
<p>
|
||||
Welcome to Subcon Town! We are a small tilde community, hosted for friends and family.
|
||||
We offer SSH access, hosting of websites, gopher holes and gemini pods, alongside
|
||||
other services like cloud services, git and fossil hosting and an activitypub server.
|
||||
</p>
|
||||
<p>
|
||||
To find out more about this system and its services, view the <a class="page-link" href="/faq">faq</a> or visit
|
||||
the <a target="_blank" href="https://library.subcon.town/">wiki</a>!
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<h1>News</h1>
|
||||
<!-- List of article stubs -->
|
||||
<?php foreach($articles as $article) { ?>
|
||||
<h2><?php echo $article->getTitle(); ?></h2>
|
||||
<div class="article-stub">
|
||||
<p><?php echo $article->getDescription(); ?></p>
|
||||
|
||||
<div class="article-stub-end">
|
||||
<a class="page-link" href="/news/<?php echo $article->getSlug(); ?>">Read more</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
0
resources/views/pages/news.php
Normal file
62
resources/views/pages/services.php
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<h1>Services</h1>
|
||||
|
||||
<p>
|
||||
We provide the following services for our members:
|
||||
</p>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Akkoma:</td>
|
||||
<td><a title="A federated microblogging server powered by Akkoma." target="_blank" href="https://akkos.fritu.re">akkos.fritu.re</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Fossil SCM:</td>
|
||||
<td><a title="A Fossil repository hosting server, powered by... Fossil." target="_blank" href="https://museum.subcon.town">museum.subcon.town</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Git:</td>
|
||||
<td><a title="A Git respository hosting server powered by Forgejo." target="_blank" href="https://forge.subcon.town">forge.subcon.town</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IRC:</td>
|
||||
<td><a title="Our internet relay chat server powered by Ergo." target="_blank" href="https://cal.subcon.town/radicale">lounge.subcon.town</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Matrix:</td>
|
||||
<td><a title="A Matrix IM chat server powered by Conduit." target="_blank" href="https://cal.subcon.town/radicale">matrix.subcon.town</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Nextcloud:</td>
|
||||
<td><a title="A cloud file hosting server powered by Nextcloud." target="_blank" href="https://cloud.subcon.town">cloud.subcon.town</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Radicale:</td>
|
||||
<td><a title="A caldav and carddav hosting server powered by Radicale." target="_blank" href="https://cal.subcon.town/radicale">cal.subcon.town</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Wiki:</td>
|
||||
<td><a title="A wiki hosting server powered by DokuWiki." target="_blank" href="https://library.subcon.town">library.subcon.town</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>XMPP:</td>
|
||||
<td><a title="A Jabber/XMPP IM chat server powered by Prosody." target="_blank" href="https://jab.subcon.town">jab.subcon.town</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>
|
||||
We also host member websites, gopher holes and gemini pods. User websites are listed below,
|
||||
for gopher holes and gemini pods, visit <a target="_blank" href="gopher://subcon.town">gopher://subcon.town</a> and
|
||||
<a target="_blank" href="gemini://subcon.town">gemini://subcon.town</a> respectively using a compatible browser.
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<h1>User tildes:</h1>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%"><a target="_blank" href="/~fristi">fristi</a></td>
|
||||
<td width="50%"></td>
|
||||
</tr>
|
||||
</table>
|
||||
15
routes/routes.php
Normal file
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||