Files
livetrail/lib/Controller.php
T
2026-08-27 12:45:30 +02:00

171 lines
6.2 KiB
PHP

<?php
namespace Franzz\Livetrail;
use Franzz\Objects\PhpObject;
use Franzz\Objects\ToolBox;
//TODO Keep only local specificities and move bulk to Franzz\Objects\Controller
class Controller extends PhpObject {
private const MUTATING_ACTIONS = [
'add_post',
'subscribe',
'unsubscribe',
'login',
'logout',
'update_project',
'upload',
'add_comment',
'add_position',
'admin_set',
'admin_create',
'admin_delete'
];
private const SESSION_WRITING_ACTIONS = [
'login',
'logout'
];
private Livetrail $oLivetrail;
private array $asReq;
private string $sCsrfToken = '';
public function __construct() {
parent::__construct(__CLASS__);
}
private function setReqVal(string $sKey, $oValue, string $sValidation=''): void {
$this->asReq[$sKey] = $this->validateValue($sValidation, $oValue);
}
public function handle($sProcessPage, array $argv = []): string {
//Start buffering so warnings/notices can be collected
ob_start();
//Parse variables
$asReq = ToolBox::getRequest($argv);
$this->asReq = [];
$sAction = $asReq['a'] ?? '';
$this->setReqVal('t', $asReq['t'] ?? '');
$this->setReqVal('name', $asReq['name'] ?? '');
$this->setReqVal('content', $asReq['content'] ?? '');
$this->setReqVal('id_project', $asReq['id_project'] ?? 0, 'positiveInt');
$this->setReqVal('id', $asReq['id'] ?? 0);
$this->setReqVal('id_entity', $asReq['id'] ?? 0, 'positiveInt');
$this->setReqVal('field', $asReq['field'] ?? '');
$this->setReqVal('value', $asReq['value'] ?? '');
$this->setReqVal('type', $asReq['type'] ?? '');
$this->setReqVal('email', $asReq['email'] ?? '');
$this->setReqVal('password', $asReq['password'] ?? '');
$this->setReqVal('latitude', $asReq['latitude'] ?? '');
$this->setReqVal('longitude', $asReq['longitude'] ?? '');
$this->setReqVal('timestamp', $asReq['timestamp'] ?? 0, 'positiveInt');
$this->setReqVal('csrf_token', $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ($_POST['csrf_token'] ?? ''));
//Authentication and CSRF protection share the same server-side session.
$this->initCsrfToken();
//Create Livetrail instance
$this->oLivetrail = new Livetrail($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 = Livetrail::getJsonResult(false, Livetrail::UNAUTHORIZED);
else {
$this->oLivetrail->setProjectId($this->asReq['id_project']);
$sResult = ($sAction == '')?$this->oLivetrail->getAppMainPage($this->getCsrfToken()):$this->dispatch($sAction);
}
//Clean errors
$sDebug = ob_get_clean();
if($sDebug != '') $this->oLivetrail->addUncaughtError($sDebug);
$this->closeSession();
return $sResult;
}
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 setCsrfToken(): void {
if(empty($_SESSION['csrf_token'])) $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
$this->sCsrfToken = $_SESSION['csrf_token'];
}
private function initCsrfToken(): void {
if(PHP_SAPI === 'cli') return;
if(session_status() !== PHP_SESSION_ACTIVE) {
$bSecure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https');
session_set_cookie_params(['httponly' => true, 'secure' => $bSecure, 'samesite' => 'Lax']);
session_start();
}
$this->setCsrfToken();
}
private function checkCsrfToken(string $sClientToken): bool {
$sServerToken = $this->getCsrfToken();
return PHP_SAPI === 'cli' || ($sServerToken !== '' && is_string($sClientToken) && hash_equals($sServerToken, $sClientToken));
}
private function closeSession(): void {
if(session_status() === PHP_SESSION_ACTIVE) session_write_close();
}
private function dispatch(string $sAction): string {
return match($sAction) {
'markers' => $this->oLivetrail->getMarkers(),
'last_update' => $this->oLivetrail->getLastUpdate(),
'next_feed' => $this->oLivetrail->getNextFeed($this->asReq['id']),
'new_feed' => $this->oLivetrail->getNewFeed($this->asReq['id']),
'add_post' => $this->oLivetrail->addPost($this->asReq['name'], $this->asReq['content']),
'subscribe' => $this->oLivetrail->subscribe(),
'unsubscribe' => $this->oLivetrail->unsubscribe(),
'login' => $this->oLivetrail->login($this->asReq['email'], $this->asReq['password'], $this->asReq['name']),
'logout' => $this->oLivetrail->logout(),
'update_project' => $this->oLivetrail->updateProject(),
default => $this->dispatchAdmin($sAction)
};
}
private function dispatchAdmin(string $sAction): string {
if(!$this->oLivetrail->checkUserClearance(User::CLEARANCE_ADMIN)) {
return Livetrail::getJsonResult(false, Livetrail::NOT_FOUND);
}
return match($sAction) {
'upload' => $this->oLivetrail->upload(),
'add_comment' => $this->oLivetrail->addComment($this->asReq['id_entity'], $this->asReq['content']),
'add_position' => $this->oLivetrail->addPosition($this->asReq['latitude'], $this->asReq['longitude'], $this->asReq['timestamp']),
'admin_get' => $this->oLivetrail->getAdminSettings(),
'admin_set' => $this->oLivetrail->setAdminSettings($this->asReq['type'], $this->asReq['id_entity'], $this->asReq['field'], $this->asReq['value']),
'admin_create' => $this->oLivetrail->createAdminSettings($this->asReq['type']),
'admin_delete' => $this->oLivetrail->deleteAdminSettings($this->asReq['type'], $this->asReq['id_entity']),
'sql' => $this->oLivetrail->getDbBuildScript(),
default => Livetrail::getJsonResult(false, Livetrail::NOT_FOUND)
};
}
private static function validateValue(string $sValidation, $oValue=0) {
return match($sValidation) {
'' => $oValue,
'positiveInt' => filter_var($oValue, FILTER_VALIDATE_INT, ['options' => ['default' => 0, 'min_range' => 0]])
};
}
}