v3 init push
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\MyThoughts;
|
||||
|
||||
use Franzz\Objects\PhpObject;
|
||||
use Franzz\Objects\ToolBox;
|
||||
|
||||
/**
|
||||
* Single entry point: parses the request, guards it, dispatches it.
|
||||
*/
|
||||
class Controller extends PhpObject {
|
||||
//Anything that changes state must arrive as POST with a valid CSRF token.
|
||||
private const MUTATING_ACTIONS = [
|
||||
'signup',
|
||||
'login',
|
||||
'logout',
|
||||
'account',
|
||||
'open_entry',
|
||||
'save_entry',
|
||||
'close_entry',
|
||||
'delete_entry'
|
||||
];
|
||||
|
||||
//Actions that need the session lock held while they run.
|
||||
private const SESSION_WRITING_ACTIONS = [
|
||||
'signup',
|
||||
'login',
|
||||
'logout'
|
||||
];
|
||||
|
||||
private MyThoughts $oMyThoughts;
|
||||
private array $asReq = [];
|
||||
private string $sCsrfToken = '';
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct(__CLASS__);
|
||||
}
|
||||
|
||||
public function handle($sProcessPage, array $argv = []): string {
|
||||
//Start buffering so warnings/notices can be collected
|
||||
ob_start();
|
||||
|
||||
$asReq = ToolBox::getRequest($argv);
|
||||
$sAction = $asReq['a'] ?? '';
|
||||
|
||||
$this->asReq = [
|
||||
't' => (string) ($asReq['t'] ?? ''),
|
||||
'id' => self::positiveInt($asReq['id'] ?? 0),
|
||||
'dir' => (string) ($asReq['dir'] ?? ''),
|
||||
'date' => (string) ($asReq['date'] ?? ''),
|
||||
'content' => (string) ($asReq['content'] ?? ''),
|
||||
'has_content'=> array_key_exists('content', $asReq),
|
||||
'name' => (string) ($asReq['name'] ?? ''),
|
||||
'email' => (string) ($asReq['email'] ?? ''),
|
||||
'password' => (string) ($asReq['password'] ?? ''),
|
||||
'remember' => !empty($asReq['remember']),
|
||||
'field' => (string) ($asReq['field'] ?? ''),
|
||||
'value' => (string) ($asReq['value'] ?? ''),
|
||||
//sendBeacon cannot set headers, so the unload close falls back to
|
||||
//carrying the token in the body.
|
||||
'csrf_token'=> (string) ($_SERVER['HTTP_X_CSRF_TOKEN'] ?? ($_POST['csrf_token'] ?? ''))
|
||||
];
|
||||
|
||||
//Authentication and CSRF protection share the same server-side session.
|
||||
$this->initCsrfToken();
|
||||
|
||||
$this->oMyThoughts = new MyThoughts($sProcessPage, $this->asReq['t']);
|
||||
|
||||
//Validate CSRF, then release the session lock before long-running work.
|
||||
$bValidMutationRequest = $this->validateMutationRequest($sAction);
|
||||
if(!$bValidMutationRequest || !in_array($sAction, self::SESSION_WRITING_ACTIONS, true)) $this->closeSession();
|
||||
|
||||
if(!$bValidMutationRequest) $sResult = MyThoughts::getJsonResult(false, MyThoughts::UNAUTHORIZED);
|
||||
else $sResult = ($sAction == '') ? $this->oMyThoughts->getAppMainPage($this->getCsrfToken()) : $this->dispatch($sAction);
|
||||
|
||||
//Clean errors
|
||||
$sDebug = ob_get_clean();
|
||||
if($sDebug != '') $this->oMyThoughts->addUncaughtError($sDebug);
|
||||
$this->closeSession();
|
||||
|
||||
return $sResult;
|
||||
}
|
||||
|
||||
private function dispatch(string $sAction): string {
|
||||
$oJournal = $this->oMyThoughts->getJournal();
|
||||
|
||||
return match($sAction) {
|
||||
/* Account */
|
||||
'signup' => $this->oMyThoughts->signup($this->asReq['name'], $this->asReq['email'], $this->asReq['password'], $this->asReq['t']),
|
||||
'login' => $this->oMyThoughts->login($this->asReq['email'], $this->asReq['password'], $this->asReq['t'], $this->asReq['remember']),
|
||||
'logout' => $this->oMyThoughts->logout(),
|
||||
'account' => $this->oMyThoughts->updateAccount($this->asReq['field'], $this->asReq['value']),
|
||||
|
||||
/* Reading the book */
|
||||
'book' => $oJournal->getBook(),
|
||||
'entries' => $oJournal->getEntries($this->asReq['dir'], $this->asReq['id']),
|
||||
'date' => $oJournal->getEntryIdAtDate($this->asReq['date']),
|
||||
|
||||
/* Writing in it */
|
||||
'open_entry' => $oJournal->openEntry(),
|
||||
'save_entry' => $oJournal->saveEntry($this->asReq['id'], $this->asReq['content']),
|
||||
'close_entry' => $oJournal->closeEntry($this->asReq['id'], $this->asReq['content'], $this->asReq['has_content']),
|
||||
'delete_entry' => $oJournal->deleteEntry($this->asReq['id']),
|
||||
|
||||
default => MyThoughts::getJsonResult(false, MyThoughts::NOT_FOUND)
|
||||
};
|
||||
}
|
||||
|
||||
/* CSRF & session */
|
||||
|
||||
private function validateMutationRequest(string $sAction): bool {
|
||||
return
|
||||
PHP_SAPI === 'cli'
|
||||
||
|
||||
!in_array($sAction, self::MUTATING_ACTIONS, true)
|
||||
||
|
||||
(($_SERVER['REQUEST_METHOD'] ?? '') === 'POST' && $this->checkCsrfToken($this->asReq['csrf_token']))
|
||||
;
|
||||
}
|
||||
|
||||
private function getCsrfToken(): string {
|
||||
if($this->sCsrfToken === '') $this->initCsrfToken();
|
||||
return $this->sCsrfToken;
|
||||
}
|
||||
|
||||
private function initCsrfToken(): void {
|
||||
if(PHP_SAPI === 'cli') return;
|
||||
|
||||
if(session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_set_cookie_params(['httponly' => true, 'secure' => User::isSecureRequest(), 'samesite' => 'Lax']);
|
||||
session_start();
|
||||
}
|
||||
|
||||
if(empty($_SESSION['csrf_token'])) $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
$this->sCsrfToken = $_SESSION['csrf_token'];
|
||||
}
|
||||
|
||||
private function checkCsrfToken(string $sClientToken): bool {
|
||||
$sServerToken = $this->getCsrfToken();
|
||||
return PHP_SAPI === 'cli' || ($sServerToken !== '' && $sClientToken !== '' && hash_equals($sServerToken, $sClientToken));
|
||||
}
|
||||
|
||||
private function closeSession(): void {
|
||||
if(session_status() === PHP_SESSION_ACTIVE) session_write_close();
|
||||
}
|
||||
|
||||
private static function positiveInt($oValue): int {
|
||||
return filter_var($oValue, FILTER_VALIDATE_INT, ['options' => ['default' => 0, 'min_range' => 0]]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user