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