First draft of main+test website
This commit is contained in:
1 parent
74c735ef26
commit
80783a4022
30 files changed
+1815
-636
No files matched your search
@@ -0,0 +1,5 @@
|
||||
# Copy to ../.secrets, one directory above the web root (true-og/.secrets on the server), so nginx can never serve it. One key=value per line.
|
||||
# The Discord webhook the application forms post to. The forms show as closed until it is set.
|
||||
discord_webhook=https://discord.com/api/webhooks/<id>/<token>
|
||||
# Where applications are appended while Discord is down. Optional: the default is ../databases/applications.txt next to this file. Must be writable by PHP; stops at 5 MB.
|
||||
# applications_file=/srv/true-og/databases/applications.txt
|
||||
@@ -1,9 +1,12 @@
|
||||
# TrueOG Website
|
||||
# TrueOG Network Website
|
||||
|
||||
This is the primary git repository representing the live state on [TrueOG.net](https://true-og.net/).
|
||||
Source of [true-og.net](https://true-og.net/). Plain PHP, HTML5, and CSS. No JavaScript.
|
||||
|
||||
The code for the website is licensed under the permissive [Tutorialzine License](https://github.com/true-og/website/blob/main/LICENSE).
|
||||
- `index.php` is the front page, `site.php` everything else, `stylesheet.css` the styles.
|
||||
- Runs on PHP 8.0+ under nginx, behind Cloudflare's orange proxy.
|
||||
- The fillable forms show as closed if dependencies are missing.
|
||||
- Secrets: copy `.secrets.template` to `../.secrets`, `discord_webhook` is for the form notifications; `applications_file` `../databases/applications.txt` catches them if Discord is down.
|
||||
- Staff Heads come from mc-heads.net through `/head/?u=<uuid>`, cached for a day.
|
||||
- Dependencies: [Font Awesome Free](https://fontawesome.com/) 7.3.1 (`Font-Awesome` submodule), [Monocraft](https://github.com/IdreesInc/Monocraft) 4.2 (`assets/fonts/`), and the 2016 [Tutorialzine flexbox template](https://tutorialzine.com/2016/06/freebie-landing-page-template-with-flexbox/) the layout grew from.
|
||||
|
||||
The font is licensed under [MIT](https://github.com/true-og/website/blob/788330a55a16471a80782750419444a46fe2b938/assets/fonts/LICENSE).
|
||||
|
||||
All other assets, including TrueOG's logos, and the TrueOG Staff Panel, are licensed under [The Unlicense](https://github.com/true-og/website/blob/main/assets/images/LICENSE).
|
||||
Licenses: `index.php` and `stylesheet.css` [Tutorialzine](LICENSE); `site.php` and the stubs [Unlicense](https://unlicense.org/); Monocraft [OFL 1.1](assets/fonts/Monocraft/LICENSE); Font Awesome [its own](Font-Awesome/LICENSE.txt); all other assets [Unlicense](assets/images/LICENSE).
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
/* Linked from Rule 1. */
|
||||
header('Location: https://appeal.trueog.net/', true, 302);
|
||||
@@ -1,23 +1,15 @@
|
||||
Copyright (c) 2023 ggbot.net
|
||||
|
||||
Copyright (c) 2022, Idrees Hassan (https://github.com/IdreesInc/Monocraft)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https\://openfontlicense.org
|
||||
|
||||
|
||||
\----------------------------------------------------------------------
|
||||
|
||||
#### SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
|
||||
\----------------------------------------------------------------------
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
-----------
|
||||
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
@@ -34,8 +26,6 @@ requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
-----------
|
||||
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
@@ -55,8 +45,6 @@ new environment.
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
-----------
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
@@ -90,14 +78,10 @@ remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
-----------
|
||||
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
-----------
|
||||
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
@@ -107,4 +91,3 @@ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square70x70logo src="favicon-70.png"/>
|
||||
<square150x150logo src="favicon-150.png"/>
|
||||
<TileColor>#2b5797</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 KiB |
Symlink
+1
@@ -0,0 +1 @@
|
||||
assets/images/bad-shop.png
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
require dirname(__DIR__) . '/site.php';
|
||||
head_image(is_string($_GET['u'] ?? null) ? $_GET['u'] : '');
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
require dirname(__DIR__) . '/site.php';
|
||||
policy_page('impressum');
|
||||
@@ -1,195 +1,211 @@
|
||||
<!-- Define file as HTML for browser. -->
|
||||
<!DOCTYPE html>
|
||||
<!-- Declare native language. -->
|
||||
<html lang="en">
|
||||
<!-- Declare Head for metadata. -->
|
||||
<head>
|
||||
|
||||
<!-- Set website character set and dimensions so that it displays correctly. -->
|
||||
<meta charset="UTF-8">
|
||||
<!-- Inform old browsers that the site is supported. -->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!-- Scale the site to the screen size of the device viewing it. -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<!-- Link to favicons for thumbnails in browsers. -->
|
||||
<!-- Generic: -->
|
||||
<link rel="icon" type="image/x-icon" href="assets/images/favicons/favicon.ico">
|
||||
<link rel="icon" href="assets/images/favicons/favicon-32.png" sizes="32x32">
|
||||
<link rel="icon" href="assets/images/favicons/favicon-76.png" sizes="76x76">
|
||||
<link rel="icon" href="assets/images/favicons/favicon-96.png" sizes="96x96">
|
||||
<link rel="icon" href="assets/images/favicons/favicon-128.png" sizes="128x128">
|
||||
<!-- Android: -->
|
||||
<link rel="shortcut icon" sizes="196x196" href="assets/images/favicons/favicon-196.png">
|
||||
<!-- iOS: -->
|
||||
<link rel="apple-touch-icon" href="assets/images/favicons/favicon-180.png" sizes="180x180">
|
||||
<!-- Windows 8 IE 10: -->
|
||||
<meta name="msapplication-TileColor" content="#FFFFFF">
|
||||
<meta name="msapplication-TileImage" content="assets/images/favicons/favicon-144.png">
|
||||
<!-- Windows 8.1 + IE11 and above: -->
|
||||
<meta name="msapplication-config" content="assets/images/favicons/browserconfig.xml">
|
||||
|
||||
<!-- Declare Page Title. -->
|
||||
<title>TrueOG</title>
|
||||
|
||||
<!-- Stylesheet URLs carry the file's modification time (?v=...), so a deploy that changes a
|
||||
stylesheet gives it a new URL and Cloudflare and browsers fetch it at once instead of
|
||||
serving a cached copy for hours. -->
|
||||
|
||||
<!-- Import Font Awesome 7 -->
|
||||
<link rel="stylesheet" href="Font-Awesome/css/all.min.css?v=<?php echo @filemtime(__DIR__ . '/Font-Awesome/css/all.min.css'); ?>">
|
||||
|
||||
<!-- Import CSS -->
|
||||
<link rel="stylesheet" href="stylesheet.css?v=<?php echo @filemtime(__DIR__ . '/stylesheet.css'); ?>">
|
||||
|
||||
<!-- Close Head. -->
|
||||
</head>
|
||||
|
||||
<!-- Declare Body. -->
|
||||
<body>
|
||||
|
||||
<!-- Declare Header. -->
|
||||
<header>
|
||||
|
||||
<!-- Display TrueOG logo in top left of navbar. -->
|
||||
<a href="https://www.true-og.net/" target="_blank">
|
||||
<img src="assets/images/logos/trueog-logo-2026-transparent.png" class="main-logo" alt="Logo"></a>
|
||||
|
||||
<!-- Declare navigation menu. -->
|
||||
<nav class="outlined">
|
||||
|
||||
<!-- Declare the list of navigation buttons. -->
|
||||
<ul>
|
||||
|
||||
<!-- Declare navigation button for Games. -->
|
||||
<li><a href="#smp"> <i class="fa-solid fa-gamepad bukkit-gold fa-width-auto" aria-hidden="true"></i> <strong>Games</strong></a></li>
|
||||
<!-- Declare navigation button for Shop. -->
|
||||
<li><a href="#shop"> <i class="fa-solid fa-bag-shopping bukkit-red fa-width-auto" aria-hidden="true"></i> <strong>Shop</strong></a></li>
|
||||
<!-- Declare navigation button for Team. -->
|
||||
<li><a href="#team"> <i class="fa-solid fa-users bukkit-dark-purple fa-width-auto" aria-hidden="true"></i> <strong>Team</strong></a></li>
|
||||
<!-- Declare navigation button for FAQ. -->
|
||||
<li><a href="#faq"> <i class="fa-solid fa-circle-question bukkit-darkgreen fa-width-auto" aria-hidden="true"></i> <strong>FAQ</strong></a></li>
|
||||
|
||||
<!-- Close the list of navigation buttons. -->
|
||||
</ul>
|
||||
|
||||
<!-- Close navigation menu. -->
|
||||
</nav>
|
||||
|
||||
<!-- Close Header. -->
|
||||
</header>
|
||||
|
||||
<!-- Keep the menu on one row beside the logo whenever it fits; otherwise stack the logo above
|
||||
a centered menu. Runs before first paint, again when fonts load, and on resize. -->
|
||||
<script>
|
||||
(function () {
|
||||
|
||||
var header = document.querySelector("header");
|
||||
var list = header.querySelector("nav ul");
|
||||
|
||||
function layout() {
|
||||
|
||||
header.classList.remove("stacked");
|
||||
list.style.flexWrap = "nowrap";
|
||||
var fits = header.scrollWidth <= header.clientWidth;
|
||||
list.style.flexWrap = "";
|
||||
header.classList.toggle("stacked", !fits);
|
||||
|
||||
// A stacked header is taller; let the banner below keep its text clear of it.
|
||||
var bottom = header.querySelector("nav").getBoundingClientRect().bottom + window.scrollY;
|
||||
document.documentElement.style.setProperty("--menu-bottom", bottom + "px");
|
||||
|
||||
}
|
||||
|
||||
layout();
|
||||
window.addEventListener("resize", layout);
|
||||
if (document.fonts) document.fonts.ready.then(layout);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- TODO: Refactor/Comment below this line -->
|
||||
<div class="banner">
|
||||
<?php
|
||||
require __DIR__ . '/site.php';
|
||||
page_start('', 'banner');
|
||||
?>
|
||||
<!-- Full-screen banner: galaxy background behind the header, tagline centered below it. -->
|
||||
<div class="background-image" style="background-image: url(assets/images/galaxy-background.png);"></div>
|
||||
<p class="bukkit-green">The <a href="https://github.com/true-og/true-og/">Open Source</a>,</p>
|
||||
<p class="bukkit-blue">Community-First,</p>
|
||||
<p><span class="bukkit-red"><u>OG</u></span> <span class="bukkit-gold">Minecraft Experience!</span></p>
|
||||
|
||||
<a class="discord-button" href="https://discord.gg/ma9pMYpBU6"><i class="fa-brands fa-discord fa-width-auto" aria-hidden="true"></i> JOIN DISCORD</a>
|
||||
<div class="tagline">
|
||||
<p class="bukkit-green">The <a href="https://github.com/true-og/true-og/">Open Source</a>,</p>
|
||||
<p class="bukkit-blue">Community-First,</p>
|
||||
<p><span class="bukkit-red"><u>OG</u></span> <span class="bukkit-gold">Minecraft Experience!</span></p>
|
||||
<a class="discord-button" href="https://discord.gg/ma9pMYpBU6"><i class="fa-brands fa-discord fa-width-auto" aria-hidden="true"></i> JOIN DISCORD</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Games: the SMP. The first row introduces it; each row after pairs a screenshot with a topic. -->
|
||||
<div class="games" id="smp">
|
||||
<div class="box">
|
||||
<div class="flex-child grid outlined">
|
||||
<h1 style="color: white; text-align: center"><i class="fa-solid fa-cubes bukkit-darkgreen fa-width-auto" aria-hidden="true"></i> The <span class="bukkit-green">True</span><span class="bukkit-red">OG</span> SMP:</h1>
|
||||
<p style="color: white; font-size: 2.5vh; padding: 0vh"><br>Our biggest and best gamemode, the SMP is where it all began. <span class="bukkit-green">True</span><span class="bukkit-red">OG</span>'s founders were once players on a server which announced a map reset and a general shift away from the "OG" gameplay meta.<br><br>We revolted against their bad choices by buying the map, splitting from the original server, and making the worlds into our own. We rebuilt the old SMP's features from the ground up, fixing many bugs, opening up the code to the community, and enhancing the experience wherever possible.<br><br>Our SMP has grown into its own, preserving the incredible history of the original worlds, enabling a unique, yet familiar meta that evokes memories of 2011-2013 community-run SMP servers.</p>
|
||||
<div class="flex-child outlined">
|
||||
<h1><i class="fa-solid fa-cubes bukkit-darkgreen fa-width-auto" aria-hidden="true"></i> The <span class="bukkit-green">True</span><span class="bukkit-red">OG</span> SMP:</h1>
|
||||
<p><br>Our biggest and best gamemode, the SMP is where it all began. <span class="bukkit-green">True</span><span class="bukkit-red">OG</span>'s founders were once players on a server which announced a map reset and a general shift away from the "OG" gameplay meta.<br><br>We revolted against their bad choices by buying the map, splitting from the original server, and making the worlds into our own. We rebuilt the old SMP's features from the ground up, fixing many bugs, opening up the code to the community, and enhancing the experience wherever possible.<br><br>Our SMP has grown into its own, preserving the incredible history of the original worlds, enabling a unique, yet familiar meta that evokes memories of 2011-2013 community-run SMP servers.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="flex-child large grid zoomedout roundedcorners" style="filter: drop-shadow(0 0 0.3rem #FFAA00); background-image: url(assets/images/smp-banner-two.png)"></div>
|
||||
<div class="flex-child small grid splitter outlined">
|
||||
<h2 style="color: white; text-align: center"><strong class="bukkit-gold">The "OG" Meta:</strong></h2>
|
||||
<p class="bukkit-gold" style="font-size: 2.5vh"><br>OG means more to us than "not being pay-to-win". OG is about maintaining and building upon what we loved about SMP servers back in the day. A simple, robust economy. Competative, fast-paced PvP. Worlds that become richer and more fun to explore as the versions go by, giving players a sense of deep time and community. The <span class="bukkit-green">True</span><span class="bukkit-red">OG</span> Meta is more "OG" than OG:SMP and more anarchistic than anarchy. Here, you are truly free to play however you want, on a fair playing field. No bots. No hacks. There's nothing quite like it in Minecraft, or any other game for that matter.</p>
|
||||
<div class="flex-child large zoomedout roundedcorners" style="filter: drop-shadow(0 0 0.3rem #FFAA00); background-image: url(assets/images/smp-banner-two.png)"></div>
|
||||
<div class="flex-child small splitter outlined">
|
||||
<h2><strong class="bukkit-gold">The "OG" Meta:</strong></h2>
|
||||
<p class="bukkit-gold"><br>OG means more to us than "not being pay-to-win". OG is about maintaining and building upon what we loved about SMP servers back in the day. A simple, robust economy. Competative, fast-paced PvP. Worlds that become richer and more fun to explore as the versions go by, giving players a sense of deep time and community. The <span class="bukkit-green">True</span><span class="bukkit-red">OG</span> Meta is more "OG" than OG:SMP and more anarchistic than anarchy. Here, you are truly free to play however you want, on a fair playing field. No bots. No hacks. There's nothing quite like it in Minecraft, or any other game for that matter.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="flex-child small grid splitter bukkit-red outlined">
|
||||
<h2 style="text-align: center"><strong>Great on 1.8:</strong></h2>
|
||||
<p style="font-size: 2.5vh"><br>Unlike some other servers, we don't believe in leaving our loyal players in the dust with no support. There are good reasons many people continue to use Minecraft 1.8. From performance, to combat, to mods. If you are one of them, welcome home! Here you will experience being treated like a first class citizen. We have working fishing rods, ladders, lily pads, and brewing stands. All potions work on 1.8 at their correct durations. Builds at spawn have been optimized for 1.8 block appearance. You can even craft enchanted golden apples!</p>
|
||||
<div class="flex-child small splitter bukkit-red outlined">
|
||||
<h2><strong>Great on 1.8:</strong></h2>
|
||||
<p><br>Unlike some other servers, we don't believe in leaving our loyal players in the dust with no support. There are good reasons many people continue to use Minecraft 1.8. From performance, to combat, to mods. If you are one of them, welcome home! Here you will experience being treated like a first class citizen. We have working fishing rods, ladders, lily pads, and brewing stands. All potions work on 1.8 at their correct durations. Builds at spawn have been optimized for 1.8 block appearance. You can even craft enchanted golden apples!</p>
|
||||
</div>
|
||||
<div class="flex-child large grid zoomedout roundedcorners" style="filter: drop-shadow(0 0 0.3rem #FF5555); background-image: url(assets/images/smp-banner-five.png)"></div>
|
||||
<div class="flex-child large zoomedout roundedcorners" style="filter: drop-shadow(0 0 0.3rem #FF5555); background-image: url(assets/images/smp-banner-five.png)"></div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="flex-child large zoomedout roundedcorners" style="filter: drop-shadow(0 0 0.3rem #55FFFF); background-image: url(assets/images/smp-banner-three.png)"></div>
|
||||
<div class="flex-child small splitter bukkit-blue outlined">
|
||||
<h2><strong>Classic Combat:</strong></h2>
|
||||
<p><br><span class="bukkit-green">True</span><span class="bukkit-red">OG</span> utilizes the 1.8 style PvP/PvE system with NO click-delay. We believe this model is vastly superior to 1.9+ combat. It's quicker, more intense, and has a longer, more fulfilling skill ladder. Mojang is well aware of the new system's flaws, which is why they haven't added it to Bedrock. The experimental combat snapshot also eliminates the click-delay, and we will consider switching to those new mechanics once they are rolled out in a future version of Minecraft. Until that time we are committed to supporting 1.8 combat.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="flex-child small splitter bukkit-green outlined">
|
||||
<h2><strong>Open Source:</strong></h2>
|
||||
<p><br><span class="bukkit-green">True</span><span class="bukkit-red">OG</span> is run by a dedicated Socialist and open source advocate. As such, the server is <a href="https://github.com/true-og/true-og" target="_blank" class="bukkit-blue">published to GitHub</a> in its entirety. Original code uses <a href="https://unlicense.org/" target="_blank" class="bukkit-blue">The Unlicense</a>, meaning it is released into the public domain for any purpose, commercial or personal, for free, forever. The code for community projects utilized by <span class="bukkit-green">True</span><span class="bukkit-red">OG</span> is linked to on GitHub. Their respective terms must be followed. The only proprietary part of <span class="bukkit-green">True</span><span class="bukkit-red">OG</span> is our post-launch map and seed. This is to prevent coordinates and bases from being compromised.</p>
|
||||
</div>
|
||||
<div class="flex-child large zoomedout roundedcorners" style="filter: drop-shadow(0 0 0.3rem #55FF55); background-image: url(assets/images/smp-banner-four.png)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="games" id="duels">
|
||||
<!-- Shop: the donor ranks, mirrored from the store at store.trueog.net. -->
|
||||
<div class="games" id="shop">
|
||||
<div class="box">
|
||||
<div class="flex-child grid outlined">
|
||||
<h1 style="color: white; text-align: center"><i class="fa-solid fa-heart-pulse bukkit-dark-red fa-width-auto" aria-hidden="true"></i> <span class="bukkit-green">True</span><span class="bukkit-red">OG</span> Duels:</h1>
|
||||
<p style="color: white; font-size: 2.5vh; padding: 0vh"><br>A long-requested feature of the Season 1 community, <span class="bukkit-green">True</span><span class="bukkit-red">OG</span> Duels allow you to challenge anyone to a fight without risking your gear, experience, or SMP stats. Duel stats are tracked in an entirely seperate system called <a href="https://dotesports.com/general/news/elo-ratings-explained-20565" target="_blank" class="bukkit-blue">ELO</a>.<br><br>All duels take place using glorious 1.8 PVP! You will be equipped with golden apples, ender pearls, a variety of potions, and weapons/armor according to your kit selections. You can cycle through a random arena, or pick your favorite from over a dozen custom builds. If you are feeling confident, you can even duel for Diamonds!</p>
|
||||
<div class="flex-child outlined">
|
||||
<h1><i class="fa-solid fa-bag-shopping bukkit-red fa-width-auto" aria-hidden="true"></i> The <span class="bukkit-green">True</span><span class="bukkit-red">OG</span> Shop:</h1>
|
||||
<p><br>Premium Ranks are affordable, <strong class="bukkit-gold">never pay-to-win</strong>, and help the server thrive! Your contribution helps us pay for server bills, DDOS protection, advertising, and a plethora of free and open source services. Any contributions in a given month that exceed our monthly expenses are distributed via a democratic vote of the staff team.<br><br>Each rank progresses from the one before it. You start your journey as an <strong>OG</strong> ($10), then upgrade to <strong class="bukkit-red">OG Pro</strong> ($50), and finally, you can become a true <strong class="bukkit-dark-red">OG Master</strong> ($10/month subscription).</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container bukkit-green" style="margin-bottom: 2vh; filter: drop-shadow(0 0 0.3rem black);">
|
||||
<img src="assets/images/Atlantis.png" class="image-item zoomedout roundedcorners" alt="Atlantis">
|
||||
<span class="overlay">Atlantis</span>
|
||||
<img src="assets/images/Blizzard.png" class="image-item zoomedout roundedcorners" alt="Blizzard">
|
||||
<div class="overlay"><span>Blizzard</span></div>
|
||||
<img src="assets/images/Castle.png" class="image-item zoomedout roundedcorners" alt="Castle">
|
||||
<div class="overlay"><span>Castle</span></div>
|
||||
<img src="assets/images/Cave.png" class="image-item zoomedout roundedcorners" alt="Cave">
|
||||
<div class="overlay"><span>Cave</span></div>
|
||||
<img src="assets/images/Colosseum.png" class="image-item zoomedout roundedcorners" alt="Colosseum">
|
||||
<div class="overlay"><span>Colosseum</span></div>
|
||||
<img src="assets/images/Ender.png" class="image-item zoomedout roundedcorners" alt="Ender">
|
||||
<div class="overlay"><span>Ender</span></div>
|
||||
<img src="assets/images/Frozone.png" class="image-item zoomedout roundedcorners" alt="Frozone">
|
||||
<div class="overlay"><span>Frozone</span></div>
|
||||
<img src="assets/images/Fungi.png" class="image-item zoomedout roundedcorners" alt="Fungi">
|
||||
<div class="overlay"><span>Fungi</span></div>
|
||||
<img src="assets/images/Hell.png" class="image-item zoomedout roundedcorners" alt="Hell">
|
||||
<div class="overlay"><span>Hell</span></div>
|
||||
<img src="assets/images/Olympus.png" class="image-item zoomedout roundedcorners" alt="Olympus">
|
||||
<div class="overlay"><span>Olympus</span></div>
|
||||
<img src="assets/images/Sewer.png" class="image-item zoomedout roundedcorners" alt="Sewer">
|
||||
<div class="overlay"><span>Sewer</span></div>
|
||||
<!-- One card per rank, linking to its store page. -->
|
||||
<div class="box ranks">
|
||||
<a class="rank outlined" href="https://store.trueog.net/package/1034440" target="_blank">
|
||||
<img src="assets/images/logos/og.png" alt="OG rank">
|
||||
<p class="price">$10 <span>one time</span></p>
|
||||
<ul>
|
||||
<li>Use /afk for an unlimited amount of time.</li>
|
||||
<li>Prominent white chat prefix.</li>
|
||||
<li>Create unions with /union create.</li>
|
||||
<li>Invite people to unions with /union invite.</li>
|
||||
<li>Access to #donor-chat on Discord.</li>
|
||||
<li>6 homes (temporary benefit until the implementation of our in-game home earning plugin).</li>
|
||||
<li>More TBA.</li>
|
||||
</ul>
|
||||
<span class="buy"><i class="fa-solid fa-cart-shopping fa-width-auto" aria-hidden="true"></i> Buy OG</span>
|
||||
</a>
|
||||
<a class="rank outlined" href="https://store.trueog.net/package/1141035" target="_blank">
|
||||
<img src="assets/images/logos/og-pro.png" alt="OG Pro rank">
|
||||
<p class="price">$50 <span>one time · best value</span></p>
|
||||
<ul>
|
||||
<li>All OG Benefits.</li>
|
||||
<li>Rent up to 2 shops at the player market.</li>
|
||||
<li>Special chat formatting (bold, italics, magic, etc).</li>
|
||||
<li>Use color codes in chat, mail, unions, signs, anvils, and book titles.</li>
|
||||
<li>Edit written books with /book.</li>
|
||||
<li>Edit signs with /editsign.</li>
|
||||
<li>Wear hats with /hat.</li>
|
||||
<li>Sit anywhere with /sit.</li>
|
||||
<li>Access your ender chest anywhere with /echest.</li>
|
||||
<li>Use 9 exclusive particle cosmetics with /cosmetics (up to 3 at once).</li>
|
||||
<li>More TBA.</li>
|
||||
</ul>
|
||||
<span class="buy"><i class="fa-solid fa-cart-shopping fa-width-auto" aria-hidden="true"></i> Buy OG Pro</span>
|
||||
</a>
|
||||
<a class="rank outlined" href="https://store.trueog.net/package/882910" target="_blank">
|
||||
<img src="assets/images/logos/og-master.png" alt="OG Master rank">
|
||||
<p class="price">$10 <span>per month</span></p>
|
||||
<ul>
|
||||
<li>All OG and OG Pro Benefits.</li>
|
||||
<li>Rent up to 3 shops at the player market.</li>
|
||||
<li>World inspection with /co inspect.</li>
|
||||
<li>Return to last teleport destination with /back.</li>
|
||||
<li>Toggle your death coordinates in chat with /tdc.</li>
|
||||
<li>Special mail, sign, and anvil formatting (strike-through, italics, magic, etc).</li>
|
||||
<li>More TBA.</li>
|
||||
</ul>
|
||||
<span class="buy"><i class="fa-solid fa-cart-shopping fa-width-auto" aria-hidden="true"></i> Buy OG Master</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>-->
|
||||
<div class="box">
|
||||
<div class="flex-child outlined">
|
||||
<p style="text-align: center">If you would like to contribute to <span class="bukkit-green">True</span><span class="bukkit-red">OG</span> without getting a rank, you may do so using <a href="https://www.paypal.com/donate/?hosted_button_id=99S3FB4HLYK78" target="_blank" class="bukkit-blue"><i class="fa-brands fa-paypal fa-width-auto" aria-hidden="true"></i> PayPal</a>, <a href="https://buy.stripe.com/7sI2bR9Xx4r27Ys000" target="_blank" class="bukkit-blue"><i class="fa-brands fa-stripe-s fa-width-auto" aria-hidden="true"></i> Stripe</a>, or <a href="https://commerce.coinbase.com/checkout/bdce2a7e-0eab-46fb-9dda-e96b0044c7a4" target="_blank" class="bukkit-blue"><i class="fa-brands fa-bitcoin fa-width-auto" aria-hidden="true"></i> Coinbase</a>.<br><br>Premium benefits are non-transferable, and subject to change. Please read the <a href="/terms/" class="bukkit-blue">Terms of Service</a>, <a href="/privacy/" class="bukkit-blue">Privacy Policy</a>, and <a href="/impressum/" class="bukkit-blue">Impressum</a> before making a purchase.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<ul>
|
||||
<li><a href="https://github.com/true-og/website" target="_blank" aria-label="TrueOG website source code on GitHub"><i class="fa-brands fa-github fa-width-auto" aria-hidden="true"></i></a></li>
|
||||
<li><a href="https://mastodon.gamedev.place/@trueog" target="_blank" aria-label="TrueOG on Mastodon"><i class="fa-brands fa-mastodon fa-width-auto" aria-hidden="true"></i></a></li>
|
||||
<li><a href="https://www.facebook.com/people/True-OG/100076093982787" target="_blank" aria-label="TrueOG on Facebook"><i class="fa-brands fa-facebook fa-width-auto" aria-hidden="true"></i></a></li>
|
||||
</ul>
|
||||
<p><br>Website based on a template from <a href="https://tutorialzine.com/2016/06/freebie-landing-page-template-with-flexbox/" target="_blank">tutorialzine</a> <br>Uses a permissive<a href="https://tutorialzine.com/license/" target="_blank"> Open Source License</a><br><br></p>
|
||||
<p><a href="https://stpeter.im/writings/essays/publicdomain.html" target="_blank">
|
||||
<img src="assets/images/logos/Anti-copyright.svg" style="width: 50px" alt="Anti-Copyright Logo"></a>
|
||||
<a href="https://iww.org/" target="_blank">
|
||||
<img src="assets/images/logos/iww.png" style="width: 48px" alt="IWW Union Logo"></a></p>
|
||||
<p class="bukkit-blue"><br>Ad astra per aspera</p>
|
||||
</footer>
|
||||
<!-- Team: a card per staff member, then the application forms (apply.php). -->
|
||||
<div class="games" id="team">
|
||||
<div class="box">
|
||||
<div class="flex-child outlined">
|
||||
<h1><i class="fa-solid fa-users bukkit-dark-purple fa-width-auto" aria-hidden="true"></i> The <span class="bukkit-green">True</span><span class="bukkit-red">OG</span> Team:</h1>
|
||||
<p style="text-align: center"><br>Our staff team is structured as a unionized, democratic worker cooperative.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Heads come from mc-heads.net by UUID through /head/, which caches them for a day. -->
|
||||
<div class="box staff">
|
||||
<?php foreach (staff() as [$uuid, $name, $role, $color]) { ?>
|
||||
<div class="member outlined <?php echo $color; ?>">
|
||||
<img src="/head/?u=<?php echo $uuid; ?>" width="128" height="128" alt="<?php echo $name; ?>'s Minecraft head">
|
||||
<p class="name"><?php echo $name; ?></p>
|
||||
<p class="role"><?php echo $role; ?></p>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="flex-child outlined">
|
||||
<?php if (!forms_open()) { ?>
|
||||
<p class="notice" style="text-align: center">Applications are closed at the moment.</p>
|
||||
<?php } ?>
|
||||
<h2><strong class="bukkit-dark-purple">Apply for Ranks:</strong></h2>
|
||||
<div class="apply">
|
||||
<a class="apply-button" href="/apply.php?form=tester"><i class="fa-solid fa-triangle-exclamation bukkit-gold fa-width-auto" aria-hidden="true"></i> Apply for Tester</a>
|
||||
<a class="apply-button" href="/apply.php?form=youtube"><i class="fa-brands fa-youtube bukkit-red fa-width-auto" aria-hidden="true"></i> Apply for YouTube</a>
|
||||
<a class="apply-button" href="/apply.php?form=builder"><i class="fa-solid fa-hammer bukkit-blue fa-width-auto" aria-hidden="true"></i> Apply for Builder</a>
|
||||
<a class="apply-button" href="/apply.php?form=moderator"><i class="fa-solid fa-heart bukkit-red fa-width-auto" aria-hidden="true"></i> Apply for Moderator</a>
|
||||
</div>
|
||||
<h2><strong class="bukkit-dark-purple">Season 1 Migration:</strong></h2>
|
||||
<div class="apply">
|
||||
<a class="apply-button" href="/apply.php?form=homes"><i class="fa-solid fa-house bukkit-green fa-width-auto" aria-hidden="true"></i> Recover Homes</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="#" class="top" aria-label="Back to top"><i class="fa-solid fa-circle-up fa-width-auto" aria-hidden="true"></i></a>
|
||||
<!-- World: the Season 1 map download. Direct link while the torrent server is down; magnet for when it is back. -->
|
||||
<div class="games" id="world">
|
||||
<div class="box">
|
||||
<div class="flex-child outlined">
|
||||
<h1><i class="fa-solid fa-globe bukkit-dark-aqua fa-width-auto" aria-hidden="true"></i> Season 1 World Download:</h1>
|
||||
<p style="text-align: center"><br>The pre-launch OG:SMP Season 1 world is available for personal use only. Our license to use the maps commercially is non-transferable, and the post-launch <span class="bukkit-green">True</span><span class="bukkit-red">OG</span> maps will not be published during the lifetime of the server.<br><br>The archive is about 171 GB. While the torrent server is down, use the direct download; the magnet link will work again once it is back and seeding.</p>
|
||||
<div class="apply">
|
||||
<a class="apply-button" href="https://true-og.net/smp.tar.gz"><i class="fa-solid fa-download bukkit-green fa-width-auto" aria-hidden="true"></i> Direct Download</a>
|
||||
<a class="apply-button" href="magnet:?xt=urn:btih:402df9c1d86bebb5f4b0523038e713dc3d50b0be&dn=smp.tar.gz&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce"><i class="fa-solid fa-magnet bukkit-gold fa-width-auto" aria-hidden="true"></i> Magnet Link</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
<!-- FAQ: questions and answers on the left, server facts on the right. -->
|
||||
<div class="games" id="faq">
|
||||
<div class="box">
|
||||
<div class="flex-child outlined">
|
||||
<h1><i class="fa-solid fa-circle-question bukkit-darkgreen fa-width-auto" aria-hidden="true"></i> Frequently Asked Questions:</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box faq">
|
||||
<div class="flex-child qa outlined">
|
||||
<p class="q">Q: Where did <span class="bukkit-green">True</span><span class="bukkit-red">OG</span> come from?</p>
|
||||
<p><strong>A:</strong> <span class="bukkit-green">True</span><span class="bukkit-red">OG</span> is a community SMP based on the OG:SMP Season 1 map by players who disagreed with the map reset and the direction of the server away from the "OG" meta.</p>
|
||||
<p class="q">Q: What version of Minecraft is the terrain generated in?</p>
|
||||
<p><strong>A:</strong> Terrain from spawn to 40k is 1.16.5. Terrain from 40k to 80k is 1.17.1. Terrain from 80k to 96k is 1.18.2. Terrain from 96k to 112k is 1.19.4. The Nether border is at 14k, and the End border is at 30 million, as always. The world border is expanded for each major update with new chunks.</p>
|
||||
<p class="q">Q: How do I navigate the spawn and community marketplace?</p>
|
||||
<p><strong>A:</strong> <a href="assets/images/market-map.png" target="_blank">Market Map</a></p>
|
||||
<p><strong>A:</strong> <a href="assets/images/market-map-2.png" target="_blank">Combined Spawn/Market Map</a></p>
|
||||
<p class="q">Q: How does the economy work?</p>
|
||||
<p><strong>A:</strong> Like 2011-2013 era SMP servers, the <span class="bukkit-green">True</span><span class="bukkit-red">OG</span> economy is based on diamonds. Unlike Essentials money, they are a tangible object with a limited supply. As the universal currency, they combat inflation and facilitate fair, understandable transactions. /pay sends another player the specified amount of diamonds. /withdraw takes the specified amount of diamonds out of your bank account and places them into your inventory. /deposit works the other way around. Diamonds stored in your inventory or at the bank will count towards your balance, which can be checked with /balance. The bank is located to the right of the community marketplace entryway.</p>
|
||||
</div>
|
||||
<div class="flex-child qa outlined">
|
||||
<p class="q"><i class="fa-solid fa-network-wired fa-width-auto" aria-hidden="true"></i> IP for Minecraft Java:</p>
|
||||
<p><strong>true-og.net</strong></p>
|
||||
<p><strong>trueog.net</strong></p>
|
||||
<p>Alternate: <strong>play.true-og.net</strong></p>
|
||||
<p>Alternate: <strong>play.trueog.net</strong></p>
|
||||
<p class="q"><i class="fa-solid fa-server fa-width-auto" aria-hidden="true"></i> Host:</p>
|
||||
<p><a href="https://www.hetzner.com/dedicated-rootserver/ax101/" target="_blank">Hetzner AX101</a> in Falkenstein, Germany</p>
|
||||
<p class="q"><i class="fa-solid fa-microchip fa-width-auto" aria-hidden="true"></i> Hardware:</p>
|
||||
<p>Dedicated AMD Ryzen 9 5950X | 128GB RAM | 2x 1.92TB NVMe SSD | 1Gbps Unmetered Connection</p>
|
||||
<p class="q"><i class="fa-solid fa-gear fa-width-auto" aria-hidden="true"></i> Software:</p>
|
||||
<p><a href="https://github.com/true-og/Purpur" target="_blank">Purpur</a> 1.19.4 with custom patches for TrueOG Network</p>
|
||||
<p class="q"><i class="fa-solid fa-scale-balanced fa-width-auto" aria-hidden="true"></i> Rules:</p>
|
||||
<p><a href="/rules/">true-og.net/rules</a></p>
|
||||
<p class="q"><i class="fa-solid fa-code fa-width-auto" aria-hidden="true"></i> Source Code:</p>
|
||||
<p><a href="https://github.com/true-og/true-og" target="_blank">TrueOG on GitHub</a></p>
|
||||
<p class="q"><i class="fa-solid fa-bag-shopping fa-width-auto" aria-hidden="true"></i> Donation Store:</p>
|
||||
<p><a href="https://store.trueog.net/" target="_blank">store.trueog.net</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</html>
|
||||
<?php
|
||||
page_end();
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
/* Printed in game by /ranks and /buy. */
|
||||
header('Location: https://store.trueog.net/package/882910', true, 302);
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
/* Printed in game by /ranks and /buy. */
|
||||
header('Location: https://store.trueog.net/package/1141035', true, 302);
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
/* Printed in game by /ranks and /buy. */
|
||||
header('Location: https://store.trueog.net/package/1034440', true, 302);
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
require dirname(__DIR__) . '/site.php';
|
||||
policy_page('privacy');
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
require dirname(__DIR__) . '/site.php';
|
||||
policy_page('rules');
|
||||
@@ -0,0 +1,795 @@
|
||||
<?php
|
||||
/* All of the site's code: settings, helpers, the page frame, policy documents, staff heads, and the application forms. No JavaScript anywhere. */
|
||||
|
||||
/* Settings are key=value lines in ../.secrets, one directory above the web root so the web server can never serve it. */
|
||||
function site_config(): array {
|
||||
|
||||
static $config = null;
|
||||
|
||||
if ($config === null) {
|
||||
|
||||
$config = ['discord_webhook' => '', 'applications_file' => dirname(__DIR__) . '/databases/applications.txt'];
|
||||
|
||||
foreach (explode("\n", (string) @file_get_contents(dirname(__DIR__) . '/.secrets')) as $line) {
|
||||
|
||||
[$key, $value] = explode('=', $line, 2) + ['', ''];
|
||||
|
||||
if (isset($config[$key])) {
|
||||
|
||||
$config[$key] = trim($value, " \t\r\"'");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $config;
|
||||
|
||||
}
|
||||
|
||||
function e(?string $text): string {
|
||||
|
||||
return htmlspecialchars((string) $text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
|
||||
}
|
||||
|
||||
/* Private directory for throttle slots and caches; '' if it cannot be made safely. */
|
||||
function temp_dir(): string {
|
||||
|
||||
$dir = sys_get_temp_dir() . '/trueog-website';
|
||||
$uid = function_exists('posix_geteuid') ? posix_geteuid() : null;
|
||||
@mkdir($dir, 0700);
|
||||
|
||||
return is_dir($dir) && is_writable($dir) && ($uid === null || fileowner($dir) === $uid) ? $dir : '';
|
||||
|
||||
}
|
||||
|
||||
/* Forms work only with a webhook, mbstring, the wrapper for the webhook's scheme (https needs openssl), and a private directory. */
|
||||
function forms_open(): bool {
|
||||
|
||||
$webhook = site_config()['discord_webhook'];
|
||||
|
||||
return $webhook !== '' && extension_loaded('mbstring') && ini_get('allow_url_fopen') && in_array(parse_url($webhook, PHP_URL_SCHEME), stream_get_wrappers(), true) && temp_dir() !== '';
|
||||
|
||||
}
|
||||
|
||||
function in_cidr(string $ip, string $cidr): bool {
|
||||
|
||||
[$net, $bits] = explode('/', $cidr);
|
||||
$a = inet_pton($ip);
|
||||
$b = inet_pton($net);
|
||||
|
||||
if ($a === false || $b === false || strlen($a) !== strlen($b)) {
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
$bytes = intdiv((int) $bits, 8);
|
||||
$rest = (int) $bits % 8;
|
||||
|
||||
return substr($a, 0, $bytes) === substr($b, 0, $bytes) && ($rest === 0 || ord($a[$bytes]) >> (8 - $rest) === ord($b[$bytes]) >> (8 - $rest));
|
||||
|
||||
}
|
||||
|
||||
/* The visitor's IP: CF-Connecting-IP only when the connection really comes from Cloudflare (cloudflare.com/ips). */
|
||||
function client_ip(): string {
|
||||
|
||||
static $cloudflare = ['173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22', '141.101.64.0/18', '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20', '197.234.240.0/22', '198.41.128.0/17', '162.158.0.0/15', '104.16.0.0/13', '104.24.0.0/14', '172.64.0.0/13', '131.0.72.0/22', '2400:cb00::/32', '2606:4700::/32', '2803:f800::/32', '2405:b500::/32', '2405:8100::/32', '2a06:98c0::/29', '2c0f:f248::/32'];
|
||||
$ip = (string) ($_SERVER['REMOTE_ADDR'] ?? '');
|
||||
$edge = (string) ($_SERVER['HTTP_CF_CONNECTING_IP'] ?? '');
|
||||
|
||||
if ($edge !== '' && array_filter($cloudflare, fn (string $cidr): bool => in_cidr($ip, $cidr)) !== []) {
|
||||
|
||||
$ip = $edge;
|
||||
|
||||
}
|
||||
|
||||
return filter_var($ip, FILTER_VALIDATE_IP) ?: '';
|
||||
|
||||
}
|
||||
|
||||
/* Throttle key: the IPv4 address, or the /64 of an IPv6 address. */
|
||||
function ip_bucket(string $ip): string {
|
||||
|
||||
$bin = inet_pton($ip);
|
||||
|
||||
return $bin !== false && strlen($bin) === 16 ? bin2hex(substr($bin, 0, 8)) : $ip;
|
||||
|
||||
}
|
||||
|
||||
/* GET, or POST as JSON when $json is given. Body on a 2xx response, else null (and the status is logged). */
|
||||
function http_fetch(string $url, ?array $json = null): ?string {
|
||||
|
||||
$options = ['timeout' => 10, 'user_agent' => 'TrueOG-Website (+https://true-og.net/)', 'ignore_errors' => true];
|
||||
|
||||
if ($json !== null) {
|
||||
|
||||
$options += ['method' => 'POST', 'header' => "Content-Type: application/json\r\n", 'content' => json_encode($json, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE)];
|
||||
|
||||
}
|
||||
|
||||
ini_set('default_socket_timeout', '10');
|
||||
$body = @file_get_contents($url, false, stream_context_create(['http' => $options]));
|
||||
$status = 0;
|
||||
|
||||
foreach ($http_response_header ?? [] as $header) {
|
||||
|
||||
if (str_starts_with($header, 'HTTP/')) {
|
||||
|
||||
$status = (int) substr($header, strcspn($header, ' ') + 1, 3); // The last status line wins after redirects.
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($body === false || $status < 200 || $status >= 300) {
|
||||
|
||||
error_log('site.php: ' . parse_url($url, PHP_URL_HOST) . " answered $status: " . substr((string) $body, 0, 200));
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
return $body;
|
||||
|
||||
}
|
||||
|
||||
/* Opens the page: <head>, then the first section (class $class, id $id) holding the header. The caller closes the section. */
|
||||
function page_start(string $title, string $class, string $id = ''): void {
|
||||
|
||||
$nav = [
|
||||
['/#smp', 'fa-gamepad bukkit-gold', 'Games'],
|
||||
['/#shop', 'fa-bag-shopping bukkit-red', 'Shop'],
|
||||
['/#team', 'fa-users bukkit-dark-purple', 'Team'],
|
||||
['/#world', 'fa-globe bukkit-dark-aqua', 'World'],
|
||||
['/rules/', 'fa-scale-balanced bukkit-blue', 'Rules'],
|
||||
['/#faq', 'fa-circle-question bukkit-darkgreen', 'FAQ'],
|
||||
];
|
||||
$version = fn (string $file): int => (int) @filemtime(__DIR__ . $file);
|
||||
header('Cache-Control: no-transform'); // Keeps Cloudflare from injecting scripts.
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="/assets/images/favicons/favicon.ico">
|
||||
<link rel="icon" href="/assets/images/favicons/favicon-32.png" sizes="32x32">
|
||||
<link rel="icon" href="/assets/images/favicons/favicon-96.png" sizes="96x96">
|
||||
<link rel="icon" href="/assets/images/favicons/favicon-196.png" sizes="196x196">
|
||||
<link rel="apple-touch-icon" href="/assets/images/favicons/favicon-180.png">
|
||||
<title><?php echo e($title === '' ? 'TrueOG' : "$title | TrueOG"); ?></title>
|
||||
<!-- ?v= is the file's mtime, so a deploy busts the Cloudflare and browser caches. -->
|
||||
<link rel="stylesheet" href="/Font-Awesome/css/all.min.css?v=<?php echo $version('/Font-Awesome/css/all.min.css'); ?>">
|
||||
<link rel="stylesheet" href="/stylesheet.css?v=<?php echo $version('/stylesheet.css'); ?>">
|
||||
</head>
|
||||
<body>
|
||||
<div class="<?php echo e($class); ?>"<?php echo $id !== '' ? ' id="' . e($id) . '"' : ''; ?>>
|
||||
<header>
|
||||
<a href="/"><img src="/assets/images/logos/trueog-logo-2026-transparent.png" class="main-logo" alt="Logo"></a>
|
||||
<nav class="outlined">
|
||||
<ul>
|
||||
<?php foreach ($nav as [$href, $icon, $label]) { ?>
|
||||
<li><a href="<?php echo $href; ?>"><i class="fa-solid <?php echo $icon; ?> fa-width-auto" aria-hidden="true"></i> <strong><?php echo $label; ?></strong></a></li>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
<?php
|
||||
}
|
||||
|
||||
function page_end(): void {
|
||||
?>
|
||||
<footer>
|
||||
<ul>
|
||||
<li><a href="https://github.com/true-og/website" target="_blank" aria-label="TrueOG website source code on GitHub"><i class="fa-brands fa-github fa-width-auto" aria-hidden="true"></i></a></li>
|
||||
<li><a href="https://mastodon.gamedev.place/@trueog" target="_blank" aria-label="TrueOG on Mastodon"><i class="fa-brands fa-mastodon fa-width-auto" aria-hidden="true"></i></a></li>
|
||||
<li><a href="https://www.facebook.com/people/True-OG/100076093982787" target="_blank" aria-label="TrueOG on Facebook"><i class="fa-brands fa-facebook fa-width-auto" aria-hidden="true"></i></a></li>
|
||||
<li><a href="https://reddit.com/r/trueog" target="_blank" aria-label="TrueOG on Reddit"><i class="fa-brands fa-reddit fa-width-auto" aria-hidden="true"></i></a></li>
|
||||
</ul>
|
||||
<p><a href="/rules/">Rules</a> · <a href="/terms/">Terms of Service</a> · <a href="/privacy/">Privacy Policy</a> · <a href="/impressum/">Impressum</a></p>
|
||||
<p><br>Website based on a template from <a href="https://tutorialzine.com/2016/06/freebie-landing-page-template-with-flexbox/" target="_blank">tutorialzine</a> <br>Uses a permissive<a href="https://tutorialzine.com/license/" target="_blank"> Open Source License</a><br><br></p>
|
||||
<p><a href="https://stpeter.im/writings/essays/publicdomain.html" target="_blank">
|
||||
<img src="/assets/images/logos/Anti-copyright.svg" style="width: 50px" alt="Anti-Copyright Logo"></a>
|
||||
<a href="https://iww.org/" target="_blank">
|
||||
<img src="/assets/images/logos/iww.png" style="width: 48px" alt="IWW Union Logo"></a></p>
|
||||
<p class="bukkit-blue"><br>Ad astra per aspera</p>
|
||||
</footer>
|
||||
<a href="#" class="top" aria-label="Back to top"><i class="fa-solid fa-circle-up fa-width-auto" aria-hidden="true"></i></a>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
}
|
||||
|
||||
/* Escapes text and turns words starting with http(s):// into anchors; trailing punctuation stays outside the link. */
|
||||
function linked(string $text): string {
|
||||
|
||||
$words = [];
|
||||
|
||||
foreach (explode(' ', $text) as $word) {
|
||||
|
||||
$link = str_starts_with($word, 'http://') || str_starts_with($word, 'https://') ? rtrim($word, '.,;:!?)') : '';
|
||||
$words[] = $link === '' ? e($word) : '<a href="' . e($link) . '" target="_blank" rel="noopener">' . e($link) . '</a>' . e(substr($word, strlen($link)));
|
||||
|
||||
}
|
||||
|
||||
return implode(' ', $words);
|
||||
|
||||
}
|
||||
|
||||
/* $file's contents, refreshed with $fetch() once older than $ttl seconds; if that fails the old copy stays another $ttl. */
|
||||
function cached(string $file, int $ttl, callable $fetch): ?string {
|
||||
|
||||
if (is_file($file) && filemtime($file) > time() - $ttl) {
|
||||
|
||||
return file_get_contents($file) ?: null;
|
||||
|
||||
}
|
||||
|
||||
$text = $fetch() ?: null;
|
||||
|
||||
if ($text !== null && ($tmp = tempnam(dirname($file), 'tmp')) !== false) {
|
||||
|
||||
file_put_contents($tmp, $text) !== false && rename($tmp, $file) || @unlink($tmp); // Atomic replace.
|
||||
|
||||
} elseif ($text === null && is_file($file)) {
|
||||
|
||||
@touch($file);
|
||||
$text = file_get_contents($file) ?: null;
|
||||
|
||||
}
|
||||
|
||||
return $text;
|
||||
|
||||
}
|
||||
|
||||
/* rules.txt to HTML: "=-=-Section-=-=" is h2, "Rule N - ..." is h3, "- " is a list item, "(...)" a note, the rest paragraphs. */
|
||||
function rules_html(string $text): string {
|
||||
|
||||
$html = '';
|
||||
$list = false;
|
||||
|
||||
foreach (explode("\n", str_replace(["\r\n", "\r"], "\n", $text)) as $line) {
|
||||
|
||||
$line = rtrim($line);
|
||||
$item = str_starts_with($line, '- ');
|
||||
|
||||
if ($list && !$item) {
|
||||
|
||||
$html .= "</ul>\n";
|
||||
$list = false;
|
||||
|
||||
}
|
||||
|
||||
if ($line === '' || strcasecmp(rtrim($line, ':'), 'TrueOG Official Rules') === 0) {
|
||||
|
||||
continue;
|
||||
|
||||
}
|
||||
|
||||
$linked = linked($item ? substr($line, 2) : $line);
|
||||
|
||||
if (str_starts_with($line, '=-=')) {
|
||||
|
||||
$html .= '<h2>' . e(rtrim(trim($line, "=- "), ':')) . "</h2>\n";
|
||||
|
||||
} elseif (str_starts_with($line, 'Rule ') && ctype_digit((string) strstr(substr($line, 5), ' ', true)) && str_contains($line, ' - ')) {
|
||||
|
||||
$html .= '<h3>' . e(rtrim($line, ':.')) . "</h3>\n";
|
||||
|
||||
} elseif ($item) {
|
||||
|
||||
$html .= ($list ? '' : "<ul>\n") . "<li>$linked</li>\n";
|
||||
$list = true;
|
||||
|
||||
} else {
|
||||
|
||||
$html .= (str_starts_with($line, '(') && str_ends_with($line, ')') ? '<p class="note">' : '<p>') . "$linked</p>\n";
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $html . ($list ? "</ul>\n" : '');
|
||||
|
||||
}
|
||||
|
||||
/* A document from the true-og repository's policies/: the sibling directory when deployed as its submodule, else GitHub, cached for an hour. */
|
||||
function policy_page(string $key): void {
|
||||
|
||||
$docs = [
|
||||
'rules' => ['rules.txt', 'Rules', 'fa-scale-balanced bukkit-blue'],
|
||||
'terms' => ['terms-of-service.html', 'Terms of Service', 'fa-file-contract bukkit-gold'],
|
||||
'privacy' => ['privacy-policy.txt', 'Privacy Policy', 'fa-user-shield bukkit-green'],
|
||||
'impressum' => ['impressum.html', 'Impressum', 'fa-building bukkit-light-purple'],
|
||||
];
|
||||
[$file, $title, $icon] = $docs[$key];
|
||||
$local = dirname(__DIR__) . '/policies/' . $file;
|
||||
$github = 'https://github.com/true-og/true-og/blob/main/policies/' . $file;
|
||||
$raw = 'https://raw.githubusercontent.com/true-og/true-og/main/policies/' . $file;
|
||||
|
||||
if (is_file($local)) {
|
||||
|
||||
$text = file_get_contents($local) ?: null;
|
||||
|
||||
} elseif (($dir = temp_dir()) === '') {
|
||||
|
||||
$text = http_fetch($raw) ?: null;
|
||||
|
||||
} else {
|
||||
|
||||
$text = cached("$dir/policy-$file", 3600, fn (): ?string => http_fetch($raw));
|
||||
|
||||
}
|
||||
|
||||
page_start($title, 'games page', 'policy');
|
||||
?>
|
||||
<div class="box">
|
||||
<div class="flex-child outlined">
|
||||
<h1><i class="fa-solid <?php echo $icon; ?> fa-width-auto" aria-hidden="true"></i> <span class="bukkit-green">True</span><span class="bukkit-red">OG</span> <?php echo e($title); ?>:</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="flex-child panel policy outlined">
|
||||
<?php if ($text === null) { ?>
|
||||
<p class="notice">This document could not be loaded right now. Read it <a href="<?php echo e($github); ?>" target="_blank" rel="noopener">on GitHub</a> in the meantime.</p>
|
||||
<?php } else { ?>
|
||||
<?php echo $key === 'rules' ? rules_html($text) : $text; ?>
|
||||
<?php } ?>
|
||||
<p class="source">Maintained in the <a href="<?php echo e($github); ?>" target="_blank" rel="noopener">TrueOG repository on GitHub</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
page_end();
|
||||
|
||||
}
|
||||
|
||||
/* The team, as [uuid, name, role, color]; by UUID so a card keeps working if the player changes their name. */
|
||||
function staff(): array {
|
||||
|
||||
return [
|
||||
['9c272295-71dd-4aad-b59e-3de12d73b68a', 'NotAlexNoyle', 'Admin', 'bukkit-red'],
|
||||
['399e3fb8-c7ec-4351-bf78-86d11687d023', 'Gx0rge', 'Developer', 'bukkit-blue'],
|
||||
['812bceca-7190-4879-801e-a2e5fa0421b5', 'SKBotNL', 'Developer', 'bukkit-blue'],
|
||||
['aec7ca7a-3ca9-4001-adfc-a81b596d8201', 'OmeniusKamis1', 'Moderator', 'bukkit-gold'],
|
||||
['a8f81a4a-d522-4cdf-a299-3c2842c13635', 'Sekalol15', 'Moderator', 'bukkit-gold'],
|
||||
['27342539-9648-4fbc-ae33-c33242c8711e', 'sam3192', 'Builder', 'bukkit-green'],
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
/* /head/?u=<uuid>: a staff member's head from mc-heads.net, cached here for a day so visitors never contact a third party. */
|
||||
function head_image(string $uuid): void {
|
||||
|
||||
$dir = temp_dir();
|
||||
$upstream = "https://mc-heads.net/avatar/$uuid/128";
|
||||
|
||||
if (!in_array($uuid, array_column(staff(), 0), true)) {
|
||||
|
||||
http_response_code(404);
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
$png = $dir === '' ? null : cached("$dir/head-$uuid.png", 86400, fn (): ?string => str_starts_with($p = (string) http_fetch($upstream), "\x89PNG") ? $p : null);
|
||||
|
||||
if ($png === null) {
|
||||
|
||||
header("Location: $upstream", true, 302);
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
header('Content-Type: image/png');
|
||||
header('Cache-Control: public, max-age=86400');
|
||||
header('Content-Length: ' . strlen($png));
|
||||
echo $png;
|
||||
|
||||
}
|
||||
|
||||
/* Field types: text, textarea, number (an age), url, radio, checkbox (several), agree (one box that must be ticked). */
|
||||
function field(string $name, string $label, string $type = 'text', bool $required = true, array $options = [], string $hint = ''): array {
|
||||
|
||||
return compact('name', 'label', 'type', 'required', 'options', 'hint');
|
||||
|
||||
}
|
||||
|
||||
/* The application forms by key. 'any' lists fields of which at least one must be filled. */
|
||||
function forms(): array {
|
||||
|
||||
$identity = [
|
||||
field('minecraft', 'Minecraft Username'),
|
||||
field('discord', 'Discord Username', 'text', true, [], 'As shown in your Discord settings.'),
|
||||
];
|
||||
$removable = 'may be removed from the team at any time and for any reason at the discretion of the server owner.';
|
||||
$playtime = field('playtime', 'How long have you played on TrueOG?');
|
||||
$age = field('age', 'How old are you?', 'number');
|
||||
|
||||
return [
|
||||
|
||||
'tester' => [
|
||||
'title' => 'Tester Sign-Up',
|
||||
'icon' => '<i class="fa-solid fa-triangle-exclamation bukkit-gold fa-width-auto" aria-hidden="true"></i>',
|
||||
'intro' => 'Beta test the TrueOG Minecraft server before it is released to the public! Signing up adds you to the server whitelist.',
|
||||
'fields' => [
|
||||
...$identity,
|
||||
field('season1', 'Did you participate in season 1 of OG:SMP, or are you new to the map?', 'radio', true, ['I participated in Season 1', 'This is my first time joining the map']),
|
||||
field('version', 'What version of Minecraft do you main?', 'text', false, [], 'For example 1.8.9 or 1.21.'),
|
||||
field('agree', 'Testers who cheat or abuse other players will not be entitled to early access. Your access to TrueOG may be revoked at any time at the discretion of the server owner.', 'agree', true, ['I have read this warning and I understand.']),
|
||||
],
|
||||
],
|
||||
|
||||
'youtube' => [
|
||||
'title' => 'YouTube Sign-Up',
|
||||
'icon' => '<i class="fa-brands fa-youtube bukkit-red fa-width-auto" aria-hidden="true"></i>',
|
||||
'intro' => 'Get extra benefits on the TrueOG Minecraft server as a reward for promoting it on your channel!',
|
||||
'requirements' => 'You must be at least 13 years old to apply. 500 subscriber minimum.',
|
||||
'fields' => [
|
||||
...$identity,
|
||||
$playtime,
|
||||
field('video', "Link to a video you've published about the server", 'url'),
|
||||
field('continue', 'Do you plan to keep making videos about TrueOG once you have the rank?', 'radio', true, ['Yes', 'No']),
|
||||
field('stream', 'Do you plan to stream on TrueOG?', 'radio', true, ['Yes', 'No', 'Maybe']),
|
||||
],
|
||||
],
|
||||
|
||||
'builder' => [
|
||||
'title' => 'Builder Application',
|
||||
'icon' => '<i class="fa-solid fa-hammer bukkit-blue fa-width-auto" aria-hidden="true"></i>',
|
||||
'intro' => 'Help shape TrueOG by building spawn areas, arenas, and community projects.',
|
||||
'requirements' => 'You must be at least 13 years old to apply.',
|
||||
'fields' => [
|
||||
...$identity,
|
||||
$playtime,
|
||||
field('portfolio', 'Links to screenshots or videos of your builds', 'textarea', true, [], 'Image hosts, YouTube, PlanetMinecraft, or anywhere else we can see your work.'),
|
||||
field('style', 'Which styles and scales of building are you best at?', 'textarea'),
|
||||
field('experience', 'Have you built for other servers or build teams? Tell us about it.', 'textarea', false),
|
||||
field('timezone', 'What timezone are you in, and when are you usually available?', 'textarea'),
|
||||
$age,
|
||||
field('agree', "Builders $removable", 'agree', true, ['I understand.']),
|
||||
],
|
||||
],
|
||||
|
||||
'moderator' => [
|
||||
'title' => 'Moderator Application',
|
||||
'icon' => '<i class="fa-solid fa-heart bukkit-red fa-width-auto" aria-hidden="true"></i>',
|
||||
'intro' => 'Apply below to become a staff member on TrueOG and help the community have the best gameplay experience possible!',
|
||||
'requirements' => 'Required: 13 years old. Working microphone. English fluency. Other languages are a bonus (especially German and Arabic).',
|
||||
'fields' => [
|
||||
...$identity,
|
||||
$playtime,
|
||||
field('past_staff', 'Have you been staff for any other servers in the past?', 'radio', true, ['Yes', 'No']),
|
||||
field('current_staff', 'Are you currently staff on any other Minecraft servers?', 'radio', true, ['Yes', 'No']),
|
||||
field('why', 'Why do you want to be a moderator?', 'textarea'),
|
||||
field('timezone', 'What timezone are you in, and when do you usually log on?', 'textarea'),
|
||||
field('community', 'What experience do you have with managing a community?', 'textarea'),
|
||||
field('reputation', 'Does the community have a positive opinion of you? Why or why not?', 'textarea'),
|
||||
field('other_roles', 'Are you interested in any other roles in addition to moderating?', 'checkbox', true, ['Building', 'Managing Community Events', 'Streaming/Making videos', 'Software Development', 'Systems Administration', 'None of these']),
|
||||
$age,
|
||||
field('agree', "Moderators $removable", 'agree', true, ['I understand.']),
|
||||
],
|
||||
],
|
||||
|
||||
'homes' => [
|
||||
'title' => 'Home Recovery',
|
||||
'icon' => '<i class="fa-solid fa-house bukkit-green fa-width-auto" aria-hidden="true"></i>',
|
||||
'intro' => 'Essentials playerdata was unfortunately not saved from OG:SMP Season 1. TrueOG staff can still attempt manual re-assignment of your homes, assuming you remember where they are located.',
|
||||
'any' => ['bed', 'home1', 'home2', 'home3', 'home4'],
|
||||
'fields' => [
|
||||
$identity[0],
|
||||
field('discord', 'Discord Username', 'text', false),
|
||||
field('bed', 'Bed Coordinates (X, Y, Z)', 'text', false),
|
||||
field('home1', 'Home 1 Coordinates (X, Y, Z)', 'text', false),
|
||||
field('home2', 'Home 2 Coordinates (X, Y, Z)', 'text', false),
|
||||
field('home3', 'Home 3 Coordinates (X, Y, Z)', 'text', false),
|
||||
field('home4', 'Home 4 Coordinates (X, Y, Z)', 'text', false),
|
||||
field('agree', 'We will do our best to verify your ownership of past homes, but if the locations provided cannot be tied to your username or group, you will not be re-assigned to them.', 'agree', true, ['I understand.']),
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
/* Returns [$values, $errors], both keyed by field name. */
|
||||
function validate(array $fields, array $post): array {
|
||||
|
||||
$values = [];
|
||||
$errors = [];
|
||||
$controls = implode('', array_map('chr', range(0, 31))) . "\x7F"; // ASCII control characters.
|
||||
|
||||
foreach ($fields as $f) {
|
||||
|
||||
$name = $f['name'];
|
||||
$raw = $post[$name] ?? '';
|
||||
$multi = $f['type'] === 'checkbox';
|
||||
$value = $multi ? array_values(array_unique(array_intersect(array_filter(is_array($raw) ? $raw : [], 'is_string'), $f['options']))) : (is_string($raw) ? trim(str_replace("\r\n", "\n", $raw)) : '');
|
||||
$values[$name] = $value;
|
||||
|
||||
if ($value === '' || $value === []) {
|
||||
|
||||
if ($f['required']) {
|
||||
|
||||
$errors[$name] = $f['type'] === 'agree' ? 'Please tick the box to continue.' : 'This question is required.';
|
||||
|
||||
}
|
||||
|
||||
continue;
|
||||
|
||||
}
|
||||
|
||||
$limit = $f['type'] === 'textarea' ? 800 : 200;
|
||||
$error = match (true) {
|
||||
$multi => null,
|
||||
!mb_check_encoding($value, 'UTF-8') => 'Please use plain text.',
|
||||
strpbrk($value, $f['type'] === 'textarea' ? str_replace(["\n", "\t"], '', $controls) : $controls) !== false => 'Please remove control characters' . ($f['type'] === 'textarea' ? '.' : ' and line breaks.'),
|
||||
in_array($f['type'], ['radio', 'agree'], true) && !in_array($value, $f['options'], true) => 'Please pick one of the options.',
|
||||
$f['type'] === 'number' && (!ctype_digit($value) || (int) $value < 13 || (int) $value > 120) => 'Please enter your age as a whole number. You must be 13 or older.',
|
||||
$f['type'] === 'url' && (!in_array(strtolower((string) parse_url($value, PHP_URL_SCHEME)), ['http', 'https'], true) || !parse_url($value, PHP_URL_HOST) || str_contains($value, ' ')) => 'Please enter a full link, starting with http:// or https://.',
|
||||
$name === 'minecraft' && (strlen($value) < 3 || strlen($value) > 16 || !ctype_alnum(strtr($value, '_', 'a'))) => 'A Minecraft username is 3 to 16 letters, numbers, or underscores.',
|
||||
mb_strlen($value) > $limit => "Please keep this to $limit characters or fewer.",
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($error !== null) {
|
||||
|
||||
$errors[$name] = $error;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return [$values, $errors];
|
||||
|
||||
}
|
||||
|
||||
/* [question, answer] rows in form order, ending with when and where it was submitted from. */
|
||||
function answers(array $fields, array $values): array {
|
||||
|
||||
$rows = [];
|
||||
|
||||
foreach ($fields as $f) {
|
||||
|
||||
$value = $values[$f['name']];
|
||||
$value = is_array($value) ? implode(', ', $value) : $value;
|
||||
$rows[] = [$f['label'], $value === '' ? '(no answer)' : $value];
|
||||
|
||||
}
|
||||
|
||||
$rows[] = ['Submitted', gmdate('Y-m-d H:i') . ' UTC from ' . client_ip()];
|
||||
|
||||
return $rows;
|
||||
|
||||
}
|
||||
|
||||
/* One embed per application (the field limits keep it inside Discord's caps), plus a reminder if the fallback file has content. */
|
||||
function send_discord(string $title, array $rows): bool {
|
||||
|
||||
['discord_webhook' => $webhook, 'applications_file' => $file] = site_config();
|
||||
$fields = array_map(fn (array $row): array => ['name' => mb_strimwidth($row[0], 0, 256, '...'), 'value' => $row[1]], $rows);
|
||||
$sent = http_fetch($webhook, ['embeds' => [['title' => $title, 'color' => 0xAA00AA, 'fields' => $fields]], 'allowed_mentions' => ['parse' => []]]) !== null;
|
||||
$pending = (int) @filesize($file);
|
||||
|
||||
if ($sent && $pending > 0) {
|
||||
|
||||
http_fetch($webhook, ['content' => "Applications were saved to $file while Discord was down ($pending bytes). Read it on the server, then delete it.", 'allowed_mentions' => ['parse' => []]]);
|
||||
|
||||
}
|
||||
|
||||
return $sent;
|
||||
|
||||
}
|
||||
|
||||
/* Fallback when Discord is down: appends the application to applications_file, up to 5 MB. */
|
||||
function save_application(string $title, array $rows): bool {
|
||||
|
||||
$file = site_config()['applications_file'];
|
||||
$text = "=== $title ===\n";
|
||||
|
||||
foreach ($rows as [$question, $answer]) {
|
||||
|
||||
$text .= "$question: " . str_replace("\n", "\n ", $answer) . "\n";
|
||||
|
||||
}
|
||||
|
||||
$saved = (int) @filesize($file) < 5_000_000 && @file_put_contents($file, "$text\n", FILE_APPEND | LOCK_EX) !== false;
|
||||
error_log('site.php: Discord webhook failed; application ' . ($saved ? "saved to $file" : "LOST, $file is full or not writable"));
|
||||
|
||||
return $saved;
|
||||
|
||||
}
|
||||
|
||||
/* Takes the sender's one-a-minute slot under a lock, so parallel posts cannot share it. */
|
||||
function reserve(string $slot): bool {
|
||||
|
||||
$handle = @fopen($slot, 'c+');
|
||||
|
||||
if ($handle === false || !flock($handle, LOCK_EX)) {
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
$stat = fstat($handle);
|
||||
$free = $stat['size'] === 0 || $stat['mtime'] <= time() - 60;
|
||||
|
||||
if ($free) {
|
||||
|
||||
ftruncate($handle, 0);
|
||||
fwrite($handle, '1');
|
||||
|
||||
}
|
||||
|
||||
flock($handle, LOCK_UN);
|
||||
fclose($handle);
|
||||
|
||||
return $free;
|
||||
|
||||
}
|
||||
|
||||
/* /apply.php lists the forms; ?form=<key> shows one and posts it to Discord, or the fallback file if Discord is down. */
|
||||
function apply_page(): void {
|
||||
|
||||
$https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https';
|
||||
session_set_cookie_params(['httponly' => true, 'samesite' => 'Lax', 'secure' => $https, 'path' => '/']);
|
||||
session_start();
|
||||
$_SESSION['csrf'] ??= bin2hex(random_bytes(16));
|
||||
|
||||
$forms = forms();
|
||||
$key = isset($_GET['form']) && is_string($_GET['form']) ? $_GET['form'] : '';
|
||||
$form = $forms[$key] ?? null;
|
||||
$open = forms_open();
|
||||
$values = [];
|
||||
$errors = [];
|
||||
$state = 'form';
|
||||
|
||||
if ($form && $open && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
[$values, $errors] = validate($form['fields'], $_POST);
|
||||
$dir = temp_dir();
|
||||
$slot = "$dir/apply-" . sha1($key . ip_bucket(client_ip()));
|
||||
$started = $_SESSION['started'][$key] ?? null;
|
||||
|
||||
if (($_POST['website'] ?? '') !== '') {
|
||||
|
||||
error_log('site.php: honeypot hit from ' . client_ip()); // A bot: pretend it worked.
|
||||
header("Location: /apply.php?form=$key&sent=1", true, 303);
|
||||
exit;
|
||||
|
||||
} elseif (!is_string($_POST['csrf'] ?? null) || !hash_equals($_SESSION['csrf'], $_POST['csrf'])) {
|
||||
|
||||
$errors['form'] = 'Your session expired. Please check your answers and send again.';
|
||||
|
||||
} elseif ($started === null || time() - $started < 3) {
|
||||
|
||||
$errors['form'] = 'That was quick. Please check your answers and send again.';
|
||||
|
||||
} elseif (isset($form['any']) && array_filter(array_intersect_key($values, array_flip($form['any']))) === []) {
|
||||
|
||||
$errors['form'] = 'Please give at least one set of coordinates.';
|
||||
|
||||
} elseif (count(array_filter(glob("$dir/apply-*") ?: [], fn (string $file): bool => filesize($file) > 0 && filemtime($file) > time() - 600)) >= 30) {
|
||||
|
||||
$errors['form'] = 'The forms are busy right now. Please try again in a few minutes.';
|
||||
|
||||
} elseif ($errors === [] && !reserve($slot)) {
|
||||
|
||||
$errors['form'] = 'Please wait a minute before sending another application.';
|
||||
|
||||
}
|
||||
|
||||
if ($errors === []) {
|
||||
|
||||
$title = "New {$form['title']} from {$values['minecraft']}";
|
||||
$rows = answers($form['fields'], $values);
|
||||
|
||||
if (send_discord($title, $rows) || save_application($title, $rows)) {
|
||||
|
||||
$_SESSION['sent'] = [$values['minecraft'], $values['discord'] !== ''];
|
||||
$_SESSION['csrf'] = bin2hex(random_bytes(16));
|
||||
unset($_SESSION['started'][$key]);
|
||||
header("Location: /apply.php?form=$key&sent=1", true, 303);
|
||||
exit;
|
||||
|
||||
}
|
||||
|
||||
$state = 'failed';
|
||||
|
||||
}
|
||||
|
||||
} elseif ($form) {
|
||||
|
||||
$_SESSION['started'][$key] = time();
|
||||
$state = isset($_GET['sent']) ? 'sent' : 'form';
|
||||
|
||||
}
|
||||
|
||||
page_start($form ? $form['title'] : 'Apply', 'games page', 'apply');
|
||||
?>
|
||||
<div class="box">
|
||||
<div class="flex-child outlined">
|
||||
<h1><?php echo $form ? $form['icon'] : '<i class="fa-solid fa-file-signature bukkit-dark-purple fa-width-auto" aria-hidden="true"></i>'; ?> <?php echo $form ? '' : 'Apply to '; ?><span class="bukkit-green">True</span><span class="bukkit-red">OG</span><?php echo $form ? ' ' . e($form['title']) : ''; ?>:</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="flex-child panel outlined">
|
||||
<?php if (!$form) { ?>
|
||||
<p>Pick a form. Applications go straight to the staff team, who will reach out on Discord.</p>
|
||||
<?php if (!$open) { ?>
|
||||
<p class="notice">Applications are closed at the moment. Ask on <a href="https://discord.gg/ma9pMYpBU6">Discord</a> if you have questions.</p>
|
||||
<?php } ?>
|
||||
<div class="apply">
|
||||
<?php foreach ($forms as $k => $f) { ?>
|
||||
<a class="apply-button" href="/apply.php?form=<?php echo $k; ?>"><?php echo $f['icon']; ?> <?php echo e($f['title']); ?></a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } elseif ($state === 'sent') { [$who, $discord] = $_SESSION['sent'] ?? ['friend', true]; ?>
|
||||
<p class="bukkit-green">Thanks, <?php echo e($who); ?>! Your <?php echo e($form['title']); ?> has been sent to the staff team.</p>
|
||||
<p><?php echo $discord ? 'We will follow up on Discord, so make sure you are in the' : 'We will follow up in game, or on Discord if you join the'; ?> <a href="https://discord.gg/ma9pMYpBU6">TrueOG Discord</a>.</p>
|
||||
<?php } else { ?>
|
||||
<p><?php echo e($form['intro']); ?></p>
|
||||
<?php if (isset($form['requirements'])) { ?>
|
||||
<p class="bukkit-gold"><?php echo e($form['requirements']); ?></p>
|
||||
<?php } ?>
|
||||
<?php if (!$open) { ?>
|
||||
<p class="notice">Applications are closed at the moment. Ask on <a href="https://discord.gg/ma9pMYpBU6">Discord</a> if you have questions.</p>
|
||||
<?php } elseif ($state === 'failed') { ?>
|
||||
<p class="notice">Your application could not be sent. Please try again later, or reach out on <a href="https://discord.gg/ma9pMYpBU6">Discord</a>.</p>
|
||||
<?php } elseif (isset($errors['form'])) { ?>
|
||||
<p class="notice"><?php echo e($errors['form']); ?></p>
|
||||
<?php } elseif ($errors !== []) { ?>
|
||||
<p class="notice">Please fix the highlighted answers below.</p>
|
||||
<?php } ?>
|
||||
<form method="post" action="/apply.php?form=<?php echo e($key); ?>">
|
||||
<fieldset<?php echo $open ? '' : ' disabled'; ?>>
|
||||
<input type="hidden" name="csrf" value="<?php echo e($_SESSION['csrf']); ?>">
|
||||
<div class="honeypot" aria-hidden="true"><label>Leave this field empty <input type="text" name="website" tabindex="-1" autocomplete="off"></label></div>
|
||||
<?php foreach ($form['fields'] as $f) {
|
||||
$name = $f['name'];
|
||||
$value = $values[$name] ?? ($f['type'] === 'checkbox' ? [] : '');
|
||||
$label = e($f['label']) . ($f['required'] ? ' <span class="required" title="Required">*</span>' : '');
|
||||
$hint = $f['hint'] !== '' ? '<p class="hint" id="h-' . $name . '">' . e($f['hint']) . '</p>' : '';
|
||||
$describe = trim(($f['hint'] !== '' ? "h-$name " : '') . (isset($errors[$name]) ? "e-$name" : ''));
|
||||
$aria = $describe !== '' ? ' aria-describedby="' . $describe . '"' : '';
|
||||
$required = ($f['required'] && $f['type'] !== 'checkbox' ? ' required' : '') . (isset($errors[$name]) && $f['options'] === [] ? ' aria-invalid="true"' : '');
|
||||
?>
|
||||
<div class="field<?php echo isset($errors[$name]) ? ' invalid' : ''; ?>">
|
||||
<?php if ($f['options'] !== []) { ?>
|
||||
<fieldset<?php echo $aria; ?>>
|
||||
<legend class="question"><?php echo $label; ?></legend><?php echo $hint; ?>
|
||||
<?php foreach ($f['options'] as $option) { ?>
|
||||
<label class="choice"><input type="<?php echo $f['type'] === 'radio' ? 'radio' : 'checkbox'; ?>" name="<?php echo $name . ($f['type'] === 'checkbox' ? '[]' : ''); ?>" value="<?php echo e($option); ?>"<?php echo ($f['type'] === 'checkbox' ? in_array($option, $value, true) : $value === $option) ? ' checked' : ''; ?><?php echo $required; ?>> <?php echo e($option); ?></label>
|
||||
<?php } ?>
|
||||
</fieldset>
|
||||
<?php } elseif ($f['type'] === 'textarea') { ?>
|
||||
<label class="question" for="f-<?php echo $name; ?>"><?php echo $label; ?></label><?php echo $hint; ?>
|
||||
<textarea id="f-<?php echo $name; ?>" name="<?php echo $name; ?>" rows="5" maxlength="800"<?php echo $required . $aria; ?>><?php echo e($value); ?></textarea>
|
||||
<?php } elseif ($f['type'] === 'number') { ?>
|
||||
<label class="question" for="f-<?php echo $name; ?>"><?php echo $label; ?></label><?php echo $hint; ?>
|
||||
<input type="number" id="f-<?php echo $name; ?>" name="<?php echo $name; ?>" value="<?php echo e($value); ?>" min="13" max="120"<?php echo $required . $aria; ?>>
|
||||
<?php } else { ?>
|
||||
<label class="question" for="f-<?php echo $name; ?>"><?php echo $label; ?></label><?php echo $hint; ?>
|
||||
<input type="<?php echo $f['type'] === 'url' ? 'url' : 'text'; ?>" id="f-<?php echo $name; ?>" name="<?php echo $name; ?>" value="<?php echo e($value); ?>"<?php echo $name === 'minecraft' ? ' maxlength="16"' : ' maxlength="200"'; ?><?php echo $required . $aria; ?>>
|
||||
<?php } ?>
|
||||
<?php if (isset($errors[$name])) { ?>
|
||||
<p class="error" id="e-<?php echo $name; ?>"><?php echo e($errors[$name]); ?></p>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<?php if ($open) { ?>
|
||||
<button type="submit" class="submit"><i class="fa-solid fa-paper-plane fa-width-auto" aria-hidden="true"></i> Send Application</button>
|
||||
<p class="hint">Your answers, IP address, and the time are sent to the staff Discord. See the <a href="/privacy/">Privacy Policy</a>.</p>
|
||||
<?php } ?>
|
||||
</fieldset>
|
||||
</form>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
page_end();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
/* Linked from the staff panel. */
|
||||
header('Location: /apply.php', true, 302);
|
||||
+773
-423
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
require dirname(__DIR__) . '/site.php';
|
||||
policy_page('terms');
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
/* Linked from the true-og README. */
|
||||
header('Location: https://todo.true-og.net/b/5aWr7xHofbbLPnh9C/kanban', true, 302);
|
||||
Reference in new issue
Block a user