Files
daydream/lib/Controller.php
2026-09-04 11:09:34 +02:00

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]]);
}
}