Implement CSRF
All checks were successful
Deploy Spot / deploy (push) Successful in 34s

This commit is contained in:
2026-05-28 13:22:44 +02:00
parent 8092846d6f
commit fdd0ada815
14 changed files with 129 additions and 106 deletions

4
composer.lock generated
View File

@@ -12,7 +12,7 @@
"source": { "source": {
"type": "git", "type": "git",
"url": "https://git.lutran.fr/franzz/objects", "url": "https://git.lutran.fr/franzz/objects",
"reference": "e28c650f2254801caa6f9756ad30ce1244c4c0c2" "reference": "d13fdacddec581b5cf5179b625f414b2453b6bf3"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@@ -21,7 +21,7 @@
} }
}, },
"description": "Objects", "description": "Objects",
"time": "2026-05-22T22:11:13+00:00" "time": "2026-05-28T10:12:19+00:00"
}, },
{ {
"name": "phpmailer/phpmailer", "name": "phpmailer/phpmailer",

View File

@@ -167,7 +167,7 @@ class Media extends PhpObject {
'-print_format json', //output format: json '-print_format json', //output format: json
'-i' //input file '-i' //input file
)); ));
exec('ffprobe '.$sParams.' "'.$sMediaPath.'"', $asResult); exec('ffprobe '.$sParams.' '.escapeshellarg($sMediaPath), $asResult);
$asExif = json_decode(implode('', $asResult), true); $asExif = json_decode(implode('', $asResult), true);
//Taken On //Taken On
@@ -269,10 +269,10 @@ class Media extends PhpObject {
$sTempPath = self::getMediaPath(uniqid('temp_').'.png'); $sTempPath = self::getMediaPath(uniqid('temp_').'.png');
$asResult = array(); $asResult = array();
$sParams = implode(' ', array( $sParams = implode(' ', array(
'-i "'.$sMediaPath.'"', //input file '-i '.escapeshellarg($sMediaPath), //input file
'-ss 00:00:01.000', //Image taken after x seconds '-ss 00:00:01.000', //Image taken after x seconds
'-vframes 1', //number of video frames to output '-vframes 1', //number of video frames to output
'"'.$sTempPath.'"', //output file escapeshellarg($sTempPath), //output file
)); ));
exec('ffmpeg '.$sParams, $asResult); exec('ffmpeg '.$sParams, $asResult);
@@ -296,7 +296,8 @@ class Media extends PhpObject {
$sMediaPath = self::getMediaPath($sMediaName); $sMediaPath = self::getMediaPath($sMediaName);
$sMediaMime = mime_content_type($sMediaPath); $sMediaMime = mime_content_type($sMediaPath);
switch($sMediaMime) { switch($sMediaMime) {
case 'video/quicktime': $sType = 'video'; break; case 'video/quicktime':
case 'video/mp4': $sType = 'video'; break;
default: $sType = 'image'; break; default: $sType = 'image'; break;
} }

View File

@@ -46,6 +46,19 @@ class Spot extends Main
const MAIN_PAGE = 'index'; const MAIN_PAGE = 'index';
const DIST_FOLDER = '../dist/'; 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 Project $oProject;
private Media $oMedia; private Media $oMedia;
@@ -186,7 +199,8 @@ class Spot extends Main
'chunk_size' => self::FEED_CHUNK_SIZE, 'chunk_size' => self::FEED_CHUNK_SIZE,
'hash_sep' => '-', 'hash_sep' => '-',
'title' => self::PROJECT_NAME, 'title' => self::PROJECT_NAME,
'default_page' => 'project' 'default_page' => 'project',
'csrf_token' => $this->getCsrfToken()
) )
), ),
self::MAIN_PAGE, self::MAIN_PAGE,
@@ -278,17 +292,6 @@ class Spot extends Main
return $oEmail->send(); return $oEmail->send();
} }
public function genCronFile() {
//$bSuccess = (file_put_contents('spot_cron.sh', '#!/bin/bash'."\n".'cd '.dirname($_SERVER['SCRIPT_FILENAME'])."\n".'php -f index.php a=update_feed')!==false);
$sFileName = 'spot_cron.sh';
$sContent =
'#!/bin/bash'."\n".
'wget -qO- '.$this->asContext['serv_name'].'index.php?a=update_project > /dev/null'."\n".
'#Crontab job: 0 * * * * . '.dirname($_SERVER['SCRIPT_FILENAME']).'/'.$sFileName.' > /dev/null'."\n";
$bSuccess = (file_put_contents($sFileName, $sContent)!==false);
return self::getJsonResult($bSuccess, '');
}
public function getMarkers($asMessageIds=array(), $asMediaIds=array(), $bInternal=false) public function getMarkers($asMessageIds=array(), $asMediaIds=array(), $bInternal=false)
{ {
//Get messages //Get messages
@@ -579,10 +582,10 @@ class Spot extends Main
return $bInternal?$asResult['feed']:self::getJsonResult(true, '', $asResult); return $bInternal?$asResult['feed']:self::getJsonResult(true, '', $asResult);
} }
public function getFeed($iRefId=0, $sDirection, $sSort) { private function getFeed($iRefId, $sDirection, $sSort) {
$this->oDb->cleanSql($iRefId); $sRefId = is_scalar($iRefId) && preg_match('/^\d+(?:\.\d+)?$/D', (string) $iRefId) ? (string) $iRefId : '0';
$this->oDb->cleanSql($sDirection); $sDirection = ($sDirection === '>')?'>':'<';
$this->oDb->cleanSql($sSort); $sSort = ($sSort === 'ASC')?'ASC':'DESC';
$sProjectIdField = Db::getId(Project::PROJ_TABLE); $sProjectIdField = Db::getId(Project::PROJ_TABLE);
$sMsgIdField = Db::getId(Feed::MSG_TABLE); $sMsgIdField = Db::getId(Feed::MSG_TABLE);
@@ -605,7 +608,7 @@ class Spot extends Main
"FROM ".self::POST_TABLE, "FROM ".self::POST_TABLE,
$this->getFeedConstraints(self::POST_TABLE, 'site_time', 'sql'), $this->getFeedConstraints(self::POST_TABLE, 'site_time', 'sql'),
") AS items", ") AS items",
($iRefId > 0)?("WHERE ref ".$sDirection." ".$iRefId):"", ($sRefId !== '0')?("WHERE ref ".$sDirection." ".$sRefId):"",
"ORDER BY ref ".$sSort, "ORDER BY ref ".$sSort,
"LIMIT ".self::FEED_CHUNK_SIZE "LIMIT ".self::FEED_CHUNK_SIZE
)); ));

