Initial code commit
This commit is contained in:
parent
3eff0536c9
commit
0d1c3e63d8
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
define('IX_ENVBASE', 'SITE');
|
||||||
|
define('IX_BASE', dirname(__FILE__));
|
||||||
|
require_once(IX_BASE . '/vendor/autoload.php');
|
||||||
|
|
||||||
|
use ix\HookMachine;
|
||||||
|
use ix\Container\Container;
|
||||||
|
use ix\Controller\Controller;
|
||||||
|
use ix\Application\Application;
|
||||||
|
|
||||||
|
/* Language initialization */
|
||||||
|
(new \i18n(IX_BASE . '/lang/{LANGUAGE}.ini', IX_BASE . '/cache/lang', 'en'))->init();
|
||||||
|
|
||||||
|
/* Container hooks */
|
||||||
|
HookMachine::add([Container::class, 'construct'], '\ix\Container\ContainerHooksHtmlRenderer::hookContainerHtmlRenderer');
|
||||||
|
HookMachine::add([Container::class, 'construct'], '\ix\Container\ContainerHooksSession::hookContainerSession');
|
||||||
|
HookMachine::add([Container::class, 'construct'], '\ix\Container\ContainerHooksEasyCSRF::hookContainerEasyCSRFSession');
|
||||||
|
|
||||||
|
/* CSRF error */
|
||||||
|
HookMachine::add([Controller::class, 'request', 'invalidCSRFToken'], '\ix\Controller\ControllerHookInvalidCSRFTokenErrorPage::hookControllerInvalidCSRFToken');
|
||||||
|
|
||||||
|
/* Application alerters */
|
||||||
|
HookMachine::add([\NeotelApply\IndexController::class, 'sendAlert'], '\NeotelApply\ApplicationAlerters::pushover');
|
||||||
|
HookMachine::add([\NeotelApply\IndexController::class, 'sendAlert'], '\NeotelApply\ApplicationAlerters::discord');
|
||||||
|
|
||||||
|
/* Application routes */
|
||||||
|
HookMachine::add([Application::class, 'create_app', 'routeRegister'], (function ($key, $app) {
|
||||||
|
$app->redirect('/', '/register', 301);
|
||||||
|
$app->any('/register', \NeotelApply\IndexController::class);
|
||||||
|
return $app;
|
||||||
|
}));
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"name": "u1f408/neotel-application",
|
||||||
|
"description": "tel.tilde.org.nz application form",
|
||||||
|
"type": "project",
|
||||||
|
"private": true,
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Iris System",
|
||||||
|
"email": "iris@iris.ac.nz"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"type": "vcs",
|
||||||
|
"url": "https://github.com/u1f408/ix-framework"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"ext-curl": "*",
|
||||||
|
"ext-json": "*",
|
||||||
|
"u1f408/ix-framework": "dev-main",
|
||||||
|
"vlucas/phpdotenv": "^5.3",
|
||||||
|
"serhiy/pushover": "^1.2"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/phpstan": "^0.12.87",
|
||||||
|
"phpunit/phpunit": "^9.5"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"NeotelApply\\": "lib/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,53 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
namespace NeotelApply;
|
||||||
|
|
||||||
|
use IrisHelpers\CurlHelpers;
|
||||||
|
use Serhiy\Pushover;
|
||||||
|
use Serhiy\Pushover\Api\Message\Message as PushoverMessage;
|
||||||
|
use Serhiy\Pushover\Api\Message\Notification as PushoverNotification;
|
||||||
|
|
||||||
|
final class ApplicationAlerters {
|
||||||
|
/**
|
||||||
|
* @param string[] $key Hook key (unused)
|
||||||
|
* @param mixed[] $params Array of [user, tilde, message]
|
||||||
|
* @return mixed[] Array of [user, tilde, message]
|
||||||
|
*/
|
||||||
|
public static function pushover(array $key, array $params): array {
|
||||||
|
list($user, $tilde, $message) = $params;
|
||||||
|
|
||||||
|
$application = new Pushover\Application($_ENV[IX_ENVBASE . '_PUSHOVER_API_TOKEN']);
|
||||||
|
$recipient = new Pushover\Recipient($_ENV[IX_ENVBASE . '_PUSHOVER_USER_KEY']);
|
||||||
|
$pushovermessage = new PushoverMessage(
|
||||||
|
"from {$user}@{$tilde}\n> {$message}",
|
||||||
|
'new neotel application',
|
||||||
|
);
|
||||||
|
$notification = new PushoverNotification($application, $recipient, $pushovermessage);
|
||||||
|
$response = $notification->push();
|
||||||
|
|
||||||
|
return [$user, $tilde, $message];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string[] $key Hook key (unused)
|
||||||
|
* @param mixed[] $params Array of [user, tilde, message]
|
||||||
|
* @return mixed[] Array of [user, tilde, message]
|
||||||
|
*/
|
||||||
|
public static function discord(array $key, array $params): array {
|
||||||
|
list($user, $tilde, $message) = $params;
|
||||||
|
|
||||||
|
$data = json_encode([
|
||||||
|
"content" => "From `{$user}@{$tilde}` \n> {$message}",
|
||||||
|
]);
|
||||||
|
|
||||||
|
$curl_opts = [
|
||||||
|
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
|
||||||
|
CURLOPT_POST => 1,
|
||||||
|
CURLOPT_POSTFIELDS => $data,
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = CurlHelpers::fetchUrl($_ENV[IX_ENVBASE . "_DISCORD_WEBHOOK_URL"], $curl_opts, true);
|
||||||
|
|
||||||
|
return [$user, $tilde, $message];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
namespace NeotelApply;
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
|
||||||
|
class Configuration {
|
||||||
|
/** @var ?Dotenv $dotenv */
|
||||||
|
private static $dotenv = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or return a Dotenv instance
|
||||||
|
*
|
||||||
|
* @param string $mode
|
||||||
|
* @return Dotenv
|
||||||
|
*/
|
||||||
|
public static function load(string $mode = 'base'): Dotenv {
|
||||||
|
if (self::$dotenv === null) {
|
||||||
|
self::$dotenv = self::create($mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$dotenv;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new Dotenv instance
|
||||||
|
*
|
||||||
|
* @param string $mode
|
||||||
|
* @return Dotenv
|
||||||
|
*/
|
||||||
|
private static function create(string $mode = 'base'): Dotenv {
|
||||||
|
// Construct and load Dotenv
|
||||||
|
$dotenv = Dotenv::createImmutable(IX_BASE);
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
// Database shit
|
||||||
|
$dotenv->required('REDIS_URL')->notEmpty();
|
||||||
|
|
||||||
|
// Require an environment
|
||||||
|
$dotenv->required('APP_ENV')->allowedValues(['development', 'test', 'production']);
|
||||||
|
|
||||||
|
// Require a session cookie
|
||||||
|
$dotenv->required(IX_ENVBASE . '_SESSIONCOOKIE')->notEmpty();
|
||||||
|
|
||||||
|
// Pushover application and user keys
|
||||||
|
$dotenv->required(IX_ENVBASE . '_PUSHOVER_API_TOKEN')->notEmpty();
|
||||||
|
$dotenv->required(IX_ENVBASE . '_PUSHOVER_USER_KEY')->notEmpty();
|
||||||
|
|
||||||
|
// Discord webhook URL
|
||||||
|
$dotenv->required(IX_ENVBASE . '_DISCORD_WEBHOOK_URL')->notEmpty();
|
||||||
|
|
||||||
|
return $dotenv;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,136 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
namespace NeotelApply;
|
||||||
|
|
||||||
|
use ix\HookMachine;
|
||||||
|
use ix\Controller\Controller;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Slim\Exception\HttpInternalServerErrorException;
|
||||||
|
|
||||||
|
class IndexController extends Controller {
|
||||||
|
/**
|
||||||
|
* @param Request $request The Request object
|
||||||
|
* @param Response $response The Response object
|
||||||
|
* @param mixed[] $args Arguments passed from the router (if any)
|
||||||
|
* @return Response The resulting Response object
|
||||||
|
*/
|
||||||
|
public function requestGET(Request $request, Response $response, ?array $args = []): Response {
|
||||||
|
$session = $this->container->get('session')->ensure_create()->retrieve();
|
||||||
|
if (!array_key_exists('numbers', $session->session_data)) {
|
||||||
|
$session->session_data['numbers'] = implode(',', [
|
||||||
|
random_int(1, 9),
|
||||||
|
random_int(1, 9),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$numbers = array_map((function($i) { return intval($i); }), explode(',', $session->session_data['numbers']));
|
||||||
|
$csrf_token = $this->container->get('csrf')->generate('csrf');
|
||||||
|
$html = $this->container->get('html');
|
||||||
|
|
||||||
|
$messageBanner = null;
|
||||||
|
if (array_key_exists('message', $args)) {
|
||||||
|
$messageBanner = $html->tagHasChildren('div', ['class' => 'message'], $args['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response->getBody()->write($html->renderDocument(
|
||||||
|
[
|
||||||
|
$html->tag('meta', ['charset' => 'utf-8']),
|
||||||
|
$html->tag('meta', ['name' => 'viewport', 'content' => 'initial-scale=1, width=device-width']),
|
||||||
|
$html->tag('link', ['rel' => 'stylesheet', 'href' => 'styles.css']),
|
||||||
|
$html->tagHasChildren('title', [], 'register for tel.tilde.org.nz'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
$html->tagHasChildren('main', [], ...[
|
||||||
|
$html->tagHasChildren('h1', [], ...[
|
||||||
|
'register for ',
|
||||||
|
$html->tagHasChildren('a', ['href' => 'https://tel.tilde.org.nz'], 'tel.tilde.org.nz'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
$messageBanner ?? '',
|
||||||
|
|
||||||
|
$html->tagHasChildren('p', [], ...[
|
||||||
|
"If you previously had a tilde.tel extension, please mention that in your application!",
|
||||||
|
]),
|
||||||
|
|
||||||
|
$html->tagHasChildren('form', ['class' => 'form', 'method' => 'POST'], ...[
|
||||||
|
$html->tag('input', ['type' => 'hidden', 'name' => '_csrf', 'value' => strval($csrf_token)]),
|
||||||
|
$html->tagHasChildren('label', ['for' => 'username'], 'Username (required)'),
|
||||||
|
$html->tag('input', ['id' => 'username', 'name' => 'username', 'type' => 'text', 'required' => 'required']),
|
||||||
|
$html->tagHasChildren('label', ['for' => 'tilde'], 'Your tilde/pubnix (required)'),
|
||||||
|
$html->tag('input', ['id' => 'tilde', 'name' => 'tilde', 'type' => 'text', 'required' => 'required']),
|
||||||
|
$html->tagHasChildren('label', ['for' => 'message'], 'Anything you want to mention?'),
|
||||||
|
$html->tagHasChildren('textarea', ['id' => 'message', 'name' => 'message'], ''),
|
||||||
|
$html->tagHasChildren('label', ['for' => 'verify'], "What is {$numbers[0]} plus {$numbers[1]}?"),
|
||||||
|
$html->tag('input', ['id' => 'verify', 'name' => 'verify', 'type' => 'number', 'required' => 'required']),
|
||||||
|
$html->tagHasChildren('button', ['type' => 'submit'], 'Submit request'),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Request $request The Request object
|
||||||
|
* @param Response $response The Response object
|
||||||
|
* @param mixed[] $args Arguments passed from the router (if any)
|
||||||
|
* @return Response The resulting Response object
|
||||||
|
*/
|
||||||
|
public function requestPOST(Request $request, Response $response, ?array $args = []): Response {
|
||||||
|
$session = $this->container->get('session')->ensure_create()->retrieve();
|
||||||
|
if (!array_key_exists('numbers', $session->session_data)) {
|
||||||
|
return $response->withHeader('Location', '/register');
|
||||||
|
}
|
||||||
|
|
||||||
|
$numbers = array_map((function($i) { return intval($i); }), explode(',', $session->session_data['numbers']));
|
||||||
|
$query_values = (array) $request->getParsedBody();
|
||||||
|
|
||||||
|
// Check CSRF
|
||||||
|
$csrf_token = null;
|
||||||
|
if (array_key_exists('_csrf', $query_values)) $csrf_token = trim($query_values['_csrf']);
|
||||||
|
$this->container->get('csrf')->check('csrf', $csrf_token);
|
||||||
|
|
||||||
|
// Check the math question
|
||||||
|
$given_sum = null;
|
||||||
|
if (array_key_exists('verify', $query_values)) $given_sum = intval(trim($query_values['verify']));
|
||||||
|
if ($given_sum != $numbers[0] + $numbers[1]) {
|
||||||
|
return $this->requestGET($request, $response, array_merge($args, [
|
||||||
|
'message' => 'The provided verification was incorrect.',
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're good, send the notifications
|
||||||
|
HookMachine::execute([self::class, 'sendAlert'], [
|
||||||
|
$query_values['username'],
|
||||||
|
$query_values['tilde'],
|
||||||
|
$query_values['message'] ?? '[no message provided]',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// And return a success message
|
||||||
|
$html = $this->container->get('html');
|
||||||
|
$response->getBody()->write($html->renderDocument(
|
||||||
|
[
|
||||||
|
$html->tag('meta', ['charset' => 'utf-8']),
|
||||||
|
$html->tag('meta', ['name' => 'viewport', 'content' => 'initial-scale=1, width=device-width']),
|
||||||
|
$html->tag('link', ['rel' => 'stylesheet', 'href' => 'styles.css']),
|
||||||
|
$html->tagHasChildren('title', [], 'register for tel.tilde.org.nz'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
$html->tagHasChildren('main', [], ...[
|
||||||
|
$html->tagHasChildren('h1', [], ...[
|
||||||
|
'register for ',
|
||||||
|
$html->tagHasChildren('a', ['href' => 'https://tel.tilde.org.nz'], 'tel.tilde.org.nz'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
$html->tagHasChildren('div', ['class' => 'message'], ...[
|
||||||
|
"Your registration request has been submitted!",
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
parameters:
|
||||||
|
level: 7
|
||||||
|
|
||||||
|
bootstrapFiles:
|
||||||
|
- tests/bootstrap.php
|
||||||
|
paths:
|
||||||
|
- lib
|
||||||
|
- public
|
||||||
|
|
||||||
|
ignoreErrors:
|
||||||
|
# Let the i18n stuff fly
|
||||||
|
- '#Function L not found#'
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<phpunit backupGlobals="true" backupStaticAttributes="true" bootstrap="tests/bootstrap.php">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Main test suite">
|
||||||
|
<directory suffix="Test.php">./tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
<coverage>
|
||||||
|
<include>
|
||||||
|
<directory suffix=".php">./lib</directory>
|
||||||
|
</include>
|
||||||
|
</coverage>
|
||||||
|
</phpunit>
|
|
@ -0,0 +1,4 @@
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule ^ index.php [QSA,L]
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
require_once(dirname(dirname(($p = realpath(__FILE__)) === false ? __FILE__ : $p)) . '/bootstrap.php');
|
||||||
|
|
||||||
|
/* Hack to allow PHP development server to serve static files */
|
||||||
|
if (php_sapi_name() === 'cli-server') {
|
||||||
|
$fileName = __DIR__ . parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
|
||||||
|
if (file_exists($fileName) && !is_dir($fileName)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Load configuration */
|
||||||
|
\NeotelApply\Configuration::load();
|
||||||
|
|
||||||
|
/* Run application */
|
||||||
|
(new \ix\Application\Application())->create_app()->run();
|
|
@ -0,0 +1,75 @@
|
||||||
|
*,
|
||||||
|
*:before,
|
||||||
|
*:after
|
||||||
|
{
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body
|
||||||
|
{
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100vw;
|
||||||
|
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column wrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
main > *
|
||||||
|
{
|
||||||
|
max-width: calc(100vw - 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message
|
||||||
|
{
|
||||||
|
width: 100%;
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
|
||||||
|
color: #000;
|
||||||
|
background: #eee;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.message-error
|
||||||
|
{
|
||||||
|
background: #c22;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form
|
||||||
|
{
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form input:not([type = "checkbox"]),
|
||||||
|
.form select,
|
||||||
|
.form button,
|
||||||
|
.form textarea
|
||||||
|
{
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.25rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
|
||||||
|
border: 1px solid #000;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
define('IX_ENVBASE', 'SITE');
|
||||||
|
define('IX_BASE', dirname(__DIR__));
|
||||||
|
require_once(IX_BASE . '/vendor/autoload.php');
|
Loading…
Reference in New Issue