Restructure project folders and remove obsolete files
All checks were successful
Deploy Spot / deploy (push) Successful in 34s

This commit is contained in:
2026-05-30 01:32:20 +02:00
parent c2685a2731
commit 034d02f042
65 changed files with 382 additions and 2165 deletions

168
lib/Controller.php Normal file
View File

@@ -0,0 +1,168 @@
<?php
namespace Franzz\Spot;
use Franzz\Objects\PhpObject;
use Franzz\Objects\ToolBox;
//TODO Keep only local specificities and move bulk to Franzz\Objects\Controller
class Controller extends PhpObject
{
const MUTATING_ACTIONS = array(
'add_post',
'subscribe',
'unsubscribe',
'update_project',
'upload',
'add_comment',
'add_position',
'admin_set',
'admin_create',
'admin_delete',
'build_geojson'
);
private Spot $oSpot;
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 = array()): string
{
//Start buffering so warnings/notices can be collected
ob_start();
//Parse variables
$asReq = ToolBox::getRequest($argv);
$this->asReq = array();
$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('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'] ?? ''));
//Create Spot Instance
$this->oSpot = new Spot($sProcessPage, $this->asReq['t']);
$this->oSpot->setProjectId($this->asReq['id_project']);
//Validate CSRF & dispatch
if(!$this->validateMutationRequest($sAction)) $sResult = Spot::getJsonResult(false, Spot::UNAUTHORIZED);
elseif($sAction == '') $sResult = $this->oSpot->getAppMainPage($this->getCsrfToken());
else $sResult = $this->dispatch($sAction);
//Clean errors
$sDebug = ob_get_clean();
if($sDebug != '') $this->oSpot->addUncaughtError($sDebug);
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;
$bCloseSession = false;
if(session_status() !== PHP_SESSION_ACTIVE) {
$bSecure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https');
session_set_cookie_params(array('httponly' => true, 'secure' => $bSecure, 'samesite' => 'Lax'));
session_start();
$bCloseSession = true;
}
$this->setCsrfToken();
if($bCloseSession) session_write_close();
}
private function checkCsrfToken(string $sClientToken): bool
{
$sServerToken = $this->getCsrfToken();
return PHP_SAPI === 'cli' || ($sServerToken !== '' && is_string($sClientToken) && hash_equals($sServerToken, $sClientToken));
}
private function dispatch(string $sAction): string
{
return match($sAction) {
'markers' => $this->oSpot->getMarkers(),
'last_update' => $this->oSpot->getLastUpdate(),
'geojson' => $this->oSpot->getProjectGeoJson(),
'next_feed' => $this->oSpot->getNextFeed($this->asReq['id']),
'new_feed' => $this->oSpot->getNewFeed($this->asReq['id']),
'add_post' => $this->oSpot->addPost($this->asReq['name'], $this->asReq['content']),
'subscribe' => $this->oSpot->subscribe($this->asReq['email'], $this->asReq['name']),
'unsubscribe' => $this->oSpot->unsubscribe(),
'unsubscribe_email' => $this->oSpot->unsubscribeFromEmail($this->asReq['id_entity']),
'update_project' => $this->oSpot->updateProject(),
default => $this->dispatchAdmin($sAction)
};
}
private function dispatchAdmin(string $sAction): string
{
if(!$this->oSpot->checkUserClearance(User::CLEARANCE_ADMIN)) {
return Spot::getJsonResult(false, Spot::NOT_FOUND);
}
return match($sAction) {
'upload' => $this->oSpot->upload(),
'add_comment' => $this->oSpot->addComment($this->asReq['id_entity'], $this->asReq['content']),
'add_position' => $this->oSpot->addPosition($this->asReq['latitude'], $this->asReq['longitude'], $this->asReq['timestamp']),
'admin_get' => $this->oSpot->getAdminSettings(),
'admin_set' => $this->oSpot->setAdminSettings($this->asReq['type'], $this->asReq['id_entity'], $this->asReq['field'], $this->asReq['value']),
'admin_create' => $this->oSpot->createAdminSettings($this->asReq['type']),
'admin_delete' => $this->oSpot->deleteAdminSettings($this->asReq['type'], $this->asReq['id_entity']),
'sql' => $this->oSpot->getDbBuildScript(),
'build_geojson' => $this->oSpot->buildGeoJSON($this->asReq['name']),
default => Spot::getJsonResult(false, Spot::NOT_FOUND)
};
}
private static function validateValue(string $sValidation, $oValue=0)
{
return match($sValidation) {
'' => $oValue,
'positiveInt' => filter_var($oValue, FILTER_VALIDATE_INT, array('options' => array('default' => 0, 'min_range' => 0)))
};
}
}

View File

