Add settings dialog

This commit is contained in:
Bartek Fabiszewski 2020-02-23 22:21:17 +01:00
parent 33afc33405
commit 27d9e6a945
36 changed files with 1173 additions and 221 deletions

View File

@ -3,6 +3,6 @@
<users id="1" login="admin" password="$2y$10$7OvZrKgonVZM9lkzrTbiou.CVhO3HjPk5y0W9L68fVwPs/osBRIMq" admin="1" />
<tracks />
<positions />
<config map_api="openlayers" />
<config />
<ol_layers />
</dataset>

View File

@ -3,6 +3,6 @@
<users />
<tracks />
<positions />
<config map_api="openlayers" />
<config />
<ol_layers />
</dataset>

View File

@ -37,22 +37,20 @@ class ConfigTest extends UloggerDatabaseTestCase {
$this->resetAutoincrement();
$dataset = [
"config" => [
[
"map_api" => $this->mapApi,
"latitude" => $this->latitude,
"longitude" => $this->longitude,
"google_key" => $this->googleKey,
"require_auth" => (int) $this->requireAuth,
"public_tracks" => (int) $this->publicTracks,
"pass_lenmin" => $this->passLenMin,
"pass_strength" => $this->passStrength,
"interval_seconds" => $this->interval,
"lang" => $this->lang,
"units" => $this->units,
"stroke_weight" => $this->strokeWeight,
"stroke_color" => hexdec(str_replace('#', '', $this->strokeColor)),
"stroke_opacity" => $this->strokeOpacity * 100
]
[ "name" => "map_api", "value" => serialize($this->mapApi) ],
[ "name" => "latitude", "value" => serialize($this->latitude) ],
[ "name" => "longitude", "value" => serialize($this->longitude) ],
[ "name" => "google_key", "value" => serialize($this->googleKey) ],
[ "name" => "require_auth", "value" => serialize($this->requireAuth) ],
[ "name" => "public_tracks", "value" => serialize($this->publicTracks) ],
[ "name" => "pass_lenmin", "value" => serialize($this->passLenMin) ],
[ "name" => "pass_strength", "value" => serialize($this->passStrength) ],
[ "name" => "interval_seconds", "value" => serialize($this->interval) ],
[ "name" => "lang", "value" => serialize($this->lang) ],
[ "name" => "units", "value" => serialize($this->units) ],
[ "name" => "stroke_weight", "value" => serialize($this->strokeWeight) ],
[ "name" => "stroke_color", "value" => serialize($this->strokeColor) ],
[ "name" => "stroke_opacity", "value" => serialize($this->strokeOpacity) ]
],
"ol_layers" => [
[
@ -103,7 +101,6 @@ class ConfigTest extends UloggerDatabaseTestCase {
$this->config->save();
$this->assertEquals(1, $this->getConnection()->getRowCount('config'), "Wrong row count");
$expected = [
"map_api" => $this->config->mapApi,
"latitude" => $this->config->initLatitude,
@ -117,12 +114,17 @@ class ConfigTest extends UloggerDatabaseTestCase {
"lang" => $this->config->lang,
"units" => $this->config->units,
"stroke_weight" => $this->config->strokeWeight,
"stroke_color" => hexdec(str_replace('#', '', $this->config->strokeColor)),
"stroke_opacity" => (int) ($this->config->strokeOpacity * 100)
"stroke_color" => $this->config->strokeColor,
"stroke_opacity" => $this->config->strokeOpacity
];
$cnt = count($expected);
$this->assertEquals($cnt, $this->getConnection()->getRowCount('config'), "Wrong row count");
$actual = $this->getConnection()->createQueryTable("config", "SELECT * FROM config");
$this->assertTableContains($expected, $actual, "Wrong actual table data: " . implode(', ', $actual->getRow(0)));
for ($i = 0; $i < $cnt; $i++) {
$row = $actual->getRow($i);
$actualValue = $row['value'];
$this->assertEquals(serialize($expected[$row['name']]), is_resource($actualValue) ? stream_get_contents($actualValue) : $actualValue);
}
$this->assertEquals(1, $this->getConnection()->getRowCount('ol_layers'), "Wrong row count");
$expected = [
"id" => $this->config->olLayers[0]->id,

View File

@ -340,16 +340,14 @@ label[for=user] {
overflow: auto;
width: 100%;
height: 100%;
padding-top: 10%;
background-color: black; /* fallback */
background-color: rgba(0, 0, 0, 0.4);
}
#modal-header {
position: relative;
top: 20px;
width: 40%;
min-width: 300px;
position: absolute;
top: -10px;
right: 10px;
margin: 0 auto;
text-align: right;
}
@ -360,8 +358,13 @@ label[for=user] {
}
#modal-body {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) !important;
font-size: 0.9em;
width: 40%;
max-width: 600px;
min-width: 300px;
margin: 0 auto 15% auto;
padding: 1em;
@ -379,7 +382,10 @@ label[for=user] {
padding-top: 1em;
}
#modal input[type=text], #modal input[type=password] {
#modal input[type=text],
#modal input[type=color],
#modal input[type=number],
#modal input[type=password] {
display: inline-block;
box-sizing: border-box;
width: 100%;
@ -482,6 +488,50 @@ button > * {
}
}
#configForm label {
display: block;
}
#configForm label b {
display: inline-block;
text-align: right;
width: 250px;
margin-right: 10px;
font-size: small;
padding-top: 5px;
}
#configForm input[type=text],
#configForm input[type=number],
#configForm input[type=color],
#configForm select {
width: 150px;
margin: 3px 0;
padding: 2px 4px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
#configForm input[type=checkbox] {
margin: 0;
}
#configForm select {
padding: 2px 0;
}
#configForm input[type=color] {
vertical-align: middle;
padding: 0;
}
#configForm img {
height: 13px;
vertical-align: middle;
margin: 0 5px;
}
/* chart */
.ct-point {
transition: 0.3s;

View File

@ -106,11 +106,31 @@ class uConfig {
/**
* @var string Stroke color
*/
public $strokeColor = '#ff0000';
public $strokeColor = "#ff0000";
/**
* @var float Stroke opacity
*/
public $strokeOpacity = 1.0;
/**
* @var string Stroke color
*/
public $colorNormal = "#ffffff";
/**
* @var string Stroke color
*/
public $colorStart = "#55b500";
/**
* @var string Stroke color
*/
public $colorStop = "#ff6a00";
/**
* @var string Stroke color
*/
public $colorExtra = "#cccccc";
/**
* @var string Stroke color
*/
public $colorHilite = "#feff6a";
public function __construct($useDatabase = true) {
if ($useDatabase) {
@ -157,28 +177,10 @@ class uConfig {
*/
public function setFromDatabase() {
try {
$query = "SELECT map_api, latitude, longitude, google_key, require_auth, public_tracks,
pass_lenmin, pass_strength, interval_seconds, lang, units,
stroke_weight, stroke_color, stroke_opacity
FROM " . self::db()->table('config') . " LIMIT 1";
$query = "SELECT name, value FROM " . self::db()->table('config');
$result = self::db()->query($query);
$row = $result->fetch();
if ($row) {
if (!empty($row['map_api'])) { $this->mapApi = $row['map_api']; }
if (is_numeric($row['latitude'])) { $this->initLatitude = (float) $row['latitude']; }
if (is_numeric($row['longitude'])) { $this->initLongitude = (float) $row['longitude']; }
if (!empty($row['google_key'])) { $this->googleKey = $row['google_key']; }
if (is_numeric($row['require_auth']) || is_bool($row['require_auth'])) { $this->requireAuthentication = (bool) $row['require_auth']; }
if (is_numeric($row['public_tracks']) || is_bool($row['public_tracks'])) { $this->publicTracks = (bool) $row['public_tracks']; }
if (is_numeric($row['pass_lenmin'])) { $this->passLenMin = (int) $row['pass_lenmin']; }
if (is_numeric($row['pass_strength'])) { $this->passStrength = (int) $row['pass_strength']; }
if (is_numeric($row['interval_seconds'])) { $this->interval = (int) $row['interval_seconds']; }
if (!empty($row['lang'])) { $this->lang = $row['lang']; }
if (!empty($row['units'])) { $this->units = $row['units']; }
if (is_numeric($row['stroke_weight'])) { $this->strokeWeight = (int) $row['stroke_weight']; }
if (is_numeric($row['stroke_color'])) { $this->strokeColor = self::getColorAsHex($row['stroke_color']); }
if (is_numeric($row['stroke_opacity'])) { $this->strokeOpacity = $row['stroke_opacity'] / 100; }
}
$arr = $result->fetchAll(PDO::FETCH_KEY_PAIR);
$this->setFromArray(array_map([ $this, 'unserialize' ], $arr));
$this->setLayersFromDatabase();
if (!$this->requireAuthentication) {
// tracks must be public if we don't require authentication
@ -190,6 +192,18 @@ class uConfig {
}
}
/**
* Unserialize data from database
* @param string|resource $data Resource returned by pgsql, string otherwise
* @return mixed
*/
private function unserialize($data) {
if (is_resource($data)) {
return unserialize(stream_get_contents($data));
}
return unserialize($data);
}
/**
* Save config values to database
* @return bool True on success, false otherwise
@ -197,28 +211,54 @@ class uConfig {
public function save() {
$ret = false;
try {
// PDO::PARAM_LOB doesn't work here with pgsql, why?
$placeholder = self::db()->lobPlaceholder();
$query = "UPDATE " . self::db()->table('config') . "
SET map_api = ?, latitude = ?, longitude = ?, google_key = ?, require_auth = ?, public_tracks = ?,
pass_lenmin = ?, pass_strength = ?, interval_seconds = ?, lang = ?, units = ?,
stroke_weight = ?, stroke_color = ?, stroke_opacity = ?";
SET value = CASE name
WHEN 'map_api' THEN $placeholder
WHEN 'latitude' THEN $placeholder
WHEN 'longitude' THEN $placeholder
WHEN 'google_key' THEN $placeholder
WHEN 'require_auth' THEN $placeholder
WHEN 'public_tracks' THEN $placeholder
WHEN 'pass_lenmin' THEN $placeholder
WHEN 'pass_strength' THEN $placeholder
WHEN 'interval_seconds' THEN $placeholder
WHEN 'lang' THEN $placeholder
WHEN 'units' THEN $placeholder
WHEN 'stroke_weight' THEN $placeholder
WHEN 'stroke_color' THEN $placeholder
WHEN 'stroke_opacity' THEN $placeholder
WHEN 'color_normal' THEN $placeholder
WHEN 'color_start' THEN $placeholder
WHEN 'color_stop' THEN $placeholder
WHEN 'color_extra' THEN $placeholder
WHEN 'color_hilite' THEN $placeholder
END";
$stmt = self::db()->prepare($query);
$params = [
$this->mapApi,
$this->initLatitude,
$this->initLongitude,
$this->googleKey,
(int) $this->requireAuthentication,
(int) $this->publicTracks,
$this->requireAuthentication,
$this->publicTracks,
$this->passLenMin,
$this->passStrength,
$this->interval,
$this->lang,
$this->units,
$this->strokeWeight,
self::getColorAsInt($this->strokeColor),
(int) ($this->strokeOpacity * 100)
$this->strokeColor,
$this->strokeOpacity,
$this->colorNormal,
$this->colorStart,
$this->colorStop,
$this->colorExtra,
$this->colorHilite
];
$stmt->execute($params);
$stmt->execute(array_map('serialize', $params));
$this->saveLayers();
$ret = true;
} catch (PDOException $e) {
@ -315,19 +355,70 @@ class uConfig {
}
/**
* @param int $color Color value as integer
* @return string Color hex string
* Set config values from array
* @param array $arr
*/
private static function getColorAsHex($color) {
return '#' . sprintf('%03x', $color);
}
/**
* @param string $color Color hex string
* @return int Color value as integer
*/
private static function getColorAsInt($color) {
return hexdec(str_replace('#', '', $color));
public function setFromArray($arr) {
if (!is_array($arr)) {
return;
}
if (!empty($arr['map_api'])) {
$this->mapApi = $arr['map_api'];
}
if (is_numeric($arr['latitude'])) {
$this->initLatitude = (float) $arr['latitude'];
}
if (is_numeric($arr['longitude'])) {
$this->initLongitude = (float) $arr['longitude'];
}
if (!is_null($arr['google_key'])) {
$this->googleKey = $arr['google_key'];
}
if (is_numeric($arr['require_auth']) || is_bool($arr['require_auth'])) {
$this->requireAuthentication = (bool) $arr['require_auth'];
}
if (is_numeric($arr['public_tracks']) || is_bool($arr['public_tracks'])) {
$this->publicTracks = (bool) $arr['public_tracks'];
}
if (is_numeric($arr['pass_lenmin'])) {
$this->passLenMin = (int) $arr['pass_lenmin'];
}
if (is_numeric($arr['pass_strength'])) {
$this->passStrength = (int) $arr['pass_strength'];
}
if (is_numeric($arr['interval_seconds'])) {
$this->interval = (int) $arr['interval_seconds'];
}
if (!empty($arr['lang'])) {
$this->lang = $arr['lang'];
}
if (!empty($arr['units'])) {
$this->units = $arr['units'];
}
if (is_numeric($arr['stroke_weight'])) {
$this->strokeWeight = (int) $arr['stroke_weight'];
}
if (!empty($arr['stroke_color'])) {
$this->strokeColor = $arr['stroke_color'];
}
if (is_numeric($arr['stroke_opacity'])) {
$this->strokeOpacity = (float) $arr['stroke_opacity'];
}
if (!empty($arr['color_normal'])) {
$this->colorNormal = $arr['color_normal'];
}
if (!empty($arr['color_start'])) {
$this->colorStart = $arr['color_start'];
}
if (!empty($arr['color_stop'])) {
$this->colorStop = $arr['color_stop'];
}
if (!empty($arr['color_extra'])) {
$this->colorExtra = $arr['color_extra'];
}
if (!empty($arr['color_hilite'])) {
$this->colorHilite = $arr['color_hilite'];
}
}
}

View File

@ -165,6 +165,23 @@
}
}
/**
* Returns placeholder for LOB data types
* @return string
*/
public function lobPlaceholder() {
switch (self::$driver) {
default:
case "mysql":
case "sqlite":
return "?";
break;
case "pgsql":
return "?::bytea";
break;
}
}
/**
* Returns function name for getting date-time column value as 'YYYY-MM-DD hh:mm:ss'
* @param string $column

View File

@ -76,10 +76,6 @@
require(ROOT_DIR . "/lang/$language.php");
}
// choose password messages based on config
$passRules = "passrules_" . $config->passStrength;
$lang['passrules'] = isset($lang[$passRules]) ? $lang[$passRules] : "";
$lang['passlenmin'] = sprintf($lang["passlenmin"], $config->passLenMin);
$this->strings = $lang;
$this->setupStrings = $langSetup;
}

View File

@ -150,11 +150,13 @@
}
public static function postBool($name, $default = NULL) {
return self::requestValue($name, $default, INPUT_POST, FILTER_VALIDATE_BOOLEAN);
$input = filter_input(INPUT_POST, $name, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
return $input !== null ? (bool) $input : $default;
}
public static function getBool($name, $default = NULL) {
return self::requestValue($name, $default, INPUT_GET, FILTER_VALIDATE_BOOLEAN);
$input = filter_input(INPUT_GET, $name, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
return $input !== null ? (bool) $input : $default;
}
public static function postInt($name, $default = NULL) {
@ -175,6 +177,10 @@
return $default;
}
public static function postArray($name, $default = NULL) {
return ((isset($_POST[$name]) && is_array($_POST[$name])) ? $_POST[$name] : $default);
}
/**
* @param string $name Input name
* @param boolean $checkMime Optionally check mime with known types

1
images/add.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="white" d="M24 9h-9v-9h-6v9h-9v6h9v9h6v-9h9z"/></svg>

After

Width:  |  Height:  |  Size: 147 B

1
images/delete.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="white" d="M3 6v18h18v-18h-18zm5 14c0 .552-.448 1-1 1s-1-.448-1-1v-10c0-.552.448-1 1-1s1 .448 1 1v10zm5 0c0 .552-.448 1-1 1s-1-.448-1-1v-10c0-.552.448-1 1-1s1 .448 1 1v10zm5 0c0 .552-.448 1-1 1s-1-.448-1-1v-10c0-.552.448-1 1-1s1 .448 1 1v10zm4-18v2h-20v-2h5.711c.9 0 1.631-1.099 1.631-2h5.315c0 .901.73 2 1.631 2h5.712z"/></svg>

After

Width:  |  Height:  |  Size: 422 B

1
images/edit.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="white" d="M7.127 22.564l-7.126 1.436 1.438-7.125 5.688 5.689zm-4.274-7.104l5.688 5.689 15.46-15.46-5.689-5.689-15.459 15.46z"/></svg>

After

Width:  |  Height:  |  Size: 228 B

1
images/settings.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="white" d="M24 13.616v-3.232c-1.651-.587-2.694-.752-3.219-2.019v-.001c-.527-1.271.1-2.134.847-3.707l-2.285-2.285c-1.561.742-2.433 1.375-3.707.847h-.001c-1.269-.526-1.435-1.576-2.019-3.219h-3.232c-.582 1.635-.749 2.692-2.019 3.219h-.001c-1.271.528-2.132-.098-3.707-.847l-2.285 2.285c.745 1.568 1.375 2.434.847 3.707-.527 1.271-1.584 1.438-3.219 2.02v3.232c1.632.58 2.692.749 3.219 2.019.53 1.282-.114 2.166-.847 3.707l2.285 2.286c1.562-.743 2.434-1.375 3.707-.847h.001c1.27.526 1.436 1.579 2.019 3.219h3.232c.582-1.636.75-2.69 2.027-3.222h.001c1.262-.524 2.12.101 3.698.851l2.285-2.286c-.744-1.563-1.375-2.433-.848-3.706.527-1.271 1.588-1.44 3.221-2.021zm-12 2.384c-2.209 0-4-1.791-4-4s1.791-4 4-4 4 1.791 4 4-1.791 4-4 4z"/></svg>

After

Width:  |  Height:  |  Size: 824 B

View File

@ -134,6 +134,7 @@
<div id="admin-menu">
<div class="menu-title"><?= $lang['adminmenu'] ?></div>
<?php if ($auth->isAdmin()): ?>
<a id="adduser" class="menu-link" data-bind="onConfigEdit"><?= $lang['config'] ?></a>
<a id="adduser" class="menu-link" data-bind="onUserAdd"><?= $lang['adduser'] ?></a>
<a id="edituser" class="menu-link" data-bind="onUserEdit"><?= $lang['edituser'] ?></a>
<?php endif; ?>

View File

@ -91,7 +91,13 @@ export default class uAjax {
} else {
for (const key in data) {
if (data.hasOwnProperty(key)) {
params.push(`${key}=${encodeURIComponent(data[key])}`);
if (Array.isArray(data[key])) {
for (const value of data[key]) {
params.push(`${key}[]=${this.encodeValue(value)}`);
}
} else {
params.push(`${key}=${this.encodeValue(data[key])}`);
}
}
}
body = params.join('&');
@ -108,4 +114,11 @@ export default class uAjax {
xhr.send(body);
});
}
static encodeValue(value) {
if (typeof value === 'object') {
value = JSON.stringify(value);
}
return encodeURIComponent(value);
}
}

View File

@ -17,19 +17,25 @@
* along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import uLayer from './layer.js';
import uAjax from './ajax.js';
import uLayerCollection from './layercollection.js';
import uObserve from './observe.js';
/**
* @class uConfig
* @property {number} interval;
* @property {string} units
* @property {string} lang
* @property {string} mapApi
* @property {?string} gkey
* @property {uLayer[]} olLayers
* @property {string} googleKey
* @property {uLayerCollection} olLayers
* @property {number} initLatitude
* @property {number} initLongitude
* @property {RegExp} passRegex
* @property {number} initLongitude
* @property {boolean} requireAuth
* @property {boolean} publicTracks
* @property {number} passStrength
* @property {number} passLenMin
* @property {number} strokeWeight
* @property {string} strokeColor
* @property {number} strokeOpacity
@ -51,19 +57,22 @@ export default class uConfig {
this.units = 'metric';
this.lang = 'en';
this.mapApi = 'openlayers';
this.gkey = null;
this.olLayers = [];
this.googleKey = '';
this.olLayers = new uLayerCollection();
this.initLatitude = 52.23;
this.initLongitude = 21.01;
this.passRegex = new RegExp('(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{12,})');
this.requireAuth = true;
this.publicTracks = false;
this.passStrength = 2;
this.passLenMin = 10;
this.strokeWeight = 2;
this.strokeColor = '#ff0000';
this.strokeOpacity = 1;
// marker colors
this.colorNormal = '#fff';
this.colorNormal = '#ffffff';
this.colorStart = '#55b500';
this.colorStop = '#ff6a00';
this.colorExtra = '#ccc';
this.colorExtra = '#cccccc';
this.colorHilite = '#feff6a';
this.initUnits();
}
@ -94,15 +103,6 @@ export default class uConfig {
this.unitDay = 'unitday';
}
/**
* @param {Array} layers
*/
loadLayers(layers) {
for (const layer of layers) {
this.olLayers.push(new uLayer(layer.id, layer.name, layer.url, layer.priority));
}
}
/**
* Load config values from data object
* @param {Object} data
@ -110,11 +110,8 @@ export default class uConfig {
load(data) {
if (data) {
for (const property in data) {
if (property === 'olLayers') {
this.loadLayers(data[property]);
} else if (property === 'passRegex') {
const re = data[property];
this[property] = new RegExp(re.substr(1, re.length - 2));
if (property === 'layers') {
this.olLayers.load(data[property]);
} else if (data.hasOwnProperty(property) && this.hasOwnProperty(property)) {
this[property] = data[property];
}
@ -123,6 +120,21 @@ export default class uConfig {
}
}
/**
* Save config values from data object
* @param {Object} data
*/
save(data) {
this.load(data);
data = Object.keys(this)
.filter((key) => typeof this[key] !== 'function')
.reduce((obj, key) => {
obj[key] = this[key];
return obj;
}, {});
return uAjax.post('utils/saveconfig.php', data);
}
reinitialize() {
uObserve.unobserveAll(this);
this.initialize();
@ -135,4 +147,40 @@ export default class uConfig {
onChanged(property, callback) {
uObserve.observe(this, property, callback);
}
/**
* @param {string} password
* @return {boolean}
*/
validPassStrength(password) {
return this.getPassRegExp().test(password);
}
/**
* Set password validation regexp
* @return {RegExp}
*/
getPassRegExp() {
let regex = '';
if (this.passStrength > 0) {
// lower and upper case
regex += '(?=.*[a-z])(?=.*[A-Z])';
}
if (this.passStrength > 1) {
// digits
regex += '(?=.*[0-9])';
}
if (this.passStrength > 2) {
// not latin, not digits
regex += '(?=.*[^a-zA-Z0-9])';
}
if (this.passLenMin > 0) {
regex += `(?=.{${this.passLenMin},})`;
}
if (regex.length === 0) {
regex = '.*';
}
return new RegExp(regex);
}
}

270
js/src/configdialogmodel.js Normal file
View File

@ -0,0 +1,270 @@
/*
* μlogger
*
* Copyright(C) 2020 Bartek Fabiszewski (www.fabiszewski.net)
*
* This is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import { lang as $, config } from './initializer.js';
import ViewModel from './viewmodel.js';
import uDialog from './dialog.js';
import uLayer from './layer.js';
import uLayerCollection from './layercollection.js';
import uSelect from './select.js';
import uUtils from './utils.js';
/**
* @class ConfigDialogModel
*/
export default class ConfigDialogModel extends ViewModel {
constructor() {
super({
interval: config.interval,
units: config.units,
lang: config.lang,
mapApi: config.mapApi,
googleKey: config.googleKey,
layerId: config.olLayers.getPriorityLayer(),
layers: new uLayerCollection(new uLayer(0, 'OpenStreetMap', '', 0), ...config.olLayers),
layerName: null,
layerUrl: null,
initLatitude: config.initLatitude,
initLongitude: config.initLongitude,
requireAuth: config.requireAuth,
publicTracks: config.publicTracks,
passStrength: config.passStrength,
passLenMin: config.passLenMin,
strokeWeight: config.strokeWeight,
strokeColor: config.strokeColor,
strokeOpacity: config.strokeOpacity,
colorNormal: config.colorNormal,
colorStart: config.colorStart,
colorStop: config.colorStop,
colorExtra: config.colorExtra,
colorHilite: config.colorHilite
});
this.model.onCancel = () => this.onCancel();
this.model.onSave = () => this.onSave();
this.model.onLayerUpdate = () => this.onLayerUpdate();
this.model.onLayerCancel = () => this.onLayerCancel();
this.model.onLayerEdit = () => this.onLayerEdit();
this.model.onLayerDelete = () => this.onLayerDelete();
this.model.onLayerAdd = () => this.onLayerAdd();
}
init() {
const html = this.getHtml();
this.dialog = new uDialog(html);
this.dialog.show();
this.bindAll(this.dialog.element);
this.toggleEditEl = this.getBoundElement('onLayerEdit').parentNode;
this.layerEditEl = this.getBoundElement('layerName').parentNode;
this.layerSelect = new uSelect(this.getBoundElement('layerId'));
this.layerSelect.setOptions(this.model.layers, this.currentLayer().listValue);
this.setPublicTracksActivity(this.model.requireAuth);
this.onChanged('layerId', (listValue) => {
const layer = this.model.layers.get(listValue);
this.model.layerName = layer ? layer.name : '';
this.model.layerUrl = layer ? layer.url : '';
this.toggleEditVisible();
});
this.onChanged('layers', (list) => this.layerSelect.setOptions(list));
this.onChanged('requireAuth', (value) => {
this.setPublicTracksActivity(value);
});
}
setPublicTracksActivity(value) {
if (value) {
this.getBoundElement('publicTracks').disabled = false;
} else {
this.model.publicTracks = true;
this.getBoundElement('publicTracks').disabled = true;
}
}
onCancel() {
this.dialog.destroy();
}
onSave() {
if (this.validate()) {
this.model.layers.setPriorityLayer(this.model.layerId);
config.save(this.model)
.then(() => this.dialog.destroy())
.catch((e) => { uUtils.error(e, `${$._('actionfailure')}\n${e.message}`); });
}
}
/**
* Validate form
* @return {boolean} True if valid
*/
validate() {
const form = this.dialog.element.querySelector('form');
return form.checkValidity();
}
toggleEditVisible() {
if (this.model.layerId > 0) {
this.toggleEditEl.style.visibility = 'visible';
} else {
this.toggleEditEl.style.visibility = 'hidden';
this.hideEditElement();
}
}
onLayerDelete() {
this.model.layers.delete(this.model.layerId);
this.model.layerId = 0;
}
onLayerEdit() {
if (this.layerEditEl.style.display === 'none') {
this.showEditElement();
} else {
this.hideEditElement();
}
}
onLayerUpdate() {
if (!this.model.layerName || !this.model.layerUrl) {
return;
}
if (this.model.layerId === -1) {
this.model.layers.addNewLayer(this.model.layerName, this.model.layerUrl);
} else {
const layer = this.currentLayer();
layer.setName(this.model.layerName);
layer.setUrl(this.model.layerUrl);
}
this.hideEditElement();
this.layerSelect.setOptions(this.model.layers);
}
onLayerCancel() {
this.hideEditElement();
this.layerSelect.setOptions(this.model.layers);
}
onLayerAdd() {
this.model.layerId = -1;
this.onLayerEdit();
}
hideEditElement() {
this.layerEditEl.style.display = 'none';
}
showEditElement() {
this.layerEditEl.style.display = 'block';
}
currentLayer() {
return this.model.layers.get(this.model.layerId);
}
/**
* @return {string}
*/
getHtml() {
let langOptions = '';
for (const [ langCode, langName ] of Object.entries($.getLangList())) {
langOptions += `<option value="${langCode}"${this.model.lang === langCode ? ' selected' : ''}>${langName}</option>`;
}
let unitOptions = '';
for (const units of [ 'metric', 'imperial', 'nautical' ]) {
unitOptions += `<option value="${units}"${this.model.units === units ? ' selected' : ''}>${$._(units)}</option>`;
}
let layerOptions = '';
for (const layer of this.model.layers) {
layerOptions += `<option value="${layer.id}"${layer.priority > 0 ? ' selected' : ''}>${layer.name}</option>`;
}
return `<div><img style="vertical-align: bottom; margin-right: 10px;" src="images/settings.svg" alt="${$._('settings')}"> <b>${$._('editingconfig')}</b></div>
<div style="clear: both; padding-bottom: 1em;"></div>
<form id="configForm">
<label><b>${$._('language')}</b>
<select data-bind="lang">
${langOptions}
</select></label>
<label><b>${$._('units')}</b>
<select data-bind="units">
${unitOptions}
</select></label>
<label><b>${$._('api')}</b>
<select data-bind="mapApi">
<option value="openlayers"${this.model.mapApi === 'openlayers' ? ' selected' : ''}>OpenLayers</option>
<option value="gmaps"${this.model.mapApi === 'gmaps' ? ' selected' : ''}>Google Maps</option>
</select></label>
<label><b>${$._('ollayers')}</b>
<select data-bind="layerId">
${layerOptions}
</select>
<a data-bind="onLayerAdd"><img src="images/add.svg" alt="${$._('add')}"></a>
<span style="visibility: hidden;">
<a data-bind="onLayerEdit"><img src="images/edit.svg" alt="${$._('edit')}"></a>
<a data-bind="onLayerDelete"><img src="images/delete.svg" alt="${$._('delete')}"></a>
</span></label>
<div style="display: none; text-align: center;">
<input type="text" maxlength="50" placeholder="${$._('layername')}" data-bind="layerName">
<input type="text" maxlength="255" placeholder="${$._('layerurl')}" data-bind="layerUrl">
<button class="button-resolve" data-bind="onLayerUpdate" type="submit">${$._('submit')}</button>
<button class="button-reject" data-bind="onLayerCancel" type="button">${$._('cancel')}</button>
</div>
<label><b>${$._('interval')}</b>
<input type="number" data-bind="interval" min="1" value="${this.model.interval}" required></label>
<label><b>${$._('longitude')}</b>
<input type="number" data-bind="longitude" min="-180" max="180" step="0.01" value="${this.model.initLongitude}" required></label>
<label><b>${$._('latitude')}</b>
<input type="number" data-bind="latitude" min="-90" max="90" step="0.01" value="${this.model.initLatitude}" required></label>
<label><b>${$._('googlekey')}</b>
<input type="text" data-bind="googleKey" value="${this.model.googleKey}"></label>
<label><b>${$._('passlength')}</b>
<input type="number" data-bind="passLenMin" min="1" value="${this.model.passLenMin}" required></label>
<label><b>${$._('passstrength')}</b>
<select data-bind="passStrength">
<option value="0"${this.model.passStrength === 0 ? ' selected' : ''}>password</option>
<option value="1"${this.model.passStrength === 1 ? ' selected' : ''}>paSsword</option>
<option value="2"${this.model.passStrength === 2 ? ' selected' : ''}>paSsword1</option>
<option value="3"${this.model.passStrength === 3 ? ' selected' : ''}>paSsword1#</option>
</select></label>
<label><b>${$._('requireauth')}</b>
<input type="checkbox" data-bind="requireAuth"${this.model.requireAuth ? ' checked' : ''}></label>
<label><b>${$._('publictracks')}</b>
<input type="checkbox" data-bind="publicTracks"${this.model.publicTracks ? ' checked' : ''}></label>
<label><b>${$._('strokeweight')}</b>
<input type="number" data-bind="strokeWeight" min="1" value="${this.model.strokeWeight}" required></label>
<label><b>${$._('strokeopacity')}</b>
<input type="number" data-bind="strokeOpacity" min="0" max="1" step="0.01" value="${this.model.strokeOpacity}" required></label>
<label><b>${$._('strokecolor')}</b>
<input type="color" data-bind="strokeColor" pattern="#[0-9a-f]{6}" maxlength="7" value="${this.model.strokeColor}" required></label>
<label><b>${$._('colornormal')}</b>
<input type="color" data-bind="strokeColor" pattern="#[0-9a-f]{6}" maxlength="7" value="${this.model.colorNormal}" required></label>
<label><b>${$._('colorstart')}</b>
<input type="color" data-bind="strokeColor" pattern="#[0-9a-f]{6}" maxlength="7" value="${this.model.colorStart}" required></label>
<label><b>${$._('colorstop')}</b>
<input type="color" data-bind="strokeColor" pattern="#[0-9a-f]{6}" maxlength="7" value="${this.model.colorStop}" required></label>
<label><b>${$._('colorextra')}</b>
<input type="color" data-bind="strokeColor" pattern="#[0-9a-f]{6}" maxlength="7" value="${this.model.colorExtra}" required></label>
<label><b>${$._('colorhilite')}</b>
<input type="color" data-bind="strokeColor" pattern="#[0-9a-f]{6}" maxlength="7" value="${this.model.colorHilite}" required></label>
<div class="buttons">
<button class="button-reject" data-bind="onCancel" type="button">${$._('cancel')}</button>
<button class="button-resolve" data-bind="onSave" type="submit">${$._('submit')}</button>
</div>
</form>`;
}
}

View File

@ -18,6 +18,7 @@
*/
import { lang as $, config } from './initializer.js';
import ConfigDialogModel from './configdialogmodel.js';
import ViewModel from './viewmodel.js';
import uUtils from './utils.js';
@ -32,6 +33,7 @@ export default class ConfigViewModel extends ViewModel {
super(config);
this.state = state;
this.model.onSetInterval = () => this.setAutoReloadInterval();
this.model.onConfigEdit = () => this.showConfigDialog();
}
/**
@ -70,4 +72,9 @@ export default class ConfigViewModel extends ViewModel {
this.model.interval = interval;
}
}
showConfigDialog() {
const vm = new ConfigDialogModel(this);
vm.init();
}
}

View File

@ -40,7 +40,6 @@ export default class uDialog {
img.setAttribute('alt', $._('close'));
buttonClose.append(img);
dialogHeader.append(buttonClose);
dialog.append(dialogHeader);
const dialogBody = document.createElement('div');
dialogBody.setAttribute('id', 'modal-body');
if (typeof content === 'string') {
@ -52,6 +51,7 @@ export default class uDialog {
} else {
dialogBody.append(content);
}
dialogBody.prepend(dialogHeader);
dialog.append(dialogBody);
this.element = dialog;
this.visible = false;

View File

@ -161,4 +161,23 @@ export default class uLang {
}
return `${Math.abs(ipos).toLocaleString(this.config.lang)}°${dec.toLocaleString(this.config.lang, { maximumFractionDigits: 2 })}'${dir}`;
}
getLocalePassRules() {
let rulesStr = '';
if (this.config.passLenMin > 0) {
rulesStr = uUtils.sprintf(this._('passlenmin') + '\n', this.config.passLenMin);
}
if (this.config.passStrength > 0 && this.config.passStrength < 4) {
rulesStr += this._(`passrules_${this.config.passStrength}`);
}
return rulesStr;
}
/**
* Get languages list { langCode: langName }
* @return {Object.<string, string>}
*/
getLangList() {
return this.strings['langArr'];
}
}

View File

@ -17,12 +17,39 @@
* along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
export default class uLayer {
import uListItem from './listitem.js';
export default class uLayer extends uListItem {
/**
* @param {number} id
* @param {string} name
* @param {string} url
* @param {number} priority
*/
// eslint-disable-next-line max-params
constructor(id, name, url, priority) {
super();
this.id = id;
this.name = name;
this.url = url;
this.priority = priority;
this.listItem(id, name);
}
/**
* @param {string} name
*/
setName(name) {
this.name = name;
this.listItem(this.id, this.name);
}
/**
* @param {string} url
*/
setUrl(url) {
this.url = url;
}
}

112
js/src/layercollection.js Normal file
View File

@ -0,0 +1,112 @@
/*
* μlogger
*
* Copyright(C) 2020 Bartek Fabiszewski (www.fabiszewski.net)
*
* This is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import uLayer from './layer.js';
export default class uLayerCollection extends Array {
/**
* Create new layer in layers array
* @param {string} name
* @param {string} url
* @param {number} priority
*/
// eslint-disable-next-line max-params
addNewLayer(name, url, priority = 0) {
this.addLayer(this.getMaxId() + 1, name, url, priority);
}
/**
* @param {number} id
* @param {string} name
* @param {string} url
* @param {number} priority
*/
// eslint-disable-next-line max-params
addLayer(id, name, url, priority = 0) {
this.push(new uLayer(id, name, url, priority));
}
/**
* @param {number} id
*/
delete(id) {
const index = this.map((o) => o.id).indexOf(id);
this.splice(index, 1);
}
/**
* @param {number|string} id Id or listValue
*/
get(id) {
if (typeof id === 'string') {
return this.find((o) => o.listValue === id);
}
return this.find((o) => o.id === id);
}
/**
* Return max id from layers array
* @return {number}
*/
getMaxId() {
return Math.max(...this.map((o) => o.id), 0);
}
/**
* @param {number} id
*/
setPriorityLayer(id) {
for (const layer of this) {
if (layer.id > 0 && layer.id === id) {
layer.priority = 1;
} else {
layer.priority = 0;
}
}
}
/**
* Return id of first layer with priority
* @return {number}
*/
getPriorityLayer() {
for (const layer of this) {
if (layer.priority > 0) {
return layer.id;
}
}
return 0;
}
/**
* Load from array
* @param {Array} layers
*/
load(layers) {
this.length = 0;
for (const layer of layers) {
if (layer.id > 0) {
this.addLayer(layer.id, layer.name, layer.url, layer.priority);
}
}
}
}

View File

@ -53,7 +53,7 @@ export default class GoogleMapsApi {
* @return {Promise<void, Error>}
*/
init() {
const params = `?${(config.gkey) ? `key=${config.gkey}&` : ''}callback=gm_loaded`;
const params = `?${(config.googleKey) ? `key=${config.googleKey}&` : ''}callback=gm_loaded`;
const gmReady = Promise.all([
GoogleMapsApi.onScriptLoaded(),
uUtils.loadScript(`https://maps.googleapis.com/maps/api/js${params}`, 'mapapi_gmaps', GoogleMapsApi.loadTimeoutMs)

View File

@ -159,17 +159,17 @@ export default class OpenLayersApi {
// add extra tile layers
for (const layer of config.olLayers) {
const ol_layer = new ol.layer.TileLayer({
const olLayer = new ol.layer.TileLayer({
name: layer.name,
visible: false,
source: new ol.source.XYZ({
url: layer.url
})
});
this.map.addLayer(ol_layer);
this.map.addLayer(olLayer);
if (layer.priority) {
this.selectedLayer.setVisible(false);
this.selectedLayer = ol_layer;
this.selectedLayer = olLayer;
this.selectedLayer.setVisible(true);
}
}
@ -401,9 +401,7 @@ export default class OpenLayersApi {
element.className = 'ol-switcher-button ol-unselectable ol-control';
element.appendChild(switcherButton);
const switcherControl = new ol.control.Control({
element: element
});
const switcherControl = new ol.control.Control({ element });
this.map.addControl(switcherControl);
}
@ -463,7 +461,7 @@ export default class OpenLayersApi {
/**
* Set or replace ZoomToExtent control
* @param extent
* @param {Array.<number>} extent
*/
setZoomToExtent(extent) {
this.map.getControls().forEach((el) => {
@ -472,7 +470,7 @@ export default class OpenLayersApi {
}
});
this.map.addControl(new ol.control.ZoomToExtent({
extent,
extent: extent,
label: OpenLayersApi.getExtentImg()
}));
}

View File

@ -132,8 +132,8 @@ export default class UserDialogModel extends ViewModel {
alert($._('passnotmatch'));
return false;
}
if (!config.passRegex.test(this.model.password)) {
alert($._('passlenmin') + '\n' + $._('passrules'));
if (!config.validPassStrength(this.model.password)) {
alert($.getLocalePassRules());
return false;
}
}

View File

@ -83,12 +83,12 @@ describe('Google Maps map API tests', () => {
// given
spyOn(uUtils, 'loadScript').and.returnValue(Promise.resolve());
spyOn(api, 'initMap');
config.gkey = 'key1234567890';
config.googleKey = 'key1234567890';
// when
api.init()
.then(() => {
// then
expect(uUtils.loadScript).toHaveBeenCalledWith(`https://maps.googleapis.com/maps/api/js?key=${config.gkey}&callback=gm_loaded`, 'mapapi_gmaps', loadTimeout);
expect(uUtils.loadScript).toHaveBeenCalledWith(`https://maps.googleapis.com/maps/api/js?key=${config.googleKey}&callback=gm_loaded`, 'mapapi_gmaps', loadTimeout);
expect(api.initMap).toHaveBeenCalledTimes(1);
done();
})
@ -111,7 +111,7 @@ describe('Google Maps map API tests', () => {
expect(google.maps.InfoWindow.prototype.addListener).toHaveBeenCalledWith('closeclick', jasmine.any(Function));
});
it('should initialize map engine with null gkey', (done) => {
it('should initialize map engine without Google API key', (done) => {
// given
spyOn(uUtils, 'loadScript').and.returnValue(Promise.resolve());
// when

View File

@ -22,6 +22,7 @@ import OpenlayersApi from '../src/mapapi/api_openlayers.js';
import TrackFactory from './helpers/trackfactory.js';
import { config } from '../src/initializer.js'
import uLayer from '../src/layer.js';
import uLayerCollection from '../src/layercollection.js';
import uUtils from '../src/utils.js';
describe('Openlayers map API tests', () => {
@ -77,10 +78,10 @@ describe('Openlayers map API tests', () => {
it('should initialize map layers with config values', () => {
// given
spyOn(api, 'initLayerSwitcher');
config.olLayers = [
config.olLayers = new uLayerCollection(
new uLayer(1, 'layer1', 'http://layer1', 0),
new uLayer(1, 'layer2', 'http://layer2', 0)
];
);
api.map = mockMap;
// when

View File

@ -33,11 +33,10 @@ describe('Config tests', () => {
expect(config.units).toBeDefined();
expect(config.lang).toBeDefined();
expect(config.mapApi).toBeDefined();
expect(config.gkey).toBeDefined();
expect(config.googleKey).toBeDefined();
expect(config.olLayers).toBeDefined();
expect(config.initLatitude).toBeDefined();
expect(config.initLongitude).toBeDefined();
expect(config.passRegex).toBeDefined();
expect(config.strokeWeight).toBeDefined();
expect(config.strokeColor).toBeDefined();
expect(config.strokeOpacity).toBeDefined();
@ -128,15 +127,16 @@ describe('Config tests', () => {
expect(config.factorSpeed).toBe(2.237);
});
it('should parse regex if present in data', () => {
it('should generate regex for values in data', () => {
// given
const data = {
passRegex: '/(?=.{5,})/'
passLenMin: 5,
passStrength: 2
};
// when
config.load(data);
// then
expect(config.passRegex).toEqual(jasmine.any(RegExp));
expect(config.getPassRegExp()).toEqual(jasmine.any(RegExp));
});
it('should reinitialize config and remove any observers', () => {
@ -165,4 +165,150 @@ describe('Config tests', () => {
config.interval = newInterval;
});
it('should validate passwords length', () => {
// given
config.passLenMin = 4;
config.passStrength = 0;
// when
let password = '123';
// then
expect(config.validPassStrength(password)).toBeFalse();
// when
password = '';
// then
expect(config.validPassStrength(password)).toBeFalse();
// when
password = '1234';
// then
expect(config.validPassStrength(password)).toBeTrue();
// when
password = '1235';
// then
expect(config.validPassStrength(password)).toBeTrue();
});
it('should validate passwords strength 0', () => {
// given
config.passLenMin = 0;
config.passStrength = 0;
// when
let password = 'abcd';
// then
expect(config.validPassStrength(password)).toBeTrue();
// when
password = 'aBcD';
// then
expect(config.validPassStrength(password)).toBeTrue();
// when
password = '';
// then
expect(config.validPassStrength(password)).toBeTrue();
// when
password = '1234';
// then
expect(config.validPassStrength(password)).toBeTrue();
// when
password = 'Abcde1235$%';
// then
expect(config.validPassStrength(password)).toBeTrue();
});
it('should validate passwords strength 1 (one upper, one lower case letter)', () => {
// given
config.passLenMin = 0;
config.passStrength = 1;
// when
let password = 'abcd';
// then
expect(config.validPassStrength(password)).toBeFalse();
// when
password = 'aBcD';
// then
expect(config.validPassStrength(password)).toBeTrue();
// when
password = '';
// then
expect(config.validPassStrength(password)).toBeFalse();
// when
password = '1234';
// then
expect(config.validPassStrength(password)).toBeFalse();
// when
password = 'Abcde1235$%';
// then
expect(config.validPassStrength(password)).toBeTrue();
});
it('should validate passwords strength 2 (one upper, one lower case letter, one digit)', () => {
// given
config.passLenMin = 0;
config.passStrength = 2;
// when
let password = 'abcd';
// then
expect(config.validPassStrength(password)).toBeFalse();
// when
password = 'aBcD';
// then
expect(config.validPassStrength(password)).toBeFalse();
// when
password = '';
// then
expect(config.validPassStrength(password)).toBeFalse();
// when
password = '1234';
// then
expect(config.validPassStrength(password)).toBeFalse();
// when
password = 'ab1234';
// then
expect(config.validPassStrength(password)).toBeFalse();
// when
password = 'aB1234';
// then
expect(config.validPassStrength(password)).toBeTrue();
// when
password = 'Abcde1235$%';
// then
expect(config.validPassStrength(password)).toBeTrue();
});
it('should validate passwords strength 3 (one upper, one lower case letter, one digit, one non-alpha)', () => {
// given
config.passLenMin = 0;
config.passStrength = 3;
// when
let password = 'abcd';
// then
expect(config.validPassStrength(password)).toBeFalse();
// when
password = 'aBcD';
// then
expect(config.validPassStrength(password)).toBeFalse();
// when
password = '';
// then
expect(config.validPassStrength(password)).toBeFalse();
// when
password = '1234';
// then
expect(config.validPassStrength(password)).toBeFalse();
// when
password = 'ab1234';
// then
expect(config.validPassStrength(password)).toBeFalse();
// when
password = 'aB1234';
// then
expect(config.validPassStrength(password)).toBeFalse();
// when
password = 'ab1234.';
// then
expect(config.validPassStrength(password)).toBeFalse();
// when
password = 'Abcde1235$%';
// then
expect(config.validPassStrength(password)).toBeTrue();
});
});

View File

@ -42,6 +42,7 @@ describe('Dialog tests', () => {
it('should create dialog with string content', () => {
// when
const body = dialog.element.querySelector('#modal-body');
body.firstChild.remove();
// then
expect(body.innerHTML).toBe(content);
expect(dialog.visible).toBe(false);
@ -53,6 +54,7 @@ describe('Dialog tests', () => {
dialog = new uDialog(content);
// when
const body = dialog.element.querySelector('#modal-body');
body.firstChild.remove();
// then
expect(body.firstChild).toBe(content);
});
@ -66,6 +68,7 @@ describe('Dialog tests', () => {
dialog = new uDialog(content);
// when
const body = dialog.element.querySelector('#modal-body');
body.firstChild.remove();
// then
expect(body.children[0]).toBe(content[0]);
expect(body.children[1]).toBe(content[1]);
@ -81,6 +84,7 @@ describe('Dialog tests', () => {
dialog = new uDialog(content);
// when
const body = dialog.element.querySelector('#modal-body');
body.firstChild.remove();
// then
expect(body.childNodes).toEqual(content);
});

View File

@ -50,7 +50,7 @@ describe('UserDialogModel tests', () => {
spyOn(uUser, 'update').and.returnValue(Promise.resolve());
spyOn(auth.user, 'setPassword').and.returnValue(Promise.resolve());
spyOn(uUser, 'add').and.returnValue(Promise.resolve(newUser));
spyOn(config.passRegex, 'test').and.returnValue(true);
spyOn(config, 'validPassStrength').and.returnValue(true);
spyOn(window, 'alert');
});
@ -343,6 +343,6 @@ describe('UserDialogModel tests', () => {
// when
dm.validate();
// then
expect(config.passRegex.test).toHaveBeenCalledWith(password);
expect(config.validPassStrength).toHaveBeenCalledWith(password);
});
});

View File

@ -140,4 +140,29 @@ $lang["unitft"] = "ft"; // feet
$lang["unitmi"] = "mi"; // mile
$lang["unitkt"] = "kt"; // knot
$lang["unitnm"] = "nm"; // nautical mile
$lang["config"] = "Settings";
$lang["editingconfig"] = "Default application settings";
$lang["latitude"] = "Initial latitude";
$lang["longitude"] = "Initial longitude";
$lang["interval"] = "Interval";
$lang["googlekey"] = "Google Maps API key";
$lang["passlength"] = "Minimum password length";
$lang["passstrength"] = "Minimum password strength";
$lang["requireauth"] = "Require authorization";
$lang["publictracks"] = "Public tracks";
$lang["strokeweight"] = "Stroke weight";
$lang["strokeopacity"] = "Stroke opacity";
$lang["strokecolor"] = "Stroke color";
$lang["colornormal"] = "Marker color";
$lang["colorstart"] = "Start marker color";
$lang["colorstop"] = "Stop marker color";
$lang["colorextra"] = "Extra marker color";
$lang["colorhilite"] = "Hilite marker color";
$lang["ollayers"] = "OpenLayers layer";
$lang["layername"] = "Layer name";
$lang["layerurl"] = "Layer URL";
$lang["add"] = "Add";
$lang["edit"] = "Edit";
$lang["delete"] = "Delete";
$lang["settings"] = "Settings";
?>

View File

@ -244,23 +244,30 @@ function getQueries($dbDriver) {
) ENGINE=InnoDB DEFAULT CHARSET=utf8";
$queries[] = "CREATE TABLE `$tConfig` (
`map_api` varchar(50) NOT NULL DEFAULT 'openlayers',
`latitude` double NOT NULL DEFAULT '52.23',
`longitude` double NOT NULL DEFAULT '21.01',
`google_key` varchar(50) DEFAULT NULL,
`require_auth` boolean NOT NULL DEFAULT TRUE,
`public_tracks` boolean NOT NULL DEFAULT FALSE,
`pass_lenmin` int(11) NOT NULL DEFAULT '10',
`pass_strength` tinyint(1) NOT NULL DEFAULT '2',
`interval_seconds` int(11) NOT NULL DEFAULT '10',
`lang` varchar(10) NOT NULL DEFAULT 'en',
`units` varchar(10) NOT NULL DEFAULT 'metric',
`stroke_weight` int(11) NOT NULL DEFAULT '2',
`stroke_color` int(11) NOT NULL DEFAULT '16711680',
`stroke_opacity` int(11) NOT NULL DEFAULT '100'
`name` varchar(20) PRIMARY KEY,
`value` tinyblob NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8";
$queries[] = "INSERT INTO `$tConfig` () VALUES ();";
$queries[] = "INSERT INTO `$tConfig` (`name`, `value`) VALUES
('color_extra', 's:7:\"#cccccc\";'),
('color_hilite', 's:7:\"#feff6a\";'),
('color_normal', 's:7:\"#ffffff\";'),
('color_start', 's:7:\"#55b500\";'),
('color_stop', 's:7:\"#ff6a00\";'),
('google_key', 's:0:\"\";'),
('interval_seconds', 'i:10;'),
('lang', 's:2:\"en\";'),
('latitude', 'd:52.229999999999997;'),
('longitude', 'd:21.010000000000002;'),
('map_api', 's:10:\"openlayers\";'),
('pass_lenmin', 'i:10;'),
('pass_strength', 'i:2;'),
('public_tracks', 'b:1;'),
('require_auth', 'b:1;'),
('stroke_color', 's:7:\"#ff0000\";'),
('stroke_opacity', 'd:1;'),
('stroke_weight', 'i:2;'),
('units', 's:6:\"metric\";');";
$queries[] = "CREATE TABLE `$tLayers` (
`id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
@ -323,23 +330,30 @@ function getQueries($dbDriver) {
$queries[] = "CREATE INDEX idx_puser_id ON $tPositions(user_id)";
$queries[] = "CREATE TABLE $tConfig (
map_api varchar(50) NOT NULL DEFAULT 'openlayers',
latitude double precision NOT NULL DEFAULT '52.23',
longitude double precision NOT NULL DEFAULT '21.01',
google_key varchar(50) DEFAULT NULL,
require_auth boolean NOT NULL DEFAULT TRUE,
public_tracks boolean NOT NULL DEFAULT FALSE,
pass_lenmin int NOT NULL DEFAULT '10',
pass_strength smallint NOT NULL DEFAULT '2',
interval_seconds int NOT NULL DEFAULT '10',
lang varchar(10) NOT NULL DEFAULT 'en',
units varchar(10) NOT NULL DEFAULT 'metric',
stroke_weight int NOT NULL DEFAULT '2',
stroke_color int NOT NULL DEFAULT '16711680',
stroke_opacity int NOT NULL DEFAULT '100'
name varchar(20) PRIMARY KEY,
value bytea NOT NULL
)";
$queries[] = "INSERT INTO $tConfig DEFAULT VALUES";
$queries[] = "INSERT INTO $tConfig (name, value) VALUES
('color_extra', 's:7:\"#cccccc\";'),
('color_hilite', 's:7:\"#feff6a\";'),
('color_normal', 's:7:\"#ffffff\";'),
('color_start', 's:7:\"#55b500\";'),
('color_stop', 's:7:\"#ff6a00\";'),
('google_key', 's:0:\"\";'),
('interval_seconds', 'i:10;'),
('lang', 's:2:\"en\";'),
('latitude', 'd:52.229999999999997;'),
('longitude', 'd:21.010000000000002;'),
('map_api', 's:10:\"openlayers\";'),
('pass_lenmin', 'i:10;'),
('pass_strength', 'i:2;'),
('public_tracks', 'b:1;'),
('require_auth', 'b:1;'),
('stroke_color', 's:7:\"#ff0000\";'),
('stroke_opacity', 'd:1;'),
('stroke_weight', 'i:2;'),
('units', 's:6:\"metric\";')";
$queries[] = "CREATE TABLE $tLayers (
id serial PRIMARY KEY,
@ -401,23 +415,30 @@ function getQueries($dbDriver) {
$queries[] = "CREATE INDEX `idx_puser_id` ON `$tPositions`(`user_id`)";
$queries[] = "CREATE TABLE `$tConfig` (
`map_api` varchar(50) NOT NULL DEFAULT 'openlayers',
`latitude` double NOT NULL DEFAULT '52.23',
`longitude` double NOT NULL DEFAULT '21.01',
`google_key` varchar(50) DEFAULT NULL,
`require_auth` integer NOT NULL DEFAULT 1,
`public_tracks` integer NOT NULL DEFAULT 0,
`pass_lenmin` integer NOT NULL DEFAULT '10',
`pass_strength` integer NOT NULL DEFAULT '2',
`interval_seconds` integer NOT NULL DEFAULT '10',
`lang` varchar(10) NOT NULL DEFAULT 'en',
`units` varchar(10) NOT NULL DEFAULT 'metric',
`stroke_weight` integer NOT NULL DEFAULT '2',
`stroke_color` integer NOT NULL DEFAULT '16711680',
`stroke_opacity` integer NOT NULL DEFAULT '100'
`name` varchar(20) PRIMARY KEY,
`value` tinyblob NOT NULL
)";
$queries[] = "INSERT INTO `$tConfig` DEFAULT VALUES";
$queries[] = "INSERT INTO `$tConfig` (`name`, `value`) VALUES
('color_extra', 's:7:\"#cccccc\";'),
('color_hilite', 's:7:\"#feff6a\";'),
('color_normal', 's:7:\"#ffffff\";'),
('color_start', 's:7:\"#55b500\";'),
('color_stop', 's:7:\"#ff6a00\";'),
('google_key', 's:0:\"\";'),
('interval_seconds', 'i:10;'),
('lang', 's:2:\"en\";'),
('latitude', 'd:52.229999999999997;'),
('longitude', 'd:21.010000000000002;'),
('map_api', 's:10:\"openlayers\";'),
('pass_lenmin', 'i:10;'),
('pass_strength', 'i:2;'),
('public_tracks', 'b:1;'),
('require_auth', 'b:1;'),
('stroke_color', 's:7:\"#ff0000\";'),
('stroke_opacity', 'd:1;'),
('stroke_weight', 'i:2;'),
('units', 's:6:\"metric\";');";
$queries[] = "CREATE TABLE `$tLayers` (
`id` integer PRIMARY KEY AUTOINCREMENT,

View File

@ -71,27 +71,34 @@ CREATE TABLE `positions` (
DROP TABLE IF EXISTS `config`;
CREATE TABLE `config` (
`map_api` varchar(50) NOT NULL DEFAULT 'openlayers',
`latitude` double NOT NULL DEFAULT '52.23',
`longitude` double NOT NULL DEFAULT '21.01',
`google_key` varchar(50) DEFAULT NULL,
`require_auth` boolean NOT NULL DEFAULT TRUE,
`public_tracks` boolean NOT NULL DEFAULT FALSE,
`pass_lenmin` int(11) NOT NULL DEFAULT '10',
`pass_strength` tinyint(1) NOT NULL DEFAULT '2',
`interval_seconds` int(11) NOT NULL DEFAULT '10',
`lang` varchar(10) NOT NULL DEFAULT 'en',
`units` varchar(10) NOT NULL DEFAULT 'metric',
`stroke_weight` int(11) NOT NULL DEFAULT '2',
`stroke_color` int(11) NOT NULL DEFAULT '16711680',
`stroke_opacity` int(11) NOT NULL DEFAULT '100'
`name` varchar(20) PRIMARY KEY,
`value` tinyblob NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--
-- Data for table `config`
--
INSERT INTO `config` () VALUES ();
INSERT INTO `config` (`name`, `value`) VALUES
('color_extra', 's:7:"#cccccc";'),
('color_hilite', 's:7:"#feff6a";'),
('color_normal', 's:7:"#ffffff";'),
('color_start', 's:7:"#55b500";'),
('color_stop', 's:7:"#ff6a00";'),
('google_key', 's:0:"";'),
('interval_seconds', 'i:10;'),
('lang', 's:2:"en";'),
('latitude', 'd:52.229999999999997;'),
('longitude', 'd:21.010000000000002;'),
('map_api', 's:10:"openlayers";'),
('pass_lenmin', 'i:10;'),
('pass_strength', 'i:2;'),
('public_tracks', 'b:1;'),
('require_auth', 'b:1;'),
('stroke_color', 's:7:"#ff0000";'),
('stroke_opacity', 'd:1;'),
('stroke_weight', 'i:2;'),
('units', 's:6:"metric";');
-- --------------------------------------------------------

View File

@ -73,27 +73,34 @@ CREATE INDEX idx_puser_id ON positions(user_id);
DROP TABLE IF EXISTS config;
CREATE TABLE config (
map_api varchar(50) NOT NULL DEFAULT 'openlayers',
latitude double precision NOT NULL DEFAULT '52.23',
longitude double precision NOT NULL DEFAULT '21.01',
google_key varchar(50) DEFAULT NULL,
require_auth boolean NOT NULL DEFAULT TRUE,
public_tracks boolean NOT NULL DEFAULT FALSE,
pass_lenmin int NOT NULL DEFAULT '10',
pass_strength smallint NOT NULL DEFAULT '2',
interval_seconds int NOT NULL DEFAULT '10',
lang varchar(10) NOT NULL DEFAULT 'en',
units varchar(10) NOT NULL DEFAULT 'metric',
stroke_weight int NOT NULL DEFAULT '2',
stroke_color int NOT NULL DEFAULT '16711680',
stroke_opacity int NOT NULL DEFAULT '100'
name varchar(20) PRIMARY KEY,
value bytea NOT NULL
);
--
-- Data for table `config`
--
INSERT INTO config DEFAULT VALUES;
INSERT INTO config (name, value) VALUES
('color_extra', 's:7:"#cccccc";'),
('color_hilite', 's:7:"#feff6a";'),
('color_normal', 's:7:"#ffffff";'),
('color_start', 's:7:"#55b500";'),
('color_stop', 's:7:"#ff6a00";'),
('google_key', 's:0:"";'),
('interval_seconds', 'i:10;'),
('lang', 's:2:"en";'),
('latitude', 'd:52.229999999999997;'),
('longitude', 'd:21.010000000000002;'),
('map_api', 's:10:"openlayers";'),
('pass_lenmin', 'i:10;'),
('pass_strength', 'i:2;'),
('public_tracks', 'b:1;'),
('require_auth', 'b:1;'),
('stroke_color', 's:7:"#ff0000";'),
('stroke_opacity', 'd:1;'),
('stroke_weight', 'i:2;'),
('units', 's:6:"metric";');
-- --------------------------------------------------------

View File

@ -68,27 +68,34 @@ CREATE INDEX `idx_puser_id` ON `positions`(`user_id`);
DROP TABLE IF EXISTS `config`;
CREATE TABLE `config` (
`map_api` varchar(50) NOT NULL DEFAULT 'openlayers',
`latitude` double NOT NULL DEFAULT '52.23',
`longitude` double NOT NULL DEFAULT '21.01',
`google_key` varchar(50) DEFAULT NULL,
`require_auth` integer NOT NULL DEFAULT 1,
`public_tracks` integer NOT NULL DEFAULT 0,
`pass_lenmin` integer NOT NULL DEFAULT '10',
`pass_strength` integer NOT NULL DEFAULT '2',
`interval_seconds` integer NOT NULL DEFAULT '10',
`lang` varchar(10) NOT NULL DEFAULT 'en',
`units` varchar(10) NOT NULL DEFAULT 'metric',
`stroke_weight` integer NOT NULL DEFAULT '2',
`stroke_color` integer NOT NULL DEFAULT '16711680',
`stroke_opacity` integer NOT NULL DEFAULT '100'
`name` varchar(20) PRIMARY KEY,
`value` tinyblob NOT NULL
);
--
-- Data for table `config`
--
INSERT INTO `config` DEFAULT VALUES;
INSERT INTO `config` (`name`, `value`) VALUES
('color_extra', 's:7:"#cccccc";'),
('color_hilite', 's:7:"#feff6a";'),
('color_normal', 's:7:"#ffffff";'),
('color_start', 's:7:"#55b500";'),
('color_stop', 's:7:"#ff6a00";'),
('google_key', 's:0:"";'),
('interval_seconds', 'i:10;'),
('lang', 's:2:"en";'),
('latitude', 'd:52.229999999999997;'),
('longitude', 'd:21.010000000000002;'),
('map_api', 's:10:"openlayers";'),
('pass_lenmin', 'i:10;'),
('pass_strength', 'i:2;'),
('public_tracks', 'b:1;'),
('require_auth', 'b:1;'),
('stroke_color', 's:7:"#ff0000";'),
('stroke_opacity', 'd:1;'),
('stroke_weight', 'i:2;'),
('units', 's:6:"metric";');
-- --------------------------------------------------------

View File

@ -40,20 +40,25 @@ $resultConfig = [
"units" => $config->units,
"lang" => $config->lang,
"mapApi" => $config->mapApi,
"gkey" => $config->googleKey,
"googleKey" => $config->googleKey,
"initLatitude" => $config->initLatitude,
"initLongitude" => $config->initLongitude,
"passRegex" => $config->passRegex(),
"requireAuth" => $config->requireAuthentication,
"publicTracks" => $config->publicTracks,
"passLenMin" => $config->passLenMin,
"passStrength" => $config->passStrength,
"strokeWeight" => $config->strokeWeight,
"strokeColor" => $config->strokeColor,
"strokeOpacity" => $config->strokeOpacity,
"olLayers" => []
"layers" => []
];
foreach ($config->olLayers as $key => $val) {
$resultConfig["olLayers"][$key] = $val;
$resultConfig["layers"][$key] = $val;
}
$resultLang = [];
$resultLang = [
"langArr" => uLang::getLanguages()
];
foreach ($langStrings as $key => $val) {
$resultLang[$key] = $val;
}

68
utils/saveconfig.php Normal file
View File

@ -0,0 +1,68 @@
<?php
/**
* μlogger
*
* Copyright(C) 2020 Bartek Fabiszewski (www.fabiszewski.net)
*
* This is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
require_once(dirname(__DIR__) . "/helpers/auth.php");
require_once(ROOT_DIR . "/helpers/config.php");
$auth = new uAuth();
if (!$auth->isAdmin()) {
uUtils::exitWithError($lang["notauthorized"]);
}
$olLayers = uUtils::postArray('olLayers');
$data = [
'map_api' => uUtils::postString('mapApi'),
'latitude' => uUtils::postFloat('initLatitude'),
'longitude' => uUtils::postFloat('initLongitude'),
'google_key' => uUtils::postString('googleKey'),
'require_auth' => uUtils::postBool('requireAuth'),
'public_tracks' => uUtils::postBool('publicTracks'),
'pass_lenmin' => uUtils::postInt('passLenMin'),
'pass_strength' => uUtils::postInt('passStrength'),
'interval_seconds' => uUtils::postInt('interval'),
'lang' => uUtils::postString('lang'),
'units' => uUtils::postString('units'),
'stroke_weight' => uUtils::postInt('strokeWeight'),
'stroke_color' => uUtils::postString('strokeColor'),
'stroke_opacity' => uUtils::postFloat('strokeOpacity'),
'color_normal' => uUtils::postInt('colorNormal'),
'color_start' => uUtils::postInt('colorStart'),
'color_stop' => uUtils::postInt('colorStop'),
'color_extra' => uUtils::postInt('colorExtra'),
'color_hilite' => uUtils::postInt('colorHilite')
];
$config = uConfig::getInstance();
$config->setFromArray($data);
if (!is_null($olLayers)) {
$config->olLayers = [];
foreach ($olLayers as $json) {
$obj = json_decode($json);
if (json_last_error() === JSON_ERROR_NONE) {
$config->olLayers[] = new uLayer($obj->id, $obj->name, $obj->url, $obj->priority);
}
}
}
if ($config->save() === false) {
uUtils::exitWithError("Server error");
}
uUtils::exitWithSuccess();