151 lines
5.0 KiB
PHP
151 lines
5.0 KiB
PHP
<?php
|
|
|
|
namespace Franzz\Daydream;
|
|
|
|
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 Daydream $oDaydream;
|
|
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->oDaydream = new Daydream($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 = Daydream::getJsonResult(false, Daydream::UNAUTHORIZED);
|
|
else $sResult = ($sAction == '') ? $this->oDaydream->getAppMainPage($this->getCsrfToken()) : $this->dispatch($sAction);
|
|
|
|
//Clean errors
|
|
$sDebug = ob_get_clean();
|
|
if($sDebug != '') $this->oDaydream->addUncaughtError($sDebug);
|
|
$this->closeSession();
|
|
|
|
return $sResult;
|
|
}
|
|
|
|
private function dispatch(string $sAction): string {
|
|
$oJournal = $this->oDaydream->getJournal();
|
|
|
|
return match($sAction) {
|
|
/* Account */
|
|
'signup' => $this->oDaydream->signup($this->asReq['name'], $this->asReq['email'], $this->asReq['password'], $this->asReq['t']),
|
|
'login' => $this->oDaydream->login($this->asReq['email'], $this->asReq['password'], $this->asReq['t'], $this->asReq['remember']),
|
|
'logout' => $this->oDaydream->logout(),
|
|
'account' => $this->oDaydream->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 => Daydream::getJsonResult(false, Daydream::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]]);
|
|
}
|
|
}
|