Restructure project folders and remove obsolete files
All checks were successful
Deploy Spot / deploy (push) Successful in 34s
All checks were successful
Deploy Spot / deploy (push) Successful in 34s
This commit is contained in:
168
lib/Controller.php
Normal file
168
lib/Controller.php
Normal 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)))
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
23
lib/Geo.php
23
lib/Geo.php
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
31
lib/Spot.php
31
lib/Spot.php
@@ -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']
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
));
|
||||
|
||||
118
lib/index.php
118
lib/index.php
@@ -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;
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user