diff --git a/.tests/fixtures/fixture_admin.xml b/.tests/fixtures/fixture_admin.xml index 362bc23..e39480f 100644 --- a/.tests/fixtures/fixture_admin.xml +++ b/.tests/fixtures/fixture_admin.xml @@ -3,6 +3,6 @@ - + diff --git a/.tests/fixtures/fixture_empty.xml b/.tests/fixtures/fixture_empty.xml index 0f69f1a..18b1d5b 100644 --- a/.tests/fixtures/fixture_empty.xml +++ b/.tests/fixtures/fixture_empty.xml @@ -3,6 +3,6 @@ - + diff --git a/.tests/tests/ConfigTest.php b/.tests/tests/ConfigTest.php index 499a01f..da701d5 100644 --- a/.tests/tests/ConfigTest.php +++ b/.tests/tests/ConfigTest.php @@ -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, diff --git a/css/main.css b/css/main.css index 61d38f9..b2da6f1 100644 --- a/css/main.css +++ b/css/main.css @@ -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; diff --git a/helpers/config.php b/helpers/config.php index 8700972..2a10944 100644 --- a/helpers/config.php +++ b/helpers/config.php @@ -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']; + } } } diff --git a/helpers/db.php b/helpers/db.php index fc8015e..bb167bc 100644 --- a/helpers/db.php +++ b/helpers/db.php @@ -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 diff --git a/helpers/lang.php b/helpers/lang.php index 0f97c0c..2737a1a 100644 --- a/helpers/lang.php +++ b/helpers/lang.php @@ -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; } diff --git a/helpers/utils.php b/helpers/utils.php index a4a8829..566d2de 100644 --- a/helpers/utils.php +++ b/helpers/utils.php @@ -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 diff --git a/images/add.svg b/images/add.svg new file mode 100644 index 0000000..236d8d3 --- /dev/null +++ b/images/add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/delete.svg b/images/delete.svg new file mode 100644 index 0000000..8c8ea25 --- /dev/null +++ b/images/delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/edit.svg b/images/edit.svg new file mode 100644 index 0000000..1125bc5 --- /dev/null +++ b/images/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/settings.svg b/images/settings.svg new file mode 100644 index 0000000..16dc4a1 --- /dev/null +++ b/images/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/index.php b/index.php index 55c3ebd..4042e3f 100644 --- a/index.php +++ b/index.php @@ -134,6 +134,7 @@
isAdmin()): ?> + diff --git a/js/src/ajax.js b/js/src/ajax.js index a9443ef..ce5bd4b 100644 --- a/js/src/ajax.js +++ b/js/src/ajax.js @@ -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); + } } diff --git a/js/src/config.js b/js/src/config.js index cdb68a8..4980ad3 100644 --- a/js/src/config.js +++ b/js/src/config.js @@ -17,19 +17,25 @@ * along with this program; if not, see . */ -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); + } } diff --git a/js/src/configdialogmodel.js b/js/src/configdialogmodel.js new file mode 100644 index 0000000..d5135f5 --- /dev/null +++ b/js/src/configdialogmodel.js @@ -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 . + */ + +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 += ``; + } + let unitOptions = ''; + for (const units of [ 'metric', 'imperial', 'nautical' ]) { + unitOptions += ``; + } + let layerOptions = ''; + for (const layer of this.model.layers) { + layerOptions += ``; + } + return `
${$._('settings')} ${$._('editingconfig')}
+
+
+ + + + +
+ + + + +
+ + + + + + + + + + + + + + + + +
+ + +
+
`; + } +} diff --git a/js/src/configviewmodel.js b/js/src/configviewmodel.js index 60111cf..bf4b815 100644 --- a/js/src/configviewmodel.js +++ b/js/src/configviewmodel.js @@ -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(); + } } diff --git a/js/src/dialog.js b/js/src/dialog.js index d8334cb..67d0f0d 100644 --- a/js/src/dialog.js +++ b/js/src/dialog.js @@ -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; diff --git a/js/src/lang.js b/js/src/lang.js index 593b554..0581eb0 100644 --- a/js/src/lang.js +++ b/js/src/lang.js @@ -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.} + */ + getLangList() { + return this.strings['langArr']; + } } diff --git a/js/src/layer.js b/js/src/layer.js index 8866221..d518c94 100644 --- a/js/src/layer.js +++ b/js/src/layer.js @@ -17,12 +17,39 @@ * along with this program; if not, see . */ -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; + } + } diff --git a/js/src/layercollection.js b/js/src/layercollection.js new file mode 100644 index 0000000..bfd08a2 --- /dev/null +++ b/js/src/layercollection.js @@ -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 . + */ + +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); + } + } + } + +} + diff --git a/js/src/mapapi/api_gmaps.js b/js/src/mapapi/api_gmaps.js index 414d0ec..4cd51a4 100644 --- a/js/src/mapapi/api_gmaps.js +++ b/js/src/mapapi/api_gmaps.js @@ -53,7 +53,7 @@ export default class GoogleMapsApi { * @return {Promise} */ 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) diff --git a/js/src/mapapi/api_openlayers.js b/js/src/mapapi/api_openlayers.js index f24a7c6..e07dc11 100644 --- a/js/src/mapapi/api_openlayers.js +++ b/js/src/mapapi/api_openlayers.js @@ -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.} 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() })); } diff --git a/js/src/userdialogmodel.js b/js/src/userdialogmodel.js index 4869661..fc64229 100644 --- a/js/src/userdialogmodel.js +++ b/js/src/userdialogmodel.js @@ -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; } } diff --git a/js/test/api_gmaps.test.js b/js/test/api_gmaps.test.js index 654a11f..550854c 100644 --- a/js/test/api_gmaps.test.js +++ b/js/test/api_gmaps.test.js @@ -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 diff --git a/js/test/api_openlayers.test.js b/js/test/api_openlayers.test.js index 520ade6..549d95f 100644 --- a/js/test/api_openlayers.test.js +++ b/js/test/api_openlayers.test.js @@ -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 diff --git a/js/test/config.test.js b/js/test/config.test.js index f76a546..f12c3e4 100644 --- a/js/test/config.test.js +++ b/js/test/config.test.js @@ -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(); + }); + }); diff --git a/js/test/dialog.test.js b/js/test/dialog.test.js index a0741b7..e54b466 100644 --- a/js/test/dialog.test.js +++ b/js/test/dialog.test.js @@ -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); }); diff --git a/js/test/userdialogmodel.test.js b/js/test/userdialogmodel.test.js index e51d072..54ef1a9 100644 --- a/js/test/userdialogmodel.test.js +++ b/js/test/userdialogmodel.test.js @@ -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); }); }); diff --git a/lang/en.php b/lang/en.php index 4ace2e0..ab47da7 100644 --- a/lang/en.php +++ b/lang/en.php @@ -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"; ?> diff --git a/scripts/setup.php b/scripts/setup.php index 578b3ca..257d5c1 100644 --- a/scripts/setup.php +++ b/scripts/setup.php @@ -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, diff --git a/scripts/ulogger.mysql b/scripts/ulogger.mysql index 1ed970a..4226089 100644 --- a/scripts/ulogger.mysql +++ b/scripts/ulogger.mysql @@ -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";'); -- -------------------------------------------------------- diff --git a/scripts/ulogger.pgsql b/scripts/ulogger.pgsql index 3c5c0b3..7c1278a 100644 --- a/scripts/ulogger.pgsql +++ b/scripts/ulogger.pgsql @@ -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";'); -- -------------------------------------------------------- diff --git a/scripts/ulogger.sqlite b/scripts/ulogger.sqlite index 804d737..e238128 100644 --- a/scripts/ulogger.sqlite +++ b/scripts/ulogger.sqlite @@ -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";'); -- -------------------------------------------------------- diff --git a/utils/getinit.php b/utils/getinit.php index edc1e2d..37a3197 100644 --- a/utils/getinit.php +++ b/utils/getinit.php @@ -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; } diff --git a/utils/saveconfig.php b/utils/saveconfig.php new file mode 100644 index 0000000..38908b1 --- /dev/null +++ b/utils/saveconfig.php @@ -0,0 +1,68 @@ +. + */ + +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();