View File

@@ -46,12 +46,15 @@ class Uploader extends UploadHandler
} }
protected function handle_file_upload($uploaded_file, $name, $size, $type, $error, $index = null, $content_range = null) { protected function handle_file_upload($uploaded_file, $name, $size, $type, $error, $index = null, $content_range = null) {
$file = parent::handle_file_upload($uploaded_file, $name, $size, $type, $error, $index, $content_range); $sExt = strtolower(pathinfo((string) $name, PATHINFO_EXTENSION));
$sStoredName = bin2hex(random_bytes(16)).($sExt !== ''?'.'.$sExt:'');
$file = parent::handle_file_upload($uploaded_file, $sStoredName, $size, $type, $error, $index, $content_range);
if(empty($file->error)) { if(empty($file->error)) {
$asResult = $this->oMedia->addMedia($file->name); $asResult = $this->oMedia->addMedia($file->name);
if(!$asResult['result']) $file->error = $this->get_error_message($asResult['desc'], $asResult['data']); if(!$asResult['result']) $file->error = $this->get_error_message($asResult['desc'], $asResult['data']);
else { else {
$file->original_name = basename((string) $name);
$file->id = $this->oMedia->getMediaId(); $file->id = $this->oMedia->getMediaId();
$file->thumbnail = $asResult['data']['thumb_path']; $file->thumbnail = $asResult['data']['thumb_path'];
} }

View File

@@ -9,33 +9,36 @@ ob_start();
$oLoader = require __DIR__.'/../vendor/autoload.php'; $oLoader = require __DIR__.'/../vendor/autoload.php';
use Franzz\Objects\ToolBox; use Franzz\Objects\ToolBox;
use Franzz\Objects\Main;
use Franzz\Spot\Spot; use Franzz\Spot\Spot;
use Franzz\Spot\User; use Franzz\Spot\User;
ToolBox::fixGlobalVars($argv ?? array()); ToolBox::fixGlobalVars($argv ?? array());
//Available variables //Available variables
$sAction = $_REQUEST['a'] ?? ''; $sAction = $_REQUEST['a'] ?? '';
$sTimezone = $_REQUEST['t'] ?? ''; $sTimezone = $_REQUEST['t'] ?? '';
$sName = $_GET['name'] ?? ''; $sName = $_REQUEST['name'] ?? '';
$sContent = $_GET['content'] ?? ''; $sContent = $_REQUEST['content'] ?? '';
$iProjectId = $_REQUEST['id_project'] ?? 0 ; $iProjectId = Spot::validatePositiveInt($_REQUEST['id_project'] ?? 0);
$sField = $_REQUEST['field'] ?? ''; $sRefId = $_REQUEST['id'] ?? 0;
$oValue = $_REQUEST['value'] ?? ''; $iEntityId = Spot::validatePositiveInt($_REQUEST['id'] ?? 0);
$iId = $_REQUEST['id'] ?? 0 ; $sField = $_REQUEST['field'] ?? '';
$sType = $_REQUEST['type'] ?? ''; $oValue = $_REQUEST['value'] ?? '';
$sEmail = $_REQUEST['email'] ?? ''; $sType = $_REQUEST['type'] ?? '';
$sLat = $_REQUEST['latitude'] ?? ''; $sEmail = $_REQUEST['email'] ?? '';
$sLng = $_REQUEST['longitude'] ?? ''; $sLat = $_REQUEST['latitude'] ?? '';
$iTimestamp = $_REQUEST['timestamp'] ?? 0; $sLng = $_REQUEST['longitude'] ?? '';
$iTimestamp = Spot::validatePositiveInt($_REQUEST['timestamp'] ?? 0);
$sCsrfToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ($_POST['csrf_token'] ?? '');
//Initiate class //Initiate class
$oSpot = new Spot(__FILE__, $sTimezone); $oSpot = new Spot(__FILE__, $sTimezone);
$oSpot->setProjectId($iProjectId); $oSpot->setProjectId($iProjectId);
$sResult = ''; $bValidRequest = $oSpot->validateMutationRequest($sAction, $sCsrfToken);
if($sAction!='') if(!$bValidRequest) $sResult = Spot::getJsonResult(false, Spot::UNAUTHORIZED);
elseif($sAction == '') $sResult = $oSpot->getAppMainPage();
else
{ {
switch($sAction) switch($sAction)
{ {
@@ -49,10 +52,10 @@ if($sAction!='')
$sResult = $oSpot->getProjectGeoJson(); $sResult = $oSpot->getProjectGeoJson();
break; break;
case 'next_feed': case 'next_feed':
$sResult = $oSpot->getNextFeed($iId); $sResult = $oSpot->getNextFeed($sRefId);
break; break;
case 'new_feed': case 'new_feed':
$sResult = $oSpot->getNewFeed($iId); $sResult = $oSpot->getNewFeed($sRefId);
break; break;
case 'add_post': case 'add_post':
$sResult = $oSpot->addPost($sName, $sContent); $sResult = $oSpot->addPost($sName, $sContent);
@@ -64,7 +67,7 @@ if($sAction!='')
$sResult = $oSpot->unsubscribe(); $sResult = $oSpot->unsubscribe();
break; break;
case 'unsubscribe_email': case 'unsubscribe_email':
$sResult = $oSpot->unsubscribeFromEmail($iId); $sResult = $oSpot->unsubscribeFromEmail($iEntityId);
break; break;
case 'update_project': case 'update_project':
$sResult = $oSpot->updateProject(); $sResult = $oSpot->updateProject();
@@ -78,7 +81,7 @@ if($sAction!='')
$sResult = $oSpot->upload(); $sResult = $oSpot->upload();
break; break;
case 'add_comment': case 'add_comment':
$sResult = $oSpot->addComment($iId, $sContent); $sResult = $oSpot->addComment($iEntityId, $sContent);
break; break;
case 'add_position': case 'add_position':
$sResult = $oSpot->addPosition($sLat, $sLng, $iTimestamp); $sResult = $oSpot->addPosition($sLat, $sLng, $iTimestamp);
@@ -87,16 +90,13 @@ if($sAction!='')
$sResult = $oSpot->getAdminSettings(); $sResult = $oSpot->getAdminSettings();
break; break;
case 'admin_set': case 'admin_set':
$sResult = $oSpot->setAdminSettings($sType, $iId, $sField, $oValue); $sResult = $oSpot->setAdminSettings($sType, $iEntityId, $sField, $oValue);
break; break;
case 'admin_create': case 'admin_create':
$sResult = $oSpot->createAdminSettings($sType); $sResult = $oSpot->createAdminSettings($sType);
break; break;
case 'admin_delete': case 'admin_delete':
$sResult = $oSpot->deleteAdminSettings($sType, $iId); $sResult = $oSpot->deleteAdminSettings($sType, $iEntityId);
break;
case 'generate_cron':
$sResult = $oSpot->genCronFile();
break; break;
case 'sql': case 'sql':
$sResult = $oSpot->getDbBuildScript(); $sResult = $oSpot->getDbBuildScript();
@@ -105,13 +105,12 @@ if($sAction!='')
$sResult = $oSpot->buildGeoJSON($sName); $sResult = $oSpot->buildGeoJSON($sName);
break; break;
default: default:
$sResult = Main::getJsonResult(false, Main::NOT_FOUND); $sResult = Spot::getJsonResult(false, Spot::NOT_FOUND);
} }
} }
else $sResult = Main::getJsonResult(false, Main::NOT_FOUND); else $sResult = Spot::getJsonResult(false, Spot::NOT_FOUND);
} }
} }
else $sResult = $oSpot->getAppMainPage();
$sDebug = ob_get_clean(); $sDebug = ob_get_clean();
if(Settings::DEBUG && $sDebug!='') $oSpot->addUncaughtError($sDebug); if(Settings::DEBUG && $sDebug!='') $oSpot->addUncaughtError($sDebug);