@@ -36,8 +36,8 @@ class Converter extends PhpObject {
}
public static function isGeoJsonValid($sCodeName) {
$sGpxFilePath = Gpx::getFilePath($sCodeName);
$sGeoJsonFilePath = GeoJson::getFilePath($sCodeName);
$sGpxFilePath = Gpx::getBackendFilePath($sCodeName);
$sGeoJsonFilePath = GeoJson::getBackendFilePath($sCodeName);
//No need to generate if gpx is missing
return !file_exists($sGpxFilePath) || file_exists($sGeoJsonFilePath) && filemtime($sGeoJsonFilePath) >= filemtime($sGpxFilePath);

View File

@@ -4,26 +4,29 @@ namespace Franzz\Spot;
use Franzz\Objects\PhpObject;
use \Settings;
class Geo extends PhpObject {
abstract class Geo extends PhpObject {
protected const EXT = '';
const GEO_FOLDER = '../geo/';
const GEO_FOLDER = 'geo';
const OPT_SIMPLE = 'simplification';
protected $asTracks;
protected $sFilePath;
protected array $asTracks;
protected string $sFilePath;
public function __construct($sCodeName) {
public function __construct(string $sCodeName) {
parent::__construct(get_class($this), Settings::DEBUG, PhpObject::MODE_HTML);
$this->sFilePath = self::getFilePath($sCodeName);
$this->sFilePath = self::getBackEndFilePath($sCodeName);
$this->asTracks = array();
}
public static function getFilePath($sCodeName) {
return self::GEO_FOLDER.$sCodeName.static::EXT;
//Access from backend
public static function getBackendFilePath(string $sCodeName) {
return '../resources/'.self::GEO_FOLDER.'/'.$sCodeName.static::EXT;
}
public static function getDistFilePath($sCodeName) {
return 'geo/'.$sCodeName.static::EXT;
//Access from frontend (/public/geo is a symlink of /resources/geo)
public static function getFrontendFilePath(string $sCodeName) {
return self::GEO_FOLDER.'/'.$sCodeName.static::EXT;
}
public function getLog() {

View File

@@ -11,9 +11,9 @@ class Media extends PhpObject {
//DB Tables
const MEDIA_TABLE = 'medias';
//Media folders
const MEDIA_FOLDER = 'files/';
const THUMB_FOLDER = self::MEDIA_FOLDER.'thumbs/';
//Media folders (works because /public/files is a symlink of /files)
const MEDIA_FOLDER = 'files';
const THUMB_FOLDER = self::MEDIA_FOLDER.'/thumbs';
const THUMB_MAX_WIDTH = 400;
@@ -288,8 +288,8 @@ class Media extends PhpObject {
}
private static function getMediaPath($sMediaName, $sFileType='media') {
if($sFileType=='thumbnail') return self::THUMB_FOLDER.$sMediaName.(strtolower(substr($sMediaName, -3))=='mov'?'.png':'');
else return self::MEDIA_FOLDER.$sMediaName;
if($sFileType=='thumbnail') return self::THUMB_FOLDER.'/'.$sMediaName.(strtolower(substr($sMediaName, -3))=='mov'?'.png':'');
else return self::MEDIA_FOLDER.'/'.$sMediaName;
}
private static function getMediaType($sMediaName) {

View File

@@ -144,7 +144,7 @@ class Project extends PhpObject {
case 2: $asProject['mode'] = self::MODE_HISTO; break;
}
$asProject['editable'] = $this->isModeEditable($asProject['mode']);
$asProject['gpxfilepath'] = Spot::addTimestampToFilePath(Gpx::getDistFilePath($sCodeName));
$asProject['gpxfilepath'] = Spot::addTimestampToFilePath(Gpx::getFrontendFilePath($sCodeName));
$asProject['codename'] = $sCodeName;
$asProject['default'] = ($sCodeName == $sDefaultProjectCodeName);
}
@@ -157,7 +157,7 @@ class Project extends PhpObject {
$this->oDb->updateRow(self::PROJ_TABLE, $this->iProjectId, ['latitude' => $aiCenter[1], 'longitude' => $aiCenter[0]]);
}
return json_decode(file_get_contents(GeoJson::getDistFilePath($this->sCodeName)), true);
return json_decode(file_get_contents(GeoJson::getBackendFilePath($this->sCodeName)), true);
}
public function getProject() {

View File

@@ -45,21 +45,6 @@ class Spot extends Main
const MAIN_PAGE = 'index';
const DIST_FOLDER = '../dist/';
const MUTATING_ACTIONS = array(
'add_post',
'subscribe',
'unsubscribe',
'update_project',
'upload',
'add_comment',
'add_position',
'admin_set',
'admin_create',
'admin_delete',
'build_geojson'
);
private Project $oProject;
private Media $oMedia;
private User $oUser;
@@ -179,14 +164,7 @@ class Spot extends Main
);
}
public function getAppMainPage() {
//Cache Page List
$asPages = array_diff($this->asMasks, array('email.update', 'email.confirmation'));
if(!$this->oUser->checkUserClearance(User::CLEARANCE_ADMIN)) {
$asPages = array_diff($asPages, array('admin', 'upload'));
}
public function getAppMainPage(string $sCsrfToken='') {
return parent::getMainPage(
array(
'projects' => $this->oProject->getProjects(),
@@ -200,7 +178,7 @@ class Spot extends Main
'hash_sep' => '-',
'title' => self::PROJECT_NAME,
'default_page' => 'project',
'csrf_token' => $this->getCsrfToken()
'csrf_token' => $sCsrfToken
)
),
self::MAIN_PAGE,
@@ -212,15 +190,14 @@ class Spot extends Main
'instances' => [
'entrypoint' => $this->getAppEntryPoints()
]
),
$asPages
)
);
}
private function getAppEntryPoints() {
return array_map(
function($sFileName) {return ['filename' => self::addTimestampToFilePath($sFileName)];},
json_decode(file_get_contents(self::DIST_FOLDER.'entrypoints.json'), true)['entrypoints']['app']
json_decode(file_get_contents('assets/entrypoints.json'), true)['entrypoints']['app']
);
}

View File

@@ -6,19 +6,10 @@ use Franzz\Objects\Translator;
class Uploader extends UploadHandler
{
/**
* Medias Management
* @var Media
*/
private $oMedia;
private Media $oMedia;
private Translator $oLang;
/**
* Languages
* @var Translator
*/
private $oLang;
public $sBody;
public string $sBody;
function __construct(Media &$oMedia, Translator &$oLang)
{
@@ -27,7 +18,7 @@ class Uploader extends UploadHandler
$this->sBody = '';
parent::__construct(array(
'upload_dir' => Media::MEDIA_FOLDER,
'upload_dir' => Media::MEDIA_FOLDER.'/',
'image_versions' => array(),
'accept_file_types' => '/\.(gif|jpe?g|png|mov|mp4)$/i'
));

View File

@@ -1,118 +0,0 @@
<?php
/* Requests Handler */
//Start buffering
ob_start();
//Run from /dist/
$oLoader = require __DIR__.'/../vendor/autoload.php';
use Franzz\Objects\ToolBox;
use Franzz\Spot\Spot;
use Franzz\Spot\User;
ToolBox::fixGlobalVars($argv ?? array());
//Available variables
$sAction = $_REQUEST['a'] ?? '';
$sTimezone = $_REQUEST['t'] ?? '';
$sName = $_REQUEST['name'] ?? '';
$sContent = $_REQUEST['content'] ?? '';
$iProjectId = Spot::validatePositiveInt($_REQUEST['id_project'] ?? 0);
$sRefId = $_REQUEST['id'] ?? 0;
$iEntityId = Spot::validatePositiveInt($_REQUEST['id'] ?? 0);
$sField = $_REQUEST['field'] ?? '';
$oValue = $_REQUEST['value'] ?? '';
$sType = $_REQUEST['type'] ?? '';
$sEmail = $_REQUEST['email'] ?? '';
$sLat = $_REQUEST['latitude'] ?? '';
$sLng = $_REQUEST['longitude'] ?? '';
$iTimestamp = Spot::validatePositiveInt($_REQUEST['timestamp'] ?? 0);
$sCsrfToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ($_POST['csrf_token'] ?? '');
//Initiate class
$oSpot = new Spot(__FILE__, $sTimezone);
$oSpot->setProjectId($iProjectId);
$bValidRequest = $oSpot->validateMutationRequest($sAction, $sCsrfToken);
if(!$bValidRequest) $sResult = Spot::getJsonResult(false, Spot::UNAUTHORIZED);
elseif($sAction == '') $sResult = $oSpot->getAppMainPage();
else
{
switch($sAction)
{
case 'markers':
$sResult = $oSpot->getMarkers();
break;
case 'last_update':
$sResult = $oSpot->getLastUpdate();
break;
case 'geojson':
$sResult = $oSpot->getProjectGeoJson();
break;
case 'next_feed':
$sResult = $oSpot->getNextFeed($sRefId);
break;
case 'new_feed':
$sResult = $oSpot->getNewFeed($sRefId);
break;
case 'add_post':
$sResult = $oSpot->addPost($sName, $sContent);
break;
case 'subscribe':
$sResult = $oSpot->subscribe($sEmail, $sName);
break;
case 'unsubscribe':
$sResult = $oSpot->unsubscribe();
break;
case 'unsubscribe_email':
$sResult = $oSpot->unsubscribeFromEmail($iEntityId);
break;
case 'update_project':
$sResult = $oSpot->updateProject();
break;
default:
if($oSpot->checkUserClearance(User::CLEARANCE_ADMIN))
{
switch($sAction)
{
case 'upload':
$sResult = $oSpot->upload();
break;
case 'add_comment':
$sResult = $oSpot->addComment($iEntityId, $sContent);
break;
case 'add_position':
$sResult = $oSpot->addPosition($sLat, $sLng, $iTimestamp);
break;
case 'admin_get':
$sResult = $oSpot->getAdminSettings();
break;
case 'admin_set':
$sResult = $oSpot->setAdminSettings($sType, $iEntityId, $sField, $oValue);
break;
case 'admin_create':
$sResult = $oSpot->createAdminSettings($sType);
break;
case 'admin_delete':
$sResult = $oSpot->deleteAdminSettings($sType, $iEntityId);
break;
case 'sql':
$sResult = $oSpot->getDbBuildScript();
break;
case 'build_geojson':
$sResult = $oSpot->buildGeoJSON($sName);
break;
default:
$sResult = Spot::getJsonResult(false, Spot::NOT_FOUND);
}
}
else $sResult = Spot::getJsonResult(false, Spot::NOT_FOUND);
}
}
$sDebug = ob_get_clean();
if(Settings::DEBUG && $sDebug!='') $oSpot->addUncaughtError($sDebug);
echo $sResult;

View File

@@ -1,33 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
<title>[#]lang:email.confirmation.subject[#]</title>
</head>
<body>
<span style="color: transparent; display: none !important; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">[#]lang:email.confirmation.preheader[#]</span>
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="width:100%;max-width:600px;">
<tr>
<td width="20%"><img src="[#]local_server[#]images/icons/mstile-144x144.png" width="90%" border="0" alt="logo" /></td>
<td><h1>[#]lang:email.confirmation.thanks_subject[#]</h1></td>
</tr>
<tr>
<td colspan="2">
<p align="justify">[#]lang:email.confirmation.body_1[#]</p>
<p align="justify">[#]lang:email.confirmation.body_2[#]</p>
<p align="justify">[#]lang:email.confirmation.body_3[#]</p>
</td>
</tr>
<tr>
<td colspan="2">
<p>[#]lang:email.confirmation.conclusion[#]<br />[#]lang:email.confirmation.signature[#]</p>
</td>
</tr>
<tr>
<td colspan="2">
<p>[#]lang:email.unsubscribe[#] <a href="[#]unsubscribe_link[#]" target="_blank" rel="noopener">[#]lang:email.unsubscribe_button[#]</a></p>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -1,47 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
<title>[#]lang:email.update.subject[#]</title>
</head>
<body>
<span style="color: transparent; display: none !important; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">[#]lang:email.update.preheader[#]</span>
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="width:100%;max-width:600px;">
<tr>
<td width="20%"><img src="[#]local_server[#]images/icons/mstile-144x144.png" width="90%" border="0" alt="logo" /></td>
<td><h1>[#]lang:email.update.title[#] [#]type[#] #[#]displayed_id[#]</h1></td>
</tr>
<tr>
<td colspan="2">
<div style="background-color:#6dff58;color:#326526;border-radius:3px;padding:1rem;margin-top:1rem;display:inline-block;box-shadow: 2px 2px 3px 0px rgba(0,0,0,.5);">
<a href="[#]local_server[#]" target="_blank" rel="noopener"><img style="border-radius:3px;" src="[#]marker_img_url[#]" alt="position" /></a>
<br />[#]lat_dms[#] [#]lon_dms[#]
<br />[#]date_time[#] ([#]timezone[#])
</div>
</td>
</tr>
<tr>
<td colspan="2">
<h2>[#]lang:email.update.latest_news[#]</h2>
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<!-- [PART] news [START] -->
<tr>
<td>
<a href="[#]local_server[#]#project-[#]project[#]-[#]type[#]-[#]id[#]" target="_blank" rel="noopener" style="text-decoration:none;background-color:#EEE;color:#333;margin-bottom:1rem;border-radius:3px;padding:5%;display:inline-block;width:90%;box-shadow: 2px 2px 3px 0px rgba(0,0,0,.5);">
<!-- [PART] media [START] --><img src="[#]local_server[#][#]thumb_path[#]" style="max-height:200px;image-orientation:from-image;" /><br /><span>[#]comment[#]</span><!-- [PART] media [END] -->
<!-- [PART] post [START] --><span>[#]content[#]</span><br /><span style="margin-top:0.5em;float:right;">--[#]formatted_name[#]</span><!-- [PART] post [END] -->
</a>
</td>
</tr>
<!-- [PART] news [END] -->
</table>
</td>
</tr>
<tr>
<td colspan="2">
<p>[#]lang:email.unsubscribe[#] <a href="[#]unsubscribe_link[#]" target="_blank" rel="noopener">[#]lang:email.unsubscribe_button[#]</a></p>
</td>
</tr>
</table>
</body>
</html>