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 @@