diff --git a/.tests/fixtures/config_0_6.php b/.tests/fixtures/config_0_6.php new file mode 100644 index 0000000..8e0373a --- /dev/null +++ b/.tests/fixtures/config_0_6.php @@ -0,0 +1,40 @@ +. + */ + +$mapapi = "testApi"; +$ol_layers["TestLayer"] = "https://test_tile.png"; +$init_latitude = 12.34; +$init_longitude = 12.34; +$gkey = "testGoogleKey"; +$dbdsn = ""; +$dbuser = ""; +$dbpass = ""; +$dbprefix = ""; +$require_authentication = 1; +$public_tracks = 0; +$admin_user = "admin"; +$pass_lenmin = 12; +$pass_strength = 2; +$interval = 1234; +$lang = "pl"; +$units = "imperial"; +$strokeWeight = 22; +$strokeColor = "#abcdef"; +$strokeOpacity = 1; +?> \ No newline at end of file diff --git a/.tests/fixtures/fixture_0_6.xml b/.tests/fixtures/fixture_0_6.xml new file mode 100644 index 0000000..a1a6b34 --- /dev/null +++ b/.tests/fixtures/fixture_0_6.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/.tests/fixtures/fixture_non_admin.xml b/.tests/fixtures/fixture_non_admin.xml new file mode 100644 index 0000000..9ace6e0 --- /dev/null +++ b/.tests/fixtures/fixture_non_admin.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.tests/fixtures/ulogger_0_6.mysql b/.tests/fixtures/ulogger_0_6.mysql new file mode 100644 index 0000000..1d55d89 --- /dev/null +++ b/.tests/fixtures/ulogger_0_6.mysql @@ -0,0 +1,87 @@ +/* + * μ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 . + */ + +-- +-- Database schema for μlogger 0.6 +-- + +-- -------------------------------------------------------- + +-- +-- Drop tables if exist +-- + +DROP TABLE IF EXISTS `positions`; +DROP TABLE IF EXISTS `tracks`; +DROP TABLE IF EXISTS `users`; +DROP TABLE IF EXISTS `config`; +DROP TABLE IF EXISTS `ol_layers`; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `users` +-- + +CREATE TABLE `users` ( + `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, + `login` varchar(15) CHARACTER SET latin1 NOT NULL UNIQUE, + `password` varchar(255) CHARACTER SET latin1 NOT NULL DEFAULT '' +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `tracks` +-- + +CREATE TABLE `tracks` ( + `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, + `user_id` int(11) NOT NULL, + `name` varchar(255) DEFAULT NULL, + `comment` varchar(1024) DEFAULT NULL, + INDEX `idx_user_id` (`user_id`), + FOREIGN KEY(`user_id`) REFERENCES `users`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `positions` +-- + +CREATE TABLE `positions` ( + `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, + `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `user_id` int(11) NOT NULL, + `track_id` int(11) NOT NULL, + `latitude` double NOT NULL, + `longitude` double NOT NULL, + `altitude` double DEFAULT NULL, + `speed` double DEFAULT NULL, + `bearing` double DEFAULT NULL, + `accuracy` int(11) DEFAULT NULL, + `provider` varchar(100) DEFAULT NULL, + `comment` varchar(255) DEFAULT NULL, + `image_id` int(11) DEFAULT NULL, + INDEX `idx_ptrack_id` (`track_id`), + INDEX `index_puser_id` (`user_id`), + FOREIGN KEY(`user_id`) REFERENCES `users`(`id`), + FOREIGN KEY(`track_id`) REFERENCES `tracks`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/.tests/fixtures/ulogger_0_6.pgsql b/.tests/fixtures/ulogger_0_6.pgsql new file mode 100644 index 0000000..616ff62 --- /dev/null +++ b/.tests/fixtures/ulogger_0_6.pgsql @@ -0,0 +1,89 @@ +/* + * μ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 . + */ + +-- +-- Database schema for μlogger 0.6 +-- + +-- -------------------------------------------------------- + +-- +-- Drop tables if exist +-- + +DROP TABLE IF EXISTS positions; +DROP TABLE IF EXISTS tracks; +DROP TABLE IF EXISTS users; +DROP TABLE IF EXISTS config; +DROP TABLE IF EXISTS ol_layers; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `users` +-- + +CREATE TABLE users ( + id serial PRIMARY KEY, + login varchar(15) NOT NULL UNIQUE, + password varchar(255) NOT NULL DEFAULT '' +); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `tracks` +-- + +CREATE TABLE tracks ( + id serial PRIMARY KEY, + user_id int NOT NULL, + name varchar(255) DEFAULT NULL, + comment varchar(1024) DEFAULT NULL, + FOREIGN KEY(user_id) REFERENCES users(id) +); + +CREATE INDEX idx_user_id ON tracks(user_id); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `positions` +-- + +CREATE TABLE positions ( + id serial PRIMARY KEY, + time timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, + user_id int NOT NULL, + track_id int NOT NULL, + latitude double precision NOT NULL, + longitude double precision NOT NULL, + altitude double precision DEFAULT NULL, + speed double precision DEFAULT NULL, + bearing double precision DEFAULT NULL, + accuracy int DEFAULT NULL, + provider varchar(100) DEFAULT NULL, + comment varchar(255) DEFAULT NULL, + image_id int DEFAULT NULL, + FOREIGN KEY(user_id) REFERENCES users(id), + FOREIGN KEY(track_id) REFERENCES tracks(id) +); + +CREATE INDEX idx_ptrack_id ON positions(track_id); +CREATE INDEX idx_puser_id ON positions(user_id); diff --git a/.tests/fixtures/ulogger_0_6.sqlite b/.tests/fixtures/ulogger_0_6.sqlite new file mode 100644 index 0000000..5bff253 --- /dev/null +++ b/.tests/fixtures/ulogger_0_6.sqlite @@ -0,0 +1,87 @@ +/* + * μ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 . + */ + +-- +-- Database schema for μlogger 0.6 +-- + +-- -------------------------------------------------------- + +-- +-- Drop tables if exist +-- + +DROP TABLE IF EXISTS `positions`; +DROP TABLE IF EXISTS `tracks`; +DROP TABLE IF EXISTS `users`; +DROP TABLE IF EXISTS `config`; +DROP TABLE IF EXISTS `ol_layers`; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `users` +-- + +CREATE TABLE `users` ( + `id` integer PRIMARY KEY AUTOINCREMENT, + `login` varchar(15) NOT NULL UNIQUE, + `password` varchar(255) NOT NULL DEFAULT '' +); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `tracks` +-- + +CREATE TABLE `tracks` ( + `id` integer PRIMARY KEY AUTOINCREMENT, + `user_id` integer NOT NULL, + `name` varchar(255) DEFAULT NULL, + `comment` varchar(1024) DEFAULT NULL, + FOREIGN KEY(`user_id`) REFERENCES `users`(`id`) +); +CREATE INDEX `idx_user_id` ON `tracks`(`user_id`); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `positions` +-- + +CREATE TABLE `positions` ( + `id` integer PRIMARY KEY AUTOINCREMENT, + `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `user_id` integer NOT NULL, + `track_id` integer NOT NULL, + `latitude` double NOT NULL, + `longitude` double NOT NULL, + `altitude` double DEFAULT NULL, + `speed` double DEFAULT NULL, + `bearing` double DEFAULT NULL, + `accuracy` integer DEFAULT NULL, + `provider` varchar(100) DEFAULT NULL, + `comment` varchar(255) DEFAULT NULL, + `image_id` integer DEFAULT NULL, + FOREIGN KEY(`user_id`) REFERENCES `users`(`id`), + FOREIGN KEY(`track_id`) REFERENCES `tracks`(`id`) +); +CREATE INDEX `idx_ptrack_id` ON `positions`(`track_id`); +CREATE INDEX `idx_puser_id` ON `positions`(`user_id`); diff --git a/.tests/lib/BaseDatabaseTestCase.php b/.tests/lib/BaseDatabaseTestCase.php index b97c519..dbdfd33 100644 --- a/.tests/lib/BaseDatabaseTestCase.php +++ b/.tests/lib/BaseDatabaseTestCase.php @@ -97,10 +97,10 @@ abstract class BaseDatabaseTestCase extends PHPUnit_Extensions_Database_TestCase protected function resetAutoincrement($users = 1, $tracks = 1, $positions = 1, $layers = 1) { if (self::$driver === "pgsql") { - self::$pdo->exec("ALTER SEQUENCE users_id_seq RESTART WITH $users"); - self::$pdo->exec("ALTER SEQUENCE tracks_id_seq RESTART WITH $tracks"); - self::$pdo->exec("ALTER SEQUENCE positions_id_seq RESTART WITH $positions"); - self::$pdo->exec("ALTER SEQUENCE ol_layers_id_seq RESTART WITH $layers"); + self::$pdo->exec("ALTER SEQUENCE IF EXISTS users_id_seq RESTART WITH $users"); + self::$pdo->exec("ALTER SEQUENCE IF EXISTS tracks_id_seq RESTART WITH $tracks"); + self::$pdo->exec("ALTER SEQUENCE IF EXISTS positions_id_seq RESTART WITH $positions"); + self::$pdo->exec("ALTER SEQUENCE IF EXISTS ol_layers_id_seq RESTART WITH $layers"); } else if (self::$driver === "sqlite") { $retry = 1; do { @@ -121,6 +121,17 @@ abstract class BaseDatabaseTestCase extends PHPUnit_Extensions_Database_TestCase } } + /** + * Reset connection + * Fixes sqlite error when db schema changes in another connection. + */ + protected function resetConnection() { + $this->closeConnection($this->conn); + $this->conn = null; + self::tearDownAfterClass(); + self::setUpBeforeClass(); + } + /** * Insert to database from array * diff --git a/.tests/lib/UloggerDatabaseTestCase.php b/.tests/lib/UloggerDatabaseTestCase.php index bf3ed07..b18895c 100644 --- a/.tests/lib/UloggerDatabaseTestCase.php +++ b/.tests/lib/UloggerDatabaseTestCase.php @@ -1,5 +1,4 @@ initConfigValues(); } + /** + * @throws ReflectionException + */ + protected function tearDown() { + parent::tearDown(); + $configClass = new ReflectionClass("uConfig"); + $configInstance = $configClass->getProperty('instance'); + $configInstance->setAccessible(true); + $configInstance->setValue(null); + } + protected function getDataSet() { $this->initConfigValues(); $this->resetAutoincrement(); diff --git a/.tests/tests/MigrateTest.php b/.tests/tests/MigrateTest.php new file mode 100644 index 0000000..1d46205 --- /dev/null +++ b/.tests/tests/MigrateTest.php @@ -0,0 +1,155 @@ +. + */ + +define("SKIP_RUN", true); +require_once(dirname(__DIR__) . "/../scripts/migrate_to_1_x.php"); +require_once(dirname(__DIR__) . "/lib/UloggerDatabaseTestCase.php"); + +class MigrateTest extends UloggerDatabaseTestCase { + + protected function tearDown() { + if ($this->getName() === "testUpdateSchemas") { + self::runSqlScript(dirname(__DIR__) . "/../scripts/ulogger." . $this->getDbDriverName()); + } + parent::tearDown(); + } + + public function testUpdateSchemas() { + self::runSqlScript(dirname(__DIR__) . "/fixtures/ulogger_0_6." . $this->getDbDriverName()); + $this->loadDataSet("fixture_0_6.xml"); + $this->assertEquals(1, $this->getConnection()->getRowCount("users"), "Wrong row count"); + $this->assertNotContains("admin", $this->getConnection()->getMetaData()->getTableColumns("users")); + $this->assertContains("image_id", $this->getConnection()->getMetaData()->getTableColumns("positions")); + $this->assertNotContains("ol_layers", $this->getConnection()->getMetaData()->getTableNames()); + $this->assertNotContains("config", $this->getConnection()->getMetaData()->getTableNames()); + $this->setOutputCallback(static function() {}); + $ret = updateSchemas(); + $this->resetConnection(); + $this->assertEquals(true, $ret, "Function updateSchemas() failed"); + $this->assertEquals(1, $this->getConnection()->getRowCount("users"), "Wrong row count"); + $this->assertEquals(1, $this->getConnection()->getRowCount("tracks"), "Wrong row count"); + $this->assertEquals(1, $this->getConnection()->getRowCount("positions"), "Wrong row count"); + $this->assertContains("admin", $this->getConnection()->getMetaData()->getTableColumns("users"), "Missing table column"); + $this->assertContains("image", $this->getConnection()->getMetaData()->getTableColumns("positions"), "Missing table column"); + $this->assertContains("ol_layers", $this->getConnection()->getMetaData()->getTableNames(), "Missing table"); + $this->assertContains("config", $this->getConnection()->getMetaData()->getTableNames(), "Missing table"); + } + + public function testUpdateConfig() { + $this->loadDataSet("fixture_non_admin.xml"); + $this->setOutputCallback(static function() {}); + $ret = updateConfig(dirname(__DIR__) . "/fixtures/config_0_6.php"); + $this->assertEquals(true, $ret, "Function updateConfig() failed"); + // admin user imported from config file + $expected = [ "admin" => 1 ]; + $actual = $this->getConnection()->createQueryTable( + "admin", + "SELECT admin FROM users WHERE login = 'admin'" + ); + $this->assertTableContains($expected, $actual, "Wrong actual table data"); + // settings imported from config file + $expected = [ "config" => [ + ["name" => "color_extra", "value" => "s:7:\"#cccccc\";"], // default + ["name" => "color_hilite", "value" => "s:7:\"#feff6a\";"], // default + ["name" => "color_normal", "value" => "s:7:\"#ffffff\";"], // default + ["name" => "color_start", "value" => "s:7:\"#55b500\";"], // default + ["name" => "color_stop", "value" => "s:7:\"#ff6a00\";"], // default + ["name" => "google_key", "value" => "s:13:\"testGoogleKey\";"], + ["name" => "interval_seconds", "value" => "i:1234;"], + ["name" => "lang", "value" => "s:2:\"pl\";"], + ["name" => "latitude", "value" => "d:12.34;"], + ["name" => "longitude", "value" => "d:12.34;"], + ["name" => "map_api", "value" => "s:7:\"testApi\";"], + ["name" => "pass_lenmin", "value" => "i:12;"], + ["name" => "pass_strength", "value" => "i:2;"], + ["name" => "public_tracks", "value" => "b:0;"], + ["name" => "require_auth", "value" => "b:1;"], + ["name" => "stroke_color", "value" => "s:7:\"#abcdef\";"], + ["name" => "stroke_opacity", "value" => "i:1;"], + ["name" => "stroke_weight", "value" => "i:22;"], + ["name" => "units", "value" => "s:8:\"imperial\";"] + ]]; + $actual = $this->getConnection()->createQueryTable( + "config", + "SELECT name, " . uDb::getInstance()->from_lob("value") . " FROM config ORDER BY name" + ); + $expected = $this->createArrayDataSet($expected)->getTable("config"); + self::assertTablesEqual($expected, $actual); + // layers imported from config file + $this->assertEquals(1, $this->getConnection()->getRowCount("ol_layers"), "Wrong row count"); + $expected = [ "id" => 1, "name" => "TestLayer", "url" => "https://test_tile.png", "priority" => 0 ]; + $actual = $this->getConnection()->createQueryTable( + "ol_layers", + "SELECT * FROM ol_layers" + ); + $this->assertTableContains($expected, $actual, "Wrong actual table data"); + } + + public function testWaitForUser() { + $this->setOutputCallback(static function() {}); + $yes = tmpfile(); + fwrite($yes, "yes"); + $ret = waitForUser(stream_get_meta_data($yes)['uri']); + fclose($yes); + $this->assertEquals(true, $ret, "Wrong return status"); + + $no = tmpfile(); + fwrite($no, "no"); + $ret = waitForUser(stream_get_meta_data($no)['uri']); + fclose($no); + $this->assertEquals(false, $ret, "Wrong return status"); + } + + /** + * Run SQL commands from file. + * Basic subset only. Multiple commands must not be on the same line. + * @param string $path Script path + * @throws PDOException + */ + private static function runSqlScript($path) { + $script = file_get_contents($path); + $count = preg_match_all('/^(?:(?:DROP|CREATE) (?:TABLE|INDEX)|INSERT|PRAGMA|SET) .*?;\s*$/smi', $script, $queries); + if ($count) { + try { + uDb::getInstance()->beginTransaction(); + foreach ($queries[0] as $query) { + uDb::getInstance()->exec($query); + } + uDb::getInstance()->commit(); + } catch (PDOException $e) { + uDb::getInstance()->rollBack(); + throw $e; + } + } + } + + private function getDbDriverName() { + return uDb::getInstance()->getAttribute(PDO::ATTR_DRIVER_NAME); + } + + private function loadDataSet($name) { + $this->resetAutoincrement(); + $dataSet = $this->createFlatXMLDataSet(dirname(__DIR__) . '/fixtures/' . $name); + $this->getDatabaseTester()->setDataSet($dataSet); + $this->getDatabaseTester()->onSetUp(); + } +} + +?> diff --git a/README.md b/README.md index 954bbef..18740f4 100644 --- a/README.md +++ b/README.md @@ -43,13 +43,20 @@ Together with a dedicated [μlogger mobile client](https://github.com/bfabiszews - Folders `.docker/` and `.tests/` as well as composer files are needed only for development. May be safely removed. ## Upgrade to version 1.x -- TODO: convert following notes to migration script -- Database changes: - - `ALTER TABLE positions CHANGE image_id image VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL` - - `ALTER TABLE users ADD admin BOOLEAN NOT NULL DEFAULT FALSE AFTER password` - - new tables for config values: `config` and `ol_layers`, see SQL files in scripts folder, eg. [mysql](https://github.com/bfabiszewski/ulogger-server/blob/master/scripts/ulogger.mysql) - - modify admin user entry in `users` table: set `admin` to `true` -- Config file changes: only database setup is defined in config file, see [config.default.php](https://github.com/bfabiszewski/ulogger-server/blob/master/config.default.php) for valid values +- Incompatible changes include database and config file changes +- Upgrading manually: + - for database changes, see MySQL example script in `scripts/migrate_to_1_x.mysql` + - set `admin` column in `users` table to true for admin users + - edit configuration from application settings dialog +- Upgrading with migration script from version 0.6: + - create database backup + - replace all project files with new ones, but keep old local config file: `config.php` + - change directory to application root folder and run migration script from the console: `php scripts/migrate_to_1_x.php` + - the script will update database schema and save old config variables to database +- Additional tasks after manual or script update: + - edit your `config.php` file and remove all variables except database settings, see [config.default.php](https://github.com/bfabiszewski/ulogger-server/blob/master/config.default.php) for valid values + - make sure `uploads` folder (for images uploaded from client app) is writable by PHP + - clear browser cache and restart web page ## Docker - Run `docker run --name ulogger -p 8080:80 -d bfabiszewski/ulogger` and access `http://localhost:8080` in your browser. Log in with `admin`:`admin` credentials and change default password. diff --git a/helpers/db.php b/helpers/db.php index dffd40a..2547087 100644 --- a/helpers/db.php +++ b/helpers/db.php @@ -184,6 +184,24 @@ require_once(ROOT_DIR . "/helpers/utils.php"); } } + /** + * Returns construct for getting LOB as string + * @param string $column Column name + * @return string + */ + public function from_lob($column) { + switch (self::$driver) { + default: + case "mysql": + case "sqlite": + return $column; + break; + case "pgsql": + return "encode($column, 'escape') AS $column"; + break; + } + } + /** * Returns function name for getting date-time column value as 'YYYY-MM-DD hh:mm:ss' * @param string $column diff --git a/scripts/migrate_to_1_x.mysql b/scripts/migrate_to_1_x.mysql new file mode 100644 index 0000000..da8a2a0 --- /dev/null +++ b/scripts/migrate_to_1_x.mysql @@ -0,0 +1,87 @@ +/* + * μ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 . + */ + +-- +-- Table structure for table `config` +-- + +CREATE TABLE `config` ( + `name` varchar(20) PRIMARY KEY, + `value` tinyblob NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- +-- Data for table `config` +-- + +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";'); + +-- +-- Table structure for table `ol_layers` +-- + +CREATE TABLE `ol_layers` ( + `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, + `name` varchar(50) NOT NULL, + `url` varchar(255) NOT NULL, + `priority` int(11) NOT NULL DEFAULT '0' +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- +-- Data for table `ol_layers` +-- + +INSERT INTO `ol_layers` (`id`, `name`, `url`, `priority`) VALUES +(1, 'OpenCycleMap', 'https://{a-c}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png', 0), +(2, 'OpenTopoMap', 'https://{a-c}.tile.opentopomap.org/{z}/{x}/{y}.png', 0), +(3, 'OpenSeaMap', 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', 0), +(4, 'ESRI', 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', 0), +(5, 'UMP', 'http://{1-3}.tiles.ump.waw.pl/ump_tiles/{z}/{x}/{y}.png', 0), +(6, 'Osmapa.pl', 'http://{a-c}.tile.openstreetmap.pl/osmapa.pl/{z}/{x}/{y}.png', 0); + +-- +-- Add admin column to `users` table +-- + +ALTER TABLE `users` ADD `admin` BOOLEAN NOT NULL DEFAULT FALSE AFTER `password`; + +-- +-- Modify image column in `positions` table +-- + +ALTER TABLE `positions` CHANGE `image_id` `image` VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL; diff --git a/scripts/migrate_to_1_x.php b/scripts/migrate_to_1_x.php new file mode 100644 index 0000000..96621dc --- /dev/null +++ b/scripts/migrate_to_1_x.php @@ -0,0 +1,376 @@ +. + */ + +/* + * CLI script for database migration from μlogger version 0.6 to version 1.x. + * Database user defined in config file must have privileges to create and modify tables. + * + * Backup your database before running this script. + */ + +if (PHP_SAPI !== "cli") { + die("This script must be called from CLI console"); +} + +if (!defined("ROOT_DIR")) { define("ROOT_DIR", dirname(__DIR__)); } + +include_once(ROOT_DIR . "/helpers/config.php"); +include_once(ROOT_DIR . "/helpers/db.php"); + +if (!defined("SKIP_RUN")) { + verifyVersion() || exit(1); + waitForUser() || exit(0); + updateSchemas() || exit(1); + updateConfig() || exit(1); + exit(0); +} + +/** + * Check μlogger version is valid for migration + * @return bool True if valid version + */ +function verifyVersion() { + if (!class_exists("uConfig") || !class_exists("uDb") || + !method_exists("uConfig", "getOfflineInstance") || + strpos(uConfig::getOfflineInstance()->version, '1.') !== 0) { + echo "You need μlogger version 1.x to run this script" . PHP_EOL; + return false; + } + return true; +} + +/** + * Waits for user confirmation + * @param string $input Optional path to read from (for tests) + * @return bool True if confirmed + */ +function waitForUser($input = "php://stdin") { + echo "This script will update database from version 0.6 to 1.x." . PHP_EOL; + echo "Creating database backup is recommended before proceeding" . PHP_EOL; + echo "Type 'yes' to continue, anything else to abort" . PHP_EOL; + $handle = fopen($input, 'rb'); + $input = fgets($handle); + fclose($handle); + if (trim($input) !== "yes") { + echo "Cancelled by user" . PHP_EOL; + return false; + } + echo PHP_EOL; + echo "Starting migration..." . PHP_EOL; + return true; +} + +/** + * Updates database schemas + * @return bool True on success + */ +function updateSchemas() { + echo "Migrating database schemas..." . PHP_EOL; + try { + $queries = getQueries(); + uDb::getInstance()->beginTransaction(); + foreach ($queries as $query) { + uDb::getInstance()->exec($query); + } + uDb::getInstance()->commit(); + } catch (PDOException $e) { + echo "Database query failed: {$e->getMessage()}" . PHP_EOL; + echo "Reverting changes..." . PHP_EOL; + uDb::getInstance()->rollBack(); + return false; + } + echo PHP_EOL; + echo "Database schemas updated successfully" . PHP_EOL; + return true; +} + +/** + * Update database settings based on current config file + * @param string $path Optional path of config (for tests) + * @return bool True on success + */ +/** @noinspection IssetArgumentExistenceInspection, PhpIncludeInspection */ +function updateConfig($path = ROOT_DIR . "/config.php") { + echo "Migrating config to database..." . PHP_EOL; + require_once($path); + if (isset($admin_user) && !empty($admin_user)) { + try { + echo "Setting user $admin_user as admin" . PHP_EOL; + $query = "UPDATE " . uDb::getInstance()->table('users') . " SET admin = TRUE WHERE login = ?"; + $stmt = uDb::getInstance()->prepare($query); + $stmt->execute([ $admin_user ]); + if ($stmt->rowCount() === 0) { + echo "User $admin_user not found in database table" . PHP_EOL; + echo "Set your admin user manually in users table" . PHP_EOL; + } + } catch (PDOException $e) { + echo "Setting admin user failed: " . $e->getMessage() . PHP_EOL; + echo "Set your admin user manually in users table" . PHP_EOL; + } + } + $config = uConfig::getOfflineInstance(); + if (isset($mapapi) && !empty($mapapi)) { + $config->mapApi = $mapapi; + } + if (isset($ol_layers) && is_array($ol_layers)) { + $id = 1; + foreach ($ol_layers as $name => $url) { + $config->olLayers[] = new uLayer($id++, $name, $url, 0); + } + } + if (isset($init_latitude) && is_numeric($init_latitude)) { + $config->initLatitude = $init_latitude; + } + if (isset($init_longitude) && is_numeric($init_longitude)) { + $config->initLongitude = $init_longitude; + } + if (isset($gkey) && !empty($gkey)) { + $config->googleKey = $gkey; + } + if (isset($require_authentication) && is_numeric($require_authentication)) { + $config->requireAuthentication = (bool) $require_authentication; + } + if (isset($public_tracks) && is_numeric($public_tracks)) { + $config->publicTracks = (bool) $public_tracks; + } + if (isset($pass_lenmin) && is_numeric($pass_lenmin)) { + $config->passLenMin = (int) $pass_lenmin; + } + if (isset($pass_strength) && is_numeric($pass_strength)) { + $config->passStrength = (int) $pass_strength; + } + if (isset($interval) && is_numeric($interval)) { + $config->interval = (int) $interval; + } + if (isset($lang) && !empty($lang)) { + $config->lang = $lang; + } + if (isset($units) && !empty($units)) { + $config->units = $units; + } + if (isset($strokeWeight) && is_numeric($strokeWeight)) { + $config->strokeWeight = (int) $strokeWeight; + } + if (isset($strokeColor) && !empty($strokeColor)) { + $config->strokeColor = $strokeColor; + } + if (isset($strokeOpacity) && is_numeric($strokeOpacity)) { + $config->strokeOpacity = $strokeOpacity; + } + if ($config->save() !== true) { + echo "Configuration migration failed. Please update your settings manually from web interface" . PHP_EOL; + return false; + } + echo "Configuration successfully migrated to database" . PHP_EOL; + return true; +} + +/** + * Get queries array for current db driver + * @return array Queries + */ +function getQueries() { + $dbDriver = uDb::getInstance()->getAttribute(PDO::ATTR_DRIVER_NAME); + $tConfig = uDb::getInstance()->table('config'); + $tLayers = uDb::getInstance()->table('ol_layers'); + $tUsers = uDb::getInstance()->table('users'); + $tTracks = uDb::getInstance()->table('tracks'); + $tPositions = uDb::getInstance()->table('positions'); + $tPositionsBackup = "{$tPositions}_backup"; + + $queries = []; + switch ($dbDriver) { + case "mysql": + $queries[] = "CREATE TABLE `$tConfig` ( + `name` varchar(20) PRIMARY KEY, + `value` tinyblob NOT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"; + + $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, + `name` varchar(50) NOT NULL, + `url` varchar(255) NOT NULL, + `priority` int(11) NOT NULL DEFAULT '0' + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"; + + $queries[] = "INSERT INTO `$tLayers` (`id`, `name`, `url`, `priority`) VALUES + (1, 'OpenCycleMap', 'https://{a-c}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png', 0), + (2, 'OpenTopoMap', 'https://{a-c}.tile.opentopomap.org/{z}/{x}/{y}.png', 0), + (3, 'OpenSeaMap', 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', 0), + (4, 'ESRI', 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', 0), + (5, 'UMP', 'http://{1-3}.tiles.ump.waw.pl/ump_tiles/{z}/{x}/{y}.png', 0), + (6, 'Osmapa.pl', 'http://{a-c}.tile.openstreetmap.pl/osmapa.pl/{z}/{x}/{y}.png', 0); + "; + + $queries[] = "ALTER TABLE `$tUsers` ADD `admin` BOOLEAN NOT NULL DEFAULT FALSE AFTER `password`"; + + $queries[] = "ALTER TABLE `$tPositions` CHANGE `image_id` `image` VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL"; + + break; + + case "pgsql": + $queries[] = "CREATE TABLE $tConfig ( + name varchar(20) PRIMARY KEY, + value bytea NOT NULL + )"; + + $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, + name varchar(50) NOT NULL, + url varchar(255) NOT NULL, + priority int NOT NULL DEFAULT '0' + )"; + + $queries[] = "INSERT INTO $tLayers (id, name, url, priority) VALUES + (1, 'OpenCycleMap', 'https://{a-c}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png', 0), + (2, 'OpenTopoMap', 'https://{a-c}.tile.opentopomap.org/{z}/{x}/{y}.png', 0), + (3, 'OpenSeaMap', 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', 0), + (4, 'ESRI', 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', 0), + (5, 'UMP', 'http://{1-3}.tiles.ump.waw.pl/ump_tiles/{z}/{x}/{y}.png', 0), + (6, 'Osmapa.pl', 'http://{a-c}.tile.openstreetmap.pl/osmapa.pl/{z}/{x}/{y}.png', 0); + "; + + $queries[] = "ALTER TABLE $tUsers ADD COLUMN admin boolean NOT NULL DEFAULT FALSE"; + $queries[] = "ALTER TABLE $tPositions DROP COLUMN image_id"; + $queries[] = "ALTER TABLE $tPositions ADD COLUMN image varchar(100) NULL DEFAULT NULL"; + + break; + + case "sqlite": + $queries[] = "CREATE TABLE `$tConfig` ( + `name` varchar(20) PRIMARY KEY, + `value` tinyblob NOT NULL + )"; + + $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, + `name` varchar(50) NOT NULL, + `url` varchar(255) NOT NULL, + `priority` integer NOT NULL DEFAULT '0' + )"; + + $queries[] = "INSERT INTO `$tLayers` (`id`, `name`, `url`, `priority`) VALUES + (1, 'OpenCycleMap', 'https://{a-c}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png', 0), + (2, 'OpenTopoMap', 'https://{a-c}.tile.opentopomap.org/{z}/{x}/{y}.png', 0), + (3, 'OpenSeaMap', 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', 0), + (4, 'ESRI', 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', 0), + (5, 'UMP', 'http://{1-3}.tiles.ump.waw.pl/ump_tiles/{z}/{x}/{y}.png', 0), + (6, 'Osmapa.pl', 'http://{a-c}.tile.openstreetmap.pl/osmapa.pl/{z}/{x}/{y}.png', 0); + "; + + $queries[] = "ALTER TABLE `$tUsers` ADD `admin` boolean NOT NULL DEFAULT FALSE"; + + $queries[] = "PRAGMA foreign_keys=OFF"; + $queries[] = "CREATE TABLE `$tPositionsBackup` ( + `id` integer PRIMARY KEY AUTOINCREMENT, + `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `user_id` integer NOT NULL, + `track_id` integer NOT NULL, + `latitude` double NOT NULL, + `longitude` double NOT NULL, + `altitude` double DEFAULT NULL, + `speed` double DEFAULT NULL, + `bearing` double DEFAULT NULL, + `accuracy` integer DEFAULT NULL, + `provider` varchar(100) DEFAULT NULL, + `comment` varchar(255) DEFAULT NULL, + `image` varchar(100) DEFAULT NULL, + FOREIGN KEY(`user_id`) REFERENCES `$tUsers`(`id`), + FOREIGN KEY(`track_id`) REFERENCES `$tTracks`(`id`) + )"; + $queries[] = "INSERT INTO `$tPositionsBackup` SELECT id, time, user_id, track_id, latitude, longitude, altitude, speed, bearing, accuracy, provider, comment, NULL FROM positions"; + $queries[] = "DROP TABLE `$tPositions`"; + $queries[] = "ALTER TABLE `$tPositionsBackup` RENAME TO `$tPositions`"; + $queries[] = "CREATE INDEX `idx_ptrack_id` ON `$tPositions`(`track_id`)"; + $queries[] = "CREATE INDEX `idx_puser_id` ON `$tPositions`(`user_id`)"; + $queries[] = "PRAGMA foreign_keys=ON"; + + break; + + default: + throw new InvalidArgumentException("Driver not supported"); + } + return $queries; +} + +?> diff --git a/scripts/setup.php b/scripts/setup.php index 91a0572..2f2eafd 100644 --- a/scripts/setup.php +++ b/scripts/setup.php @@ -29,7 +29,7 @@ if (version_compare(PHP_VERSION, "5.5.0", "<")) { die("Sorry, ulogger will not work with PHP version lower than 5.5 (you have " . PHP_VERSION . ")"); } -define("ROOT_DIR", dirname(__DIR__)); +if (!defined("ROOT_DIR")) { define("ROOT_DIR", dirname(__DIR__)); } require_once(ROOT_DIR . "/helpers/db.php"); require_once(ROOT_DIR . "/helpers/config.php");