View File

@@ -22,6 +22,7 @@ const oApi = new Api({
server: appConfig.consts.server, server: appConfig.consts.server,
processPage: appConfig.consts.process_page, processPage: appConfig.consts.process_page,
timezone: oUser.timezone, timezone: oUser.timezone,
csrfToken: appConfig.consts.csrf_token,
errorCode: appConfig.consts.error, errorCode: appConfig.consts.error,
lang: oLang lang: oLang
}); });

View File

@@ -50,7 +50,7 @@ export default {
} }
}, },
createElem(sType) { createElem(sType) {
this.api.get('admin_create', {type: sType}) this.api.post('admin_create', {type: sType})
.then((aoNewElemTypes) => { .then((aoNewElemTypes) => {
for(const [sType, aoNewElems] of Object.entries(aoNewElemTypes)) { for(const [sType, aoNewElems] of Object.entries(aoNewElemTypes)) {
for(const [iKey, oNewElem] of Object.entries(aoNewElems)) { for(const [iKey, oNewElem] of Object.entries(aoNewElems)) {
@@ -68,7 +68,7 @@ export default {
id: oElem.id id: oElem.id
}; };
this.api.get('admin_delete', asInputs) this.api.post('admin_delete', asInputs)
.then((asData) => { .then((asData) => {
delete this.elems[asInputs.type][asInputs.id]; delete this.elems[asInputs.type][asInputs.id];
this.addFeedback('success', this.l('admin.delete_success'), asInputs); this.addFeedback('success', this.l('admin.delete_success'), asInputs);
@@ -90,7 +90,7 @@ export default {
value: sNewVal value: sNewVal
}; };
this.api.get('admin_set', asInputs) this.api.post('admin_set', asInputs)
.then((asData) => { .then((asData) => {
this.elems[oElem.type][oElem.id][oEvent.target.name] = sNewVal; this.elems[oElem.type][oElem.id][oEvent.target.name] = sNewVal;
this.addFeedback('success', this.l('admin.save_success'), asInputs); this.addFeedback('success', this.l('admin.save_success'), asInputs);
@@ -106,7 +106,7 @@ export default {
this.saveTimer = setTimeout(() => {this.updateElem(oElem, oEvent);}, 2000); this.saveTimer = setTimeout(() => {this.updateElem(oElem, oEvent);}, 2000);
}, },
updateProject() { updateProject() {
this.api.get('update_project') this.api.post('update_project')
.then((asData, sMsg) => {this.addFeedback('success', sMsg, {'update':'project'});}) .then((asData, sMsg) => {this.addFeedback('success', sMsg, {'update':'project'});})
.catch((sMsg) => {this.addFeedback('error', sMsg, {'update':'project'});}); .catch((sMsg) => {this.addFeedback('error', sMsg, {'update':'project'});});
} }

View File

@@ -514,8 +514,8 @@ export default {
return { return {
top: this.mapPadding, top: this.mapPadding,
bottom: this.mapPadding, bottom: this.mapPadding,
left: this.mapPadding + ((!bIsMobile && this.panels.settingsOpen && this.settings)?this.settings.getWidth():0), left: this.mapPadding + ((!bIsMobile && this.panels.leftOpen && this.settings)?this.settings.getWidth():0),
right: this.mapPadding + ((!bIsMobile && this.panels.feedOpen && this.feed)?this.feed.getWidth():0) right: this.mapPadding + ((!bIsMobile && this.panels.rightOpen && this.feed)?this.feed.getWidth():0)
}; };
}, },
updateMapPadding(iDuration=0) { updateMapPadding(iDuration=0) {

View File

@@ -39,7 +39,7 @@ export default {
const sAction = this.action; const sAction = this.action;
this.loading = true; this.loading = true;
this.api.request(sAction, {'email': this.user.email, 'name': this.user.name}) this.api.request(sAction, {'email': this.user.email, 'name': this.user.name}, 'POST')
.then((asResponse) => { .then((asResponse) => {
this.feedbacks.push({type: asResponse.result, msg: asResponse.desc}); this.feedbacks.push({type: asResponse.result, msg: asResponse.desc});
this.user.setInfo(asResponse.data); this.user.setInfo(asResponse.data);

View File

@@ -134,7 +134,7 @@
send() { send() {
if(this.postMessage != '' && this.user.name != '') { if(this.postMessage != '' && this.user.name != '') {
this.sending = true; this.sending = true;
this.api.get( this.api.post(
'add_post', 'add_post',
{ {
id_project: this.project.project.id, id_project: this.project.project.id,

View File

@@ -51,6 +51,7 @@ export default {
endpoint, endpoint,
fieldName: 'files[]', fieldName: 'files[]',
formData: true, formData: true,
headers: {'X-CSRF-Token': this.consts.csrf_token},
allowedMetaFields: ['t', 'name', 'type'], allowedMetaFields: ['t', 'name', 'type'],
getResponseData(xhr) { getResponseData(xhr) {
return JSON.parse(xhr.responseText || '{}'); return JSON.parse(xhr.responseText || '{}');
@@ -65,7 +66,7 @@ export default {
const uploadedFiles = response?.body?.files || []; const uploadedFiles = response?.body?.files || [];
uploadedFiles.forEach((uploadedFile) => { uploadedFiles.forEach((uploadedFile) => {
const hasError = Object.prototype.hasOwnProperty.call(uploadedFile, 'error'); const hasError = Object.prototype.hasOwnProperty.call(uploadedFile, 'error');
this.logs.push(hasError ? uploadedFile.error : this.lang.get('upload.success', [uploadedFile.name])); this.logs.push(hasError ? uploadedFile.error : this.lang.get('upload.success', [uploadedFile.original_name || uploadedFile.name]));
if(!hasError) this.files.push({...uploadedFile, content: ''}); if(!hasError) this.files.push({...uploadedFile, content: ''});
}); });
}); });
@@ -85,7 +86,7 @@ export default {
event.target.value = ''; event.target.value = '';
}, },
addComment(oFile) { addComment(oFile) {
this.api.get('add_comment', { this.api.post('add_comment', {
id: oFile.id, id: oFile.id,
content: oFile.content content: oFile.content
}) })
@@ -98,7 +99,7 @@ export default {
navigator.geolocation.getCurrentPosition( navigator.geolocation.getCurrentPosition(
(position) => { (position) => {
this.logs.push('Sending position...'); this.logs.push('Sending position...');
this.api.get('add_position', { this.api.post('add_position', {
'latitude': position.coords.latitude, 'latitude': position.coords.latitude,
'longitude': position.coords.longitude, 'longitude': position.coords.longitude,
'timestamp': Math.round(position.timestamp / 1000) 'timestamp': Math.round(position.timestamp / 1000)

View File

@@ -1,17 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: Adobe Illustrator 23.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <!-- Generator: Adobe Illustrator 23.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg width="406" height="469" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 406 469" style="enable-background:new 0 0 406 469;"> <svg width="406" height="469" enable-background="new 0 0 406 469" version="1.1" viewBox="0 0 406 469" xmlns="http://www.w3.org/2000/svg">
<style type="text/css">.st0{fill:#F18A00;}</style> <style type="text/css">.st0{fill:#F18A00;}</style>
<g id="Layer_3"/> <path class="st0" d="m85.806 195.8c-1-0.8-1.3-2.3-0.6-3.4 11.1-18.2 56.5-85.8 117.3-85.8 49.6 0 90.4 33.4 110.3 53.3 1.2 1.2 2.9 1.9 4.6 1.9s3.4-0.7 4.6-1.9l16.4-16.3c1-1 1.1-2.5 0.2-3.5-5.1-6.1-15.3-17.2-29.8-28.2-31.7-24.1-67.5-36.3-106.3-36.3-79.4 0-129.8 75.4-142.3 96.5-0.8 1.4-2.6 1.7-3.8 0.7l-55.4-43.6c-1-0.8-1.3-2.2-0.7-3.3 5.9-10.8 23-39.4 48.5-63.7 42.8-40.7 95.1-62.2 151.4-62.2 56 0 109.2 18.9 153.9 54.8 27.3 21.9 45.5 43.2 51.3 50.4 0.8 1 0.7 2.5-0.2 3.5l-12.3 12.2c-1.1 1.1-2.9 1-3.8-0.2-7.3-8.9-26.2-30.5-49.7-49.2-41.1-32.7-88-49.2-139.2-49.2-50.9 0-96.5 18.6-135.4 55.4-20.9 19.7-30.4 34.1-35.6 42.3-0.7 1.1-0.5 2.6 0.6 3.5l16.7 13.2c1.2 0.9 2.6 1.4 4.1 1.4 2.2 0 4.2-1.1 5.4-2.9 7.4-10.7 15.9-20.6 26.8-31.1 34.3-33.1 75.7-50.6 119.9-50.6 92 0 151.2 70.7 165.3 89.4 0.8 1.1 0.7 2.5-0.3 3.4l-49.8 49.3c-1.1 1.1-2.8 1-3.8-0.1-15.9-18.1-63-66.5-111.4-66.5-23.6 0-46.6 11.3-68.4 33.5-7.2 7.3-13.9 15.6-19.9 24.4-0.8 1.1-0.5 2.7 0.6 3.6l93.1 73.2c1.1 0.9 1.3 2.5 0.4 3.7l-10.5 13.3c-0.9 1.1-2.5 1.3-3.7 0.4l-108.5-85.3z" enable-background="new 0 0 406 469"/>
<g id="Layer_8"/> <path class="st0" d="m205.91 468.9c-56 0-109.2-18.9-153.9-54.8-27.3-21.9-45.5-43.2-51.3-50.4-0.8-1-0.7-2.5 0.2-3.5l12.3-12.2c1.1-1.1 2.9-1 3.8 0.2 7.3 8.9 26.2 30.5 49.7 49.2 41.1 32.7 88 49.2 139.2 49.2 50.9 0 96.5-18.6 135.4-55.4 20.9-19.7 30.4-34.1 35.6-42.3 0.7-1.1 0.5-2.6-0.6-3.5l-16.7-13.2c-1.2-0.9-2.6-1.4-4.1-1.4-2.2 0-4.2 1.1-5.4 2.9-7.4 10.7-15.9 20.6-26.8 31.1-34.3 33.1-75.7 50.6-119.8 50.6-92 0-151.2-70.7-165.3-89.4-0.8-1.1-0.7-2.5 0.3-3.4l49.8-49.3c1.1-1.1 2.8-1 3.8 0.1 15.9 18.1 63 66.5 111.4 66.5 23.6 0 46.6-11.3 68.4-33.5 7.2-7.3 13.9-15.6 19.9-24.4 0.8-1.1 0.5-2.7-0.6-3.6l-93.1-73.2c-1.1-0.9-1.3-2.5-0.4-3.7l10.5-13.3c0.9-1.1 2.5-1.3 3.7-0.4l108.3 85.2c1 0.8 1.3 2.3 0.6 3.4-11.1 18.2-56.5 85.8-117.3 85.8-49.6 0-90.4-33.4-110.3-53.3-1.2-1.2-2.9-1.9-4.6-1.9s-3.4 0.7-4.6 1.9l-16.4 16.3c-1 1-1.1 2.5-0.2 3.5 5.1 6.1 15.3 17.2 29.8 28.2 31.7 24.1 67.5 36.3 106.3 36.3 79.4 0 129.8-75.4 142.3-96.5 0.8-1.4 2.6-1.7 3.8-0.7l55.4 43.6c1 0.8 1.3 2.2 0.7 3.3-5.9 10.8-23 39.4-48.5 63.7-42.6 40.8-95 62.3-151.3 62.3z" enable-background="new 0 0 406 469"/>
<g id="Layer_9"/> </svg>
<g id="Layer_7"/>
<g id="Layer_6"/>
<g id="Layer_4"/>
<g>
<g>
<path class="st0" d="m85.80618,195.80013c-1,-0.8 -1.3,-2.3 -0.6,-3.4c11.1,-18.2 56.5,-85.8 117.3,-85.8c49.6,0 90.4,33.4 110.3,53.3c1.2,1.2 2.9,1.9 4.6,1.9c1.7,0 3.4,-0.7 4.6,-1.9l16.4,-16.3c1,-1 1.1,-2.5 0.2,-3.5c-5.1,-6.1 -15.3,-17.2 -29.8,-28.2c-31.7,-24.1 -67.5,-36.3 -106.3,-36.3c-79.4,0 -129.8,75.4 -142.3,96.5c-0.8,1.4 -2.6,1.7 -3.8,0.7l-55.4,-43.6c-1,-0.8 -1.3,-2.2 -0.7,-3.3c5.9,-10.8 23,-39.4 48.5,-63.7c42.8,-40.7 95.1,-62.2 151.4,-62.2c56,0 109.2,18.9 153.9,54.8c27.3,21.9 45.5,43.2 51.3,50.4c0.8,1 0.7,2.5 -0.2,3.5l-12.3,12.2c-1.1,1.1 -2.9,1 -3.8,-0.2c-7.3,-8.9 -26.2,-30.5 -49.7,-49.2c-41.1,-32.7 -88,-49.2 -139.2,-49.2c-50.9,0 -96.5,18.6 -135.4,55.4c-20.9,19.7 -30.4,34.1 -35.6,42.3c-0.7,1.1 -0.5,2.6 0.6,3.5l16.7,13.2c1.2,0.9 2.6,1.4 4.1,1.4c2.2,0 4.2,-1.1 5.4,-2.9c7.4,-10.7 15.9,-20.6 26.8,-31.1c34.3,-33.1 75.7,-50.6 119.9,-50.6c92,0 151.2,70.7 165.3,89.4c0.8,1.1 0.7,2.5 -0.3,3.4l-49.8,49.3c-1.1,1.1 -2.8,1 -3.8,-0.1c-15.9,-18.1 -63,-66.5 -111.4,-66.5c-23.6,0 -46.6,11.3 -68.4,33.5c-7.2,7.3 -13.9,15.6 -19.9,24.4c-0.8,1.1 -0.5,2.7 0.6,3.6l93.1,73.2c1.1,0.9 1.3,2.5 0.4,3.7l-10.5,13.3c-0.9,1.1 -2.5,1.3 -3.7,0.4l-108.5,-85.3z" />
<path class="st0" d="m205.90618,468.90013c-56,0 -109.2,-18.9 -153.9,-54.8c-27.3,-21.9 -45.5,-43.2 -51.3,-50.4c-0.8,-1 -0.7,-2.5 0.2,-3.5l12.3,-12.2c1.1,-1.1 2.9,-1 3.8,0.2c7.3,8.9 26.2,30.5 49.7,49.2c41.1,32.7 88,49.2 139.2,49.2c50.9,0 96.5,-18.6 135.4,-55.4c20.9,-19.7 30.4,-34.1 35.6,-42.3c0.7,-1.1 0.5,-2.6 -0.6,-3.5l-16.7,-13.2c-1.2,-0.9 -2.6,-1.4 -4.1,-1.4c-2.2,0 -4.2,1.1 -5.4,2.9c-7.4,10.7 -15.9,20.6 -26.8,31.1c-34.3,33.1 -75.7,50.6 -119.8,50.6c-92,0 -151.2,-70.7 -165.3,-89.4c-0.8,-1.1 -0.7,-2.5 0.3,-3.4l49.8,-49.3c1.1,-1.1 2.8,-1 3.8,0.1c15.9,18.1 63,66.5 111.4,66.5c23.6,0 46.6,-11.3 68.4,-33.5c7.2,-7.3 13.9,-15.6 19.9,-24.4c0.8,-1.1 0.5,-2.7 -0.6,-3.6l-93.1,-73.2c-1.1,-0.9 -1.3,-2.5 -0.4,-3.7l10.5,-13.3c0.9,-1.1 2.5,-1.3 3.7,-0.4l108.3,85.2c1,0.8 1.3,2.3 0.6,3.4c-11.1,18.2 -56.5,85.8 -117.3,85.8c-49.6,0 -90.4,-33.4 -110.3,-53.3c-1.2,-1.2 -2.9,-1.9 -4.6,-1.9s-3.4,0.7 -4.6,1.9l-16.4,16.3c-1,1 -1.1,2.5 -0.2,3.5c5.1,6.1 15.3,17.2 29.8,28.2c31.7,24.1 67.5,36.3 106.3,36.3c79.4,0 129.8,-75.4 142.3,-96.5c0.8,-1.4 2.6,-1.7 3.8,-0.7l55.4,43.6c1,0.8 1.3,2.2 0.7,3.3c-5.9,10.8 -23,39.4 -48.5,63.7c-42.6,40.8 -95,62.3 -151.3,62.3z" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -1,44 +1,60 @@
export default class Api { export default class Api {
constructor({server, processPage, timezone, errorCode, lang}) { constructor({server, processPage, timezone, csrfToken, errorCode, lang}) {
this.server = server; this.server = server;
this.processPage = processPage; this.processPage = processPage;
this.timezone = timezone; this.timezone = timezone;
this.csrfToken = csrfToken;
this.errorCode = errorCode; this.errorCode = errorCode;
this.lang = lang; this.lang = lang;
} }
async get(action, params = {}) { async get(sAction, asParams = {}) {
const response = await this.request(action, params); const response = await this.request(sAction, asParams, 'GET');
return response.data; return response.data;
} }
async request(action, params = {}) { async post(sAction, asParams = {}) {
const requestParams = { const response = await this.request(sAction, asParams, 'POST');
...params, return response.data;
a: action, }
async request(sAction, asParams = {}, method = 'GET') {
const oUrl = new URL(this.processPage, this.server);
const sUrlParams = new URLSearchParams({
...asParams,
a: sAction,
t: this.timezone t: this.timezone
}).toString();
const asOptions = {
method,
headers: {'Accept': 'application/json'}
}; };
const url = new URL(this.processPage, this.server); if(method === 'GET') {
url.search = new URLSearchParams(requestParams).toString(); oUrl.search = sUrlParams;
}
const request = await fetch(url, { else {
method: 'GET', asOptions.headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
headers: {'Content-Type': 'application/json'} asOptions.headers['X-CSRF-Token'] = this.csrfToken;
}); asOptions.body = sUrlParams;
if(!request.ok) {
throw new Error('Error HTTP ' + request.status + ': ' + request.statusText);
} }
const response = await request.json(); const oRequest = await fetch(oUrl, asOptions);
response.desc = this.lang.parse(response.desc);
if(response.result == this.errorCode) { if(!oRequest.ok) {
throw response.desc; throw new Error('Error HTTP ' + oRequest.status + ': ' + oRequest.statusText);
} }
return response; const oResponse = await oRequest.json();
oResponse.desc = this.lang.parse(oResponse.desc);
if(oResponse.result == this.errorCode) {
throw oResponse.desc;
}
return oResponse;
} }
} }

View File

@@ -1,4 +1,5 @@
import { LngLat } from 'maplibre-gl'; const EARTH_RADIUS = 6371008.8;
const DEGREES_TO_RADIANS = Math.PI / 180;
export default class Projects { export default class Projects {
@@ -63,12 +64,10 @@ export default class Projects {
let iDistance = 0, iElevDrop = 0, iElevGain = 0, iTime = 0; let iDistance = 0, iElevDrop = 0, iElevGain = 0, iTime = 0;
for(let i = 1; i < aoCoords.length; i++) { for(let i = 1; i < aoCoords.length; i++) {
const oCurrPoint = new LngLat(aoCoords[i][0], aoCoords[i][1]);
const oPrevPoint = new LngLat(aoCoords[i - 1][0], aoCoords[i - 1][1]);
const iCurrElev = Number(aoCoords[i][2]); const iCurrElev = Number(aoCoords[i][2]);
const iPrevElev = Number(aoCoords[i - 1][2]); const iPrevElev = Number(aoCoords[i - 1][2]);
const iElevDelta = (Number.isFinite(iCurrElev) && Number.isFinite(iPrevElev))?(iCurrElev - iPrevElev):0; const iElevDelta = (Number.isFinite(iCurrElev) && Number.isFinite(iPrevElev))?(iCurrElev - iPrevElev):0;
const iSegDistance = oCurrPoint.distanceTo(oPrevPoint); const iSegDistance = this.getDistance(aoCoords[i], aoCoords[i - 1]);
if(iSegDistance <= 0) continue; if(iSegDistance <= 0) continue;
iDistance += iSegDistance; iDistance += iSegDistance;
@@ -94,4 +93,14 @@ export default class Projects {
time: iTime time: iTime
}; };
} }
getDistance(aoPointA, aoPointB) {
const iLatA = aoPointA[1] * DEGREES_TO_RADIANS;
const iLatB = aoPointB[1] * DEGREES_TO_RADIANS;
const iAngle =
Math.sin(iLatA) * Math.sin(iLatB)
+ Math.cos(iLatA) * Math.cos(iLatB) * Math.cos((aoPointB[0] - aoPointA[0]) * DEGREES_TO_RADIANS);
return EARTH_RADIUS * Math.acos(Math.min(iAngle, 1));
}
} }