From 24ca9b1a33edf3fdc60df66d1763743c0b05607e Mon Sep 17 00:00:00 2001 From: maxpozdeev Date: Mon, 5 Dec 2022 10:45:24 +0300 Subject: [PATCH 01/23] + add custom collation to sqlite for tag name (to be utf8-case-insensitive) and task title (to sort by title) --- src/setup.php | 78 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 68 insertions(+), 10 deletions(-) diff --git a/src/setup.php b/src/setup.php index 49a3e66..7f876f4 100644 --- a/src/setup.php +++ b/src/setup.php @@ -7,7 +7,7 @@ */ // Can be used to upgrade database from myTinyTodo v1.4 or later -$lastVer = '1.7'; +$lastVer = '1.8'; if (version_compare(PHP_VERSION, '7.2.0') < 0) { die("PHP 7.2 or above is required"); @@ -85,7 +85,7 @@ if ($configExists) DBConnection::init($db); } else { - if ($ver != '1.7') { + if ($ver == '') { Config::$noDatabase = true; //will not load settings from database in init.php } require_once('./init.php'); @@ -177,7 +177,7 @@ elseif ($ver == $lastVer) } else { - if (!in_array($ver, array('1.4'))) { + if (!in_array($ver, array('1.4','1.7'))) { exitMessage(htmlspecialchars("Can not update. Unsupported database version ($ver).")); } @@ -192,9 +192,12 @@ else # update process check_post_stoken(); - if ($ver == '1.4') - { + if ($ver == '1.4') { update_14_17($db, $dbtype); + update_17_18($db, $dbtype); + } + elseif ($ver == '1.7') { + update_17_18($db, $dbtype); } } @@ -344,14 +347,14 @@ function createSqliteTables($db) $db->ex( "CREATE TABLE {$db->prefix}todolist ( id INTEGER PRIMARY KEY, - uuid CHAR(36) NOT NULL, + uuid CHAR(36) NOT NULL default '', list_id INTEGER UNSIGNED NOT NULL default 0, d_created INTEGER UNSIGNED NOT NULL default 0, d_completed INTEGER UNSIGNED NOT NULL default 0, d_edited INTEGER UNSIGNED NOT NULL default 0, compl TINYINT UNSIGNED NOT NULL default 0, - title VARCHAR(250) NOT NULL, - note TEXT, + title VARCHAR(250) NOT NULL default '' COLLATE UTF8CI, + note TEXT COLLATE UTF8CI default NULL, prio TINYINT NOT NULL default 0, ow INTEGER NOT NULL default 0, tags VARCHAR(600) NOT NULL default '', @@ -365,9 +368,9 @@ function createSqliteTables($db) $db->ex( "CREATE TABLE {$db->prefix}tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, - name VARCHAR(50) NOT NULL COLLATE NOCASE + name VARCHAR(50) NOT NULL DEFAULT '' COLLATE UTF8CI ) "); - $db->ex("CREATE UNIQUE INDEX tags_name ON {$db->prefix}tags (name COLLATE NOCASE)"); + $db->ex("CREATE INDEX tags_name ON {$db->prefix}tags (name)"); //NB: unique in mysql $db->ex( @@ -642,3 +645,58 @@ UNIQUE KEY `id` (`id`) Config::saveDbConfig(); } ### end of 1.7 ##### + +### update v1.7 to v1.8 ########## +function update_17_18(Database_Abstract $db, $dbtype) +{ + $db->ex("BEGIN"); + + if ($dbtype == 'sqlite') + { + // Use UTF8CI collate. Old sqlite does not support DROP COLUMN + $db->ex("DROP INDEX todo_list_id"); + $db->ex("DROP INDEX todo_uuid"); + $db->ex("ALTER TABLE {$db->prefix}todolist RENAME TO {$db->prefix}todolist_old"); + $db->ex( + "CREATE TABLE {$db->prefix}todolist ( + id INTEGER PRIMARY KEY, + uuid CHAR(36) NOT NULL default '', + list_id INTEGER UNSIGNED NOT NULL default 0, + d_created INTEGER UNSIGNED NOT NULL default 0, + d_completed INTEGER UNSIGNED NOT NULL default 0, + d_edited INTEGER UNSIGNED NOT NULL default 0, + compl TINYINT UNSIGNED NOT NULL default 0, + title VARCHAR(250) NOT NULL default '' COLLATE UTF8CI, + note TEXT COLLATE UTF8CI default NULL, + prio TINYINT NOT NULL default 0, + ow INTEGER NOT NULL default 0, + tags VARCHAR(600) NOT NULL default '', + tags_ids VARCHAR(250) NOT NULL default '', + duedate DATE default NULL )" + ); + $db->ex("INSERT INTO {$db->prefix}todolist SELECT * FROM {$db->prefix}todolist_old"); + $db->ex("CREATE INDEX todo_list_id ON {$db->prefix}todolist (list_id)"); + $db->ex("CREATE UNIQUE INDEX todo_uuid ON {$db->prefix}todolist (uuid)"); + $db->ex("DROP TABLE {$db->prefix}todolist_old"); + + $db->ex("DROP INDEX tags_name"); + $db->ex("ALTER TABLE {$db->prefix}tags RENAME TO {$db->prefix}tags_old"); + $db->ex( + "CREATE TABLE {$db->prefix}tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(50) NOT NULL DEFAULT '' COLLATE UTF8CI )" + ); + $db->ex("INSERT INTO {$db->prefix}tags SELECT * FROM {$db->prefix}tags_old"); + $db->ex("CREATE INDEX tags_name ON {$db->prefix}tags (name)"); + $db->ex("DROP TABLE {$db->prefix}tags_old"); + } + + $db->ex("COMMIT"); + + if ($dbtype == 'sqlite') { + $db->ex("VACUUM"); + } + + +} +### end of 1.8 ##### From 5c174fade37ebb45011f456e44ca6a634c91b814 Mon Sep 17 00:00:00 2001 From: maxpozdeev Date: Mon, 5 Dec 2022 11:40:39 +0300 Subject: [PATCH 02/23] - fix page title in setup (cherry picked from commit 79c06713cb819128bc0951535093a7b6092a88d5) --- src/setup.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/setup.php b/src/setup.php index 7f876f4..b7c72c9 100644 --- a/src/setup.php +++ b/src/setup.php @@ -27,7 +27,6 @@ require_once(MTTINC. 'class.dbconnection.php'); require_once(MTTINC. 'class.config.php'); require_once(MTTINC. 'version.php'); -$mttVersion = mytinytodo\Version::VERSION; $db = null; $ver = ''; $error = ''; @@ -42,7 +41,8 @@ $configExists = file_exists(MTTPATH. 'config.php'); $oldConfigExists = file_exists(MTTPATH. 'db/config.php'); -echo 'myTinyTodo $mttVersion Setup'; +$mttVersion = htmlspecialchars(mytinytodo\Version::VERSION); +echo "myTinyTodo $mttVersion Setup"; echo "myTinyTodo $mttVersion Setup

"; if (!$configExists && $oldConfigExists) From 47d37a46b585c2ef2cadbb535d25932a41445d4f Mon Sep 17 00:00:00 2001 From: maxpozdeev Date: Mon, 5 Dec 2022 12:41:51 +0300 Subject: [PATCH 03/23] set version to 1.8-dev --- src/includes/version.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/includes/version.php b/src/includes/version.php index 2bdceaa..a9cb109 100644 --- a/src/includes/version.php +++ b/src/includes/version.php @@ -4,6 +4,6 @@ namespace mytinytodo; class Version { - const VERSION = '1.7.3'; - const DB_VERSION = '1.7'; + const VERSION = '1.8-dev'; + const DB_VERSION = '1.8'; } From 2566881fbf911cad759424677c8ccd8903b17760 Mon Sep 17 00:00:00 2001 From: maxpozdeev Date: Wed, 7 Dec 2022 15:48:18 +0300 Subject: [PATCH 04/23] + search task in sqlite by normalized value (no diacritics) (use Normalizer from Intl extension) --- src/includes/class.db.sqlite3.php | 113 ++++++++++++++++++++++++++++-- 1 file changed, 107 insertions(+), 6 deletions(-) diff --git a/src/includes/class.db.sqlite3.php b/src/includes/class.db.sqlite3.php index 0abeaf2..348244c 100644 --- a/src/includes/class.db.sqlite3.php +++ b/src/includes/class.db.sqlite3.php @@ -62,8 +62,16 @@ class Database_Sqlite3 extends Database_Abstract /** @var int */ protected $affected = 0; - function __construct() + /** @var bool */ + protected $useNormalizedUtf8 = true; + + function __construct(array $params = null) { + if (is_array($params)) { + if (isset($params['useNormalizedUtf8'])) { + $this->useNormalizedUtf8 = boolval($params['useNormalizedUtf8']); + } + } } function connect(array $params): void @@ -73,8 +81,10 @@ class Database_Sqlite3 extends Database_Abstract PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ); $this->dbh = new PDO("sqlite:$filename", null, null, $options); //throws PDOException - $this->dbh->sqliteCreateFunction('utf8_lower', [$this, 'utf8lower'], 1); - $this->dbh->sqliteCreateCollation('UTF8CI', [$this, 'utf8ci']); + $this->dbh->sqliteCreateFunction('utf8_lower', [$this, 'utf8_lower'], 1); + $this->dbh->sqliteCreateFunction('utf8_normalized_lower', [$this, 'utf8_normalized_lower'], 1); + $this->dbh->sqliteCreateCollation('UTF8CI', [$this, 'collate_utf8ci']); + $this->dbh->sqliteCreateCollation('UTF8CI_NORMALIZED', [$this, 'collate_utf8ci_normalized']); } /* @@ -170,7 +180,10 @@ class Database_Sqlite3 extends Database_Abstract function like(string $column, string $format, string $string): string { $column = str_replace('"', '""', $column); - return 'utf8_lower("'. $column. '") LIKE '. $this->quoteForLike($format, mb_strtolower($string, 'UTF-8')); + if ($this->useNormalizedUtf8) { + return 'utf8_normalized_lower("'. $column. '") LIKE '. $this->quoteForLike($format, $this->utf8_normalized_lower($string)); + } + return 'utf8_lower("'. $column. '") LIKE '. $this->quoteForLike($format, $this->utf8_lower($string)); } function lastInsertId(?string $name = null): ?string @@ -204,14 +217,102 @@ class Database_Sqlite3 extends Database_Abstract return false; } - public function utf8lower($value) + public function utf8_lower($value): string { if (is_null($value)) return ''; return mb_strtolower((string)$value, 'UTF-8'); } - public function utf8ci(string $str1, string $str2): int + public function utf8_normalized_lower($value): string + { + if (is_null($value)) return ''; + $value = self::normalizeValue((string) $value); + return mb_strtolower($value, 'UTF-8'); + } + + public function collate_utf8ci(string $str1, string $str2): int { return strcmp(mb_strtolower($str1, 'UTF-8'), mb_strtolower($str2, 'UTF-8')); } + + public function collate_utf8ci_normalized(string $str1, string $str2): int + { + $str1 = self::normalizeValue($str1); + $str2 = self::normalizeValue($str2); + return strcmp(mb_strtolower($str1, 'UTF-8'), mb_strtolower($str2, 'UTF-8')); + } + + public static function normalizeValue(string $str): string + { + $str = Normalizer::normalize($str, Normalizer::FORM_KD); + + if (false === preg_match_all("/./u", $str, $m)) { + $ea = error_get_last(); + $error = ($ea && isset($ea['message'])) ? $ea['message'] : "preg_match_all() failed"; + throw new Exception($error); + } + $chars = $m[0]; + static $map = [ + // https://en.wikipedia.org/wiki/Combining_character + "\u{0300}" => '', "\u{0301}" => '', "\u{0302}" => '', "\u{0303}" => '', "\u{0304}" => '', "\u{0305}" => '', "\u{0306}" => '', "\u{0307}" => '', + "\u{0308}" => '', "\u{0309}" => '', "\u{030a}" => '', "\u{030b}" => '', "\u{030c}" => '', "\u{030d}" => '', "\u{030e}" => '', "\u{030f}" => '', + "\u{0310}" => '', "\u{0311}" => '', "\u{0312}" => '', "\u{0313}" => '', "\u{0314}" => '', "\u{0315}" => '', "\u{0316}" => '', "\u{0317}" => '', + "\u{0318}" => '', "\u{0319}" => '', "\u{031a}" => '', "\u{031b}" => '', "\u{031c}" => '', "\u{031d}" => '', "\u{031e}" => '', "\u{031f}" => '', + "\u{0320}" => '', "\u{0321}" => '', "\u{0322}" => '', "\u{0323}" => '', "\u{0324}" => '', "\u{0325}" => '', "\u{0326}" => '', "\u{0327}" => '', + "\u{0328}" => '', "\u{0329}" => '', "\u{032a}" => '', "\u{032b}" => '', "\u{032c}" => '', "\u{032d}" => '', "\u{032e}" => '', "\u{032f}" => '', + "\u{0330}" => '', "\u{0331}" => '', "\u{0332}" => '', "\u{0333}" => '', "\u{0334}" => '', "\u{0335}" => '', "\u{0336}" => '', "\u{0337}" => '', + "\u{0338}" => '', "\u{0339}" => '', "\u{033a}" => '', "\u{033b}" => '', "\u{033c}" => '', "\u{033d}" => '', "\u{033e}" => '', "\u{033f}" => '', + "\u{0340}" => '', "\u{0341}" => '', "\u{0342}" => '', "\u{0343}" => '', "\u{0344}" => '', "\u{0345}" => '', "\u{0346}" => '', "\u{0347}" => '', + "\u{0348}" => '', "\u{0349}" => '', "\u{034a}" => '', "\u{034b}" => '', "\u{034c}" => '', "\u{034d}" => '', "\u{034e}" => '', "\u{034f}" => '', + "\u{0350}" => '', "\u{0351}" => '', "\u{0352}" => '', "\u{0353}" => '', "\u{0354}" => '', "\u{0355}" => '', "\u{0356}" => '', "\u{0357}" => '', + "\u{0358}" => '', "\u{0359}" => '', "\u{035a}" => '', "\u{035b}" => '', "\u{035c}" => '', "\u{035d}" => '', "\u{035e}" => '', "\u{035f}" => '', + "\u{0360}" => '', "\u{0361}" => '', "\u{0362}" => '', "\u{0363}" => '', "\u{0364}" => '', "\u{0365}" => '', "\u{0366}" => '', "\u{0367}" => '', + "\u{0368}" => '', "\u{0369}" => '', "\u{036a}" => '', "\u{036b}" => '', "\u{036c}" => '', "\u{036d}" => '', "\u{036e}" => '', "\u{036f}" => '', + + "\u{1ab0}" => '', "\u{1ab1}" => '', "\u{1ab2}" => '', "\u{1ab3}" => '', "\u{1ab4}" => '', "\u{1ab5}" => '', "\u{1ab6}" => '', "\u{1ab7}" => '', + "\u{1ab8}" => '', "\u{1ab9}" => '', "\u{1aba}" => '', "\u{1abb}" => '', "\u{1abc}" => '', "\u{1abd}" => '', "\u{1abe}" => '', "\u{1abf}" => '', + "\u{1ac0}" => '', "\u{1ac1}" => '', "\u{1ac2}" => '', "\u{1ac3}" => '', "\u{1ac4}" => '', "\u{1ac5}" => '', "\u{1ac6}" => '', "\u{1ac7}" => '', + "\u{1ac8}" => '', "\u{1ac9}" => '', "\u{1aca}" => '', "\u{1acb}" => '', "\u{1acc}" => '', "\u{1acd}" => '', "\u{1ace}" => '', "\u{1acf}" => '', + "\u{1ad0}" => '', "\u{1ad1}" => '', "\u{1ad2}" => '', "\u{1ad3}" => '', "\u{1ad4}" => '', "\u{1ad5}" => '', "\u{1ad6}" => '', "\u{1ad7}" => '', + "\u{1ad8}" => '', "\u{1ad9}" => '', "\u{1ada}" => '', "\u{1adb}" => '', "\u{1adc}" => '', "\u{1add}" => '', "\u{1ade}" => '', "\u{1adf}" => '', + "\u{1ae0}" => '', "\u{1ae1}" => '', "\u{1ae2}" => '', "\u{1ae3}" => '', "\u{1ae4}" => '', "\u{1ae5}" => '', "\u{1ae6}" => '', "\u{1ae7}" => '', + "\u{1ae8}" => '', "\u{1ae9}" => '', "\u{1aea}" => '', "\u{1aeb}" => '', "\u{1aec}" => '', "\u{1aed}" => '', "\u{1aee}" => '', "\u{1aef}" => '', + "\u{1af0}" => '', "\u{1af1}" => '', "\u{1af2}" => '', "\u{1af3}" => '', "\u{1af4}" => '', "\u{1af5}" => '', "\u{1af6}" => '', "\u{1af7}" => '', + "\u{1af8}" => '', "\u{1af9}" => '', "\u{1afa}" => '', "\u{1afb}" => '', "\u{1afc}" => '', "\u{1afd}" => '', "\u{1afe}" => '', "\u{1aff}" => '', + + "\u{1dc0}" => '', "\u{1dc1}" => '', "\u{1dc2}" => '', "\u{1dc3}" => '', "\u{1dc4}" => '', "\u{1dc5}" => '', "\u{1dc6}" => '', "\u{1dc7}" => '', + "\u{1dc8}" => '', "\u{1dc9}" => '', "\u{1dca}" => '', "\u{1dcb}" => '', "\u{1dcc}" => '', "\u{1dcd}" => '', "\u{1dce}" => '', "\u{1dcf}" => '', + "\u{1dd0}" => '', "\u{1dd1}" => '', "\u{1dd2}" => '', "\u{1dd3}" => '', "\u{1dd4}" => '', "\u{1dd5}" => '', "\u{1dd6}" => '', "\u{1dd7}" => '', + "\u{1dd8}" => '', "\u{1dd9}" => '', "\u{1dda}" => '', "\u{1ddb}" => '', "\u{1ddc}" => '', "\u{1ddd}" => '', "\u{1dde}" => '', "\u{1ddf}" => '', + "\u{1de0}" => '', "\u{1de1}" => '', "\u{1de2}" => '', "\u{1de3}" => '', "\u{1de4}" => '', "\u{1de5}" => '', "\u{1de6}" => '', "\u{1de7}" => '', + "\u{1de8}" => '', "\u{1de9}" => '', "\u{1dea}" => '', "\u{1deb}" => '', "\u{1dec}" => '', "\u{1ded}" => '', "\u{1dee}" => '', "\u{1def}" => '', + "\u{1df0}" => '', "\u{1df1}" => '', "\u{1df2}" => '', "\u{1df3}" => '', "\u{1df4}" => '', "\u{1df5}" => '', "\u{1df6}" => '', "\u{1df7}" => '', + "\u{1df8}" => '', "\u{1df9}" => '', "\u{1dfa}" => '', "\u{1dfb}" => '', "\u{1dfc}" => '', "\u{1dfd}" => '', "\u{1dfe}" => '', "\u{1dff}" => '', + + "\u{20d0}" => '', "\u{20d1}" => '', "\u{20d2}" => '', "\u{20d3}" => '', "\u{20d4}" => '', "\u{20d5}" => '', "\u{20d6}" => '', "\u{20d7}" => '', + "\u{20d8}" => '', "\u{20d9}" => '', "\u{20da}" => '', "\u{20db}" => '', "\u{20dc}" => '', "\u{20dd}" => '', "\u{20de}" => '', "\u{20df}" => '', + "\u{20e0}" => '', "\u{20e1}" => '', "\u{20e2}" => '', "\u{20e3}" => '', "\u{20e4}" => '', "\u{20e5}" => '', "\u{20e6}" => '', "\u{20e7}" => '', + "\u{20e8}" => '', "\u{20e9}" => '', "\u{20ea}" => '', "\u{20eb}" => '', "\u{20ec}" => '', "\u{20ed}" => '', "\u{20ee}" => '', "\u{20ef}" => '', + "\u{20f0}" => '', "\u{20f1}" => '', "\u{20f2}" => '', "\u{20f3}" => '', "\u{20f4}" => '', "\u{20f5}" => '', "\u{20f6}" => '', "\u{20f7}" => '', + "\u{20f8}" => '', "\u{20f9}" => '', "\u{20fa}" => '', "\u{20fb}" => '', "\u{20fc}" => '', "\u{20fd}" => '', "\u{20fe}" => '', "\u{20ff}" => '', + + "\u{fe20}" => '', "\u{fe21}" => '', "\u{fe22}" => '', "\u{fe23}" => '', "\u{fe24}" => '', "\u{fe25}" => '', "\u{fe26}" => '', "\u{fe27}" => '', + "\u{fe28}" => '', "\u{fe29}" => '', "\u{fe2a}" => '', "\u{fe2b}" => '', "\u{fe2c}" => '', "\u{fe2d}" => '', "\u{fe2e}" => '', "\u{fe2f}" => '', + + 'Æ' => 'AE', // "U+00c6" + 'æ' => 'ae', // "U+00e6" + 'Œ' => 'OE', // "U+0152" + 'œ' => 'oe', // "U+0153" + ]; + + $len = count($chars); + for ($i = 0; $i < $len; $i++) { + $unichar = $chars[$i]; + if (isset($map[$unichar])) { + $chars[$i] = $map[$unichar]; + } + } + return implode('', $chars); + } + } From 7ef88217cbcbbc02af78c9a3c23ac77efcb52786 Mon Sep 17 00:00:00 2001 From: maxpozdeev Date: Wed, 7 Dec 2022 15:53:47 +0300 Subject: [PATCH 05/23] add Normalizer polyfill by Symfony --- composer.json | 3 +- composer.lock | 180 ++++++++++++++++++++++++++++++++++++-------------- src/init.php | 11 +-- 3 files changed, 141 insertions(+), 53 deletions(-) diff --git a/composer.json b/composer.json index 83c8724..b4e1580 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,8 @@ "vendor-dir": "src/includes/vendor" }, "require": { - "erusev/parsedown": "^1.7" + "erusev/parsedown": "^1.7", + "symfony/polyfill-intl-normalizer": "^1.27" }, "require-dev": { "league/commonmark": "^2.3" diff --git a/composer.lock b/composer.lock index 5ff3829..b36dc60 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "18661f8c2155d955e7962271812bbed0", + "content-hash": "cb6818ea27ab475ab65854fa66fd68bd", "packages": [ { "name": "erusev/parsedown", @@ -55,21 +55,105 @@ "source": "https://github.com/erusev/parsedown/tree/1.7.x" }, "time": "2019-12-30T22:54:17+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" } ], "packages-dev": [ { "name": "dflydev/dot-access-data", - "version": "v3.0.1", + "version": "v3.0.2", "source": { "type": "git", "url": "https://github.com/dflydev/dflydev-dot-access-data.git", - "reference": "0992cc19268b259a39e86f296da5f0677841f42c" + "reference": "f41715465d65213d644d3141a6a93081be5d3549" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/0992cc19268b259a39e86f296da5f0677841f42c", - "reference": "0992cc19268b259a39e86f296da5f0677841f42c", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/f41715465d65213d644d3141a6a93081be5d3549", + "reference": "f41715465d65213d644d3141a6a93081be5d3549", "shasum": "" }, "require": { @@ -80,7 +164,7 @@ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", "scrutinizer/ocular": "1.6.0", "squizlabs/php_codesniffer": "^3.5", - "vimeo/psalm": "^3.14" + "vimeo/psalm": "^4.0.0" }, "type": "library", "extra": { @@ -129,22 +213,22 @@ ], "support": { "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", - "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.1" + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.2" }, - "time": "2021-08-13T13:06:58+00:00" + "time": "2022-10-27T11:44:00+00:00" }, { "name": "league/commonmark", - "version": "2.3.4", + "version": "2.3.7", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "155ec1c95626b16fda0889cf15904d24890a60d5" + "reference": "a36bd2be4f5387c0f3a8792a0d76b7d68865abbf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/155ec1c95626b16fda0889cf15904d24890a60d5", - "reference": "155ec1c95626b16fda0889cf15904d24890a60d5", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/a36bd2be4f5387c0f3a8792a0d76b7d68865abbf", + "reference": "a36bd2be4f5387c0f3a8792a0d76b7d68865abbf", "shasum": "" }, "require": { @@ -164,15 +248,15 @@ "erusev/parsedown": "^1.0", "ext-json": "*", "github/gfm": "0.29.0", - "michelf/php-markdown": "^1.4", + "michelf/php-markdown": "^1.4 || ^2.0", "nyholm/psr7": "^1.5", - "phpstan/phpstan": "^0.12.88 || ^1.0.0", - "phpunit/phpunit": "^9.5.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21", "scrutinizer/ocular": "^1.8.1", - "symfony/finder": "^5.3", + "symfony/finder": "^5.3 | ^6.0", "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0", - "unleashedtech/php-coding-standard": "^3.1", - "vimeo/psalm": "^4.7.3" + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0" }, "suggest": { "symfony/yaml": "v2.3+ required if using the Front Matter extension" @@ -237,7 +321,7 @@ "type": "tidelift" } ], - "time": "2022-07-17T16:25:47+00:00" + "time": "2022-11-03T17:29:46+00:00" }, { "name": "league/config", @@ -323,25 +407,25 @@ }, { "name": "nette/schema", - "version": "v1.2.2", + "version": "v1.2.3", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "9a39cef03a5b34c7de64f551538cbba05c2be5df" + "reference": "abbdbb70e0245d5f3bf77874cea1dfb0c930d06f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/9a39cef03a5b34c7de64f551538cbba05c2be5df", - "reference": "9a39cef03a5b34c7de64f551538cbba05c2be5df", + "url": "https://api.github.com/repos/nette/schema/zipball/abbdbb70e0245d5f3bf77874cea1dfb0c930d06f", + "reference": "abbdbb70e0245d5f3bf77874cea1dfb0c930d06f", "shasum": "" }, "require": { "nette/utils": "^2.5.7 || ^3.1.5 || ^4.0", - "php": ">=7.1 <8.2" + "php": ">=7.1 <8.3" }, "require-dev": { "nette/tester": "^2.3 || ^2.4", - "phpstan/phpstan-nette": "^0.12", + "phpstan/phpstan-nette": "^1.0", "tracy/tracy": "^2.7" }, "type": "library", @@ -379,26 +463,26 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.2.2" + "source": "https://github.com/nette/schema/tree/v1.2.3" }, - "time": "2021-10-15T11:40:02+00:00" + "time": "2022-10-13T01:24:26+00:00" }, { "name": "nette/utils", - "version": "v3.2.7", + "version": "v3.2.8", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "0af4e3de4df9f1543534beab255ccf459e7a2c99" + "reference": "02a54c4c872b99e4ec05c4aec54b5a06eb0f6368" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/0af4e3de4df9f1543534beab255ccf459e7a2c99", - "reference": "0af4e3de4df9f1543534beab255ccf459e7a2c99", + "url": "https://api.github.com/repos/nette/utils/zipball/02a54c4c872b99e4ec05c4aec54b5a06eb0f6368", + "reference": "02a54c4c872b99e4ec05c4aec54b5a06eb0f6368", "shasum": "" }, "require": { - "php": ">=7.2 <8.2" + "php": ">=7.2 <8.3" }, "conflict": { "nette/di": "<3.0.6" @@ -464,9 +548,9 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v3.2.7" + "source": "https://github.com/nette/utils/tree/v3.2.8" }, - "time": "2022-01-24T11:29:14+00:00" + "time": "2022-09-12T23:36:20+00:00" }, { "name": "psr/event-dispatcher", @@ -520,16 +604,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.1.1", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918" + "reference": "1ee04c65529dea5d8744774d474e7cbd2f1206d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918", - "reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/1ee04c65529dea5d8744774d474e7cbd2f1206d3", + "reference": "1ee04c65529dea5d8744774d474e7cbd2f1206d3", "shasum": "" }, "require": { @@ -538,7 +622,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.1-dev" + "dev-main": "3.3-dev" }, "thanks": { "name": "symfony/contracts", @@ -567,7 +651,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.1.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.2.0" }, "funding": [ { @@ -583,20 +667,20 @@ "type": "tidelift" } ], - "time": "2022-02-25T11:15:52+00:00" + "time": "2022-11-25T10:21:52+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace" + "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace", - "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", "shasum": "" }, "require": { @@ -605,7 +689,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -650,7 +734,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" }, "funding": [ { @@ -666,7 +750,7 @@ "type": "tidelift" } ], - "time": "2022-05-10T07:21:04+00:00" + "time": "2022-11-03T14:55:06+00:00" } ], "aliases": [], diff --git a/src/init.php b/src/init.php index 372c919..1d9bac1 100644 --- a/src/init.php +++ b/src/init.php @@ -103,12 +103,12 @@ function configureDbConnection() } DBConnection::init($db); try { - $db->connect( array( + $db->connect([ 'host' => MTT_DB_HOST, 'user' => MTT_DB_USER, 'password' => MTT_DB_PASSWORD, 'db' => MTT_DB_NAME, - )); + ]); } catch(Exception $e) { logAndDie("Failed to connect to mysql database: ". $e->getMessage()); @@ -119,9 +119,12 @@ function configureDbConnection() # SQLite3 Database elseif (MTT_DB_TYPE == 'sqlite') { + require_once(MTTINC. 'vendor/autoload.php'); require_once(MTTINC. 'class.db.sqlite3.php'); - $db = DBConnection::init(new Database_Sqlite3); - $db->connect( array( 'filename' => MTTPATH. 'db/todolist.db' ) ); + $db = DBConnection::init(new Database_Sqlite3()); + $db->connect([ + 'filename' => MTTPATH. 'db/todolist.db' + ]); } else { die("Incorrect database connection config"); From 0990900da26879307d104fa4b8ad25fee8261c9d Mon Sep 17 00:00:00 2001 From: maxpozdeev Date: Wed, 14 Dec 2022 00:29:57 +0300 Subject: [PATCH 06/23] liitle optiomization of sql to get tasks --- src/includes/api/TasksController.php | 38 +++++++++++++++------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/includes/api/TasksController.php b/src/includes/api/TasksController.php index cf8fded..0e726b9 100644 --- a/src/includes/api/TasksController.php +++ b/src/includes/api/TasksController.php @@ -22,14 +22,15 @@ class TasksController extends ApiController { checkReadAccess($listId); $db = DBConnection::instance(); - $sqlWhere = $inner = $sqlWhereListId = $sqlInnerWhereListId = ''; + $sqlWhere = $sqlWhereListId = $sqlInnerWhereListId = ''; if ($listId == -1) { $userLists = $this->getUserListsSimple(); - $sqlWhereListId = "{$db->prefix}todolist.list_id IN (". implode(',', array_keys($userLists)). ") "; - $sqlInnerWhereListId = "list_id IN (". implode(',', array_keys($userLists)). ") "; + $userListsIds = implode(',', array_keys($userLists)); + $sqlWhereListId = "todo.list_id IN ($userListsIds) "; + $sqlInnerWhereListId = "list_id IN ($userListsIds) "; } else { - $sqlWhereListId = "{$db->prefix}todolist.list_id=". $listId; + $sqlWhereListId = "todo.list_id=". $listId; $sqlInnerWhereListId = "list_id=$listId "; } if (_get('compl') == 0) { @@ -52,20 +53,18 @@ class TasksController extends ApiController { } } - // Include tags: All - if (sizeof($tagIds) > 1) { - $inner .= "INNER JOIN (SELECT task_id, COUNT(tag_id) AS c FROM {$db->prefix}tag2task WHERE $sqlInnerWhereListId AND tag_id IN (". - implode(',',$tagIds). ") GROUP BY task_id) AS t2t ON id=t2t.task_id"; - $sqlWhere .= " AND c=". sizeof($tagIds); - } - elseif ($tagIds) { - $inner .= "INNER JOIN {$db->prefix}tag2task ON id=task_id"; - $sqlWhere .= " AND tag_id = {$tagIds[0]}"; + // Include tags + if ($tagIds) { + $sqlWhere .= " AND todo.id IN (SELECT task_id FROM {$db->prefix}tag2task WHERE tag_id IN (". implode(',',$tagIds). ")"; + if (count($tagIds) > 1) { + $sqlWhere .= " GROUP BY task_id HAVING COUNT(tag_id)=". count($tagIds); + } + $sqlWhere .= ")"; } // Exclude tags - if (sizeof($tagExIds) > 0) { - $sqlWhere .= " AND {$db->prefix}todolist.id NOT IN (SELECT DISTINCT task_id FROM {$db->prefix}tag2task WHERE $sqlInnerWhereListId AND tag_id IN (". + if (count($tagExIds) > 0) { + $sqlWhere .= " AND todo.id NOT IN (SELECT DISTINCT task_id FROM {$db->prefix}tag2task WHERE $sqlInnerWhereListId AND tag_id IN (". implode(',',$tagExIds). "))"; } //no optimization for single exTag @@ -74,7 +73,7 @@ class TasksController extends ApiController { $s = trim(_get('s')); if ($s != '') { if (preg_match("|^#(\d+)$|", $s, $m)) { - $sqlWhere .= " AND {$db->prefix}todolist.id = ". (int)$m[1]; + $sqlWhere .= " AND todo.id = ". (int)$m[1]; } else { $sqlWhere .= " AND (". $db->like("title", "%%%s%%", $s). " OR ". $db->like("note", "%%%s%%", $s). ")"; @@ -98,7 +97,12 @@ class TasksController extends ApiController { $t = array(); $t['total'] = 0; $t['list'] = array(); - $q = $db->dq("SELECT *, duedate IS NULL AS ddn FROM {$db->prefix}todolist $inner WHERE $sqlWhereListId $sqlWhere $sqlSort"); + + $q = $db->dq("SELECT todo.*, duedate IS NULL AS ddn + FROM {$db->prefix}todolist AS todo + WHERE $sqlWhereListId $sqlWhere + GROUP BY todo.id $sqlSort"); + while ($r = $q->fetchAssoc()) { $t['total']++; From 8da989ed5132bc5aa618db194798a0dc7640f10c Mon Sep 17 00:00:00 2001 From: maxpozdeev Date: Mon, 12 Dec 2022 11:03:30 +0300 Subject: [PATCH 07/23] update dev-docker with php 8.2 to use release version of php (cherry picked from commit f0d0adf493cdeefb9358a44534c541dd2b673fb0) --- docker/dev/mtt-php82-apache/Dockerfile-web | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/dev/mtt-php82-apache/Dockerfile-web b/docker/dev/mtt-php82-apache/Dockerfile-web index 1811e3d..f23d10d 100644 --- a/docker/dev/mtt-php82-apache/Dockerfile-web +++ b/docker/dev/mtt-php82-apache/Dockerfile-web @@ -1,4 +1,4 @@ -FROM php:8.2-rc-apache +FROM php:8.2-apache RUN docker-php-ext-install mysqli && \ mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" && \ From 3a68339cb90b04f640bc9f0a56d2ada0a410ad79 Mon Sep 17 00:00:00 2001 From: maxpozdeev Date: Thu, 23 Mar 2023 22:28:00 +0300 Subject: [PATCH 08/23] - fix deprecation notices in php 8.2 (cherry picked from commit 8833e7c417fc266e47ed384ab0e50745cf966734) --- src/export.php | 12 ++++++------ src/feed.php | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/export.php b/src/export.php index 8a3b9bf..4ff7d04 100644 --- a/src/export.php +++ b/src/export.php @@ -42,15 +42,15 @@ if($format == 'ical') printICal($listData, $data); else printCSV($listData, $data); -function printCSV($listData, $data) +function printCSV(array $listData, array $data) { $s = "\xEF\xBB\xBF". "Completed;Priority;Task;Notes;Tags;Due;DateCreated;DateCompleted\n"; foreach($data as $r) { $s .= ($r['compl']?'1':'0'). ';'. - $r['prio']. ';'. escape_csv($r['title']). ';'. - escape_csv($r['note']). ';'. - escape_csv($r['tags']). ';'. + $r['prio']. ';'. escape_csv($r['title'] ?? ''). ';'. + escape_csv($r['note'] ?? ''). ';'. + escape_csv($r['tags'] ?? ''). ';'. $r['duedate']. ';'. date('Y-m-d H:i:s O',$r['d_created']). ';'. ($r['d_completed'] ? date('Y-m-d H:i:s O',$r['d_completed']) :''). "\n"; @@ -60,7 +60,7 @@ function printCSV($listData, $data) print $s; } -function escape_csv($v) +function escape_csv(string $v) { //escape formulas $nf = ''; @@ -71,7 +71,7 @@ function escape_csv($v) return '"'. $nf. str_replace('"', '""', $v). '"'; } -function printICal($listData, $data) +function printICal(array $listData, array $data) { $mttToIcalPrio = array("1" => 5, "2" => 1); $s = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nMETHOD:PUBLISH\r\nCALSCALE:GREGORIAN\r\nPRODID:-//myTinyTodo//iCalendar Export v1.4//EN\r\n". diff --git a/src/feed.php b/src/feed.php index 2f9d896..0475fb1 100644 --- a/src/feed.php +++ b/src/feed.php @@ -62,7 +62,7 @@ htmlarray_ref($listData); printRss($data, $listData); -function fillData(&$data, $listId, $field, $sqlWhere ) +function fillData(array &$data, int $listId, string $field, string $sqlWhere ) { $lang = Lang::instance(); $db = DBConnection::instance(); @@ -97,7 +97,7 @@ function fillData(&$data, $listId, $field, $sqlWhere ) } } -function printRss($data, $listData) +function printRss(array $data, array $listData) { $lang = Lang::instance(); $link = get_mttinfo('url'). "?list=". (int)$listData['id']; @@ -108,7 +108,7 @@ function printRss($data, $listData) "\n". "$listData[_feed_title]\n". "$link\n". - "\n". + "\n". "$listData[_feed_descr]\n". "$buildDate\n\n"; @@ -131,13 +131,13 @@ function printRss($data, $listData) } if ( $status !='' ) $status = "[$status] "; - $s .= "\n". - "". $status. $v['title']. "\n". - "". $itemLink. "\n". - "". $v['_d']. "\n". - "\n". - "$guid\n". - "\n\n"; + $s .= "\t\n". + "\t\t". $status. $v['title']. "\n". + "\t\t". $itemLink. "\n". + "\t\t". $v['_d']. "\n". + "\t\t\n". + "\t\t$guid\n". + "\t\n\n"; } $s .= "\n"; From 88155698615f7b25a84008d01a5b94936b43550f Mon Sep 17 00:00:00 2001 From: maxpozdeev Date: Fri, 24 Mar 2023 18:19:21 +0300 Subject: [PATCH 09/23] ** remove "tags" and "tags_ids" fields from tasks table (used for better performance) --- src/export.php | 20 +++---- src/feed.php | 15 +++--- src/includes/api/TasksController.php | 68 +++++++++++++----------- src/includes/class.dbcore.php | 79 ++++++++++++++++++++++++++++ src/setup.php | 28 +++++----- 5 files changed, 146 insertions(+), 64 deletions(-) diff --git a/src/export.php b/src/export.php index 4ff7d04..091439f 100644 --- a/src/export.php +++ b/src/export.php @@ -24,22 +24,14 @@ if (!$listData) { die("No list found."); } -$sqlSort = "ORDER BY compl ASC, "; -if($listData['sorting'] == 1) $sqlSort .= "prio DESC, ddn ASC, duedate ASC, ow ASC"; -elseif($listData['sorting'] == 2) $sqlSort .= "ddn ASC, duedate ASC, prio DESC, ow ASC"; -else $sqlSort .= "ow ASC"; +$data = DBCore::defaultInstance()->getTasksByListId($listId, '', (int)$listData['sorting']); -$data = array(); -$q = $db->dq("SELECT *, duedate IS NULL AS ddn FROM {$db->prefix}todolist WHERE list_id=$listId $sqlSort"); -while($r = $q->fetchAssoc()) -{ - $data[] = $r; +if (_get('format') == 'ical') { + printICal($listData, $data); +} +else { + printCSV($listData, $data); } - -$format = _get('format'); - -if($format == 'ical') printICal($listData, $data); -else printCSV($listData, $data); function printCSV(array $listData, array $data) diff --git a/src/feed.php b/src/feed.php index 0475fb1..ebe2d61 100644 --- a/src/feed.php +++ b/src/feed.php @@ -32,7 +32,7 @@ $feedType = _get('feed'); if($feedType == 'completed') { $listData['_feed_descr'] = $lang->get('feed_completed_tasks'); - fillData( $data, $listId, 'd_completed', 'AND compl=1' ); + fillData( $data, $listId, 'd_completed', 'compl=1' ); } elseif($feedType == 'modified') { $listData['_feed_descr'] = $lang->get('feed_modified_tasks'); @@ -40,13 +40,13 @@ elseif($feedType == 'modified') { } elseif($feedType == 'current') { $listData['_feed_descr'] = $lang->get('feed_new_tasks'); - fillData( $data, $listId, 'd_created', 'AND compl=0' ); + fillData( $data, $listId, 'd_created', 'compl=0' ); } elseif($feedType == 'status') { $listData['_feed_descr'] = $lang->get('feed_tasks'); fillData( $data, $listId, 'd_created', '' ); - fillData( $data, $listId, 'd_edited', 'AND compl=0 AND d_edited > d_created' ); - fillData( $data, $listId, 'd_completed', 'AND compl=1' ); + fillData( $data, $listId, 'd_edited', 'compl=0 AND d_edited > d_created' ); + fillData( $data, $listId, 'd_completed', 'compl=1' ); } else { $listData['_feed_descr'] = $lang->get('feed_new_tasks'); @@ -64,15 +64,14 @@ printRss($data, $listData); function fillData(array &$data, int $listId, string $field, string $sqlWhere ) { + $tasks = DBCore::defaultInstance()->getTasksByListId($listId, $sqlWhere, "$field DESC", 100); $lang = Lang::instance(); - $db = DBConnection::instance(); - $q = $db->dq("SELECT * FROM {$db->prefix}todolist WHERE list_id=$listId $sqlWhere ORDER BY $field DESC LIMIT 100"); - while ($r = $q->fetchAssoc()) + foreach ($tasks as $r) { if ($r['prio'] > 0) { $r['prio'] = '+'.$r['prio']; } - $a = array(); + $a = array(); //for _descr $a[] = $lang->get('task'). ": ". $r['title']; if ($r['prio']) { $a[] = $lang->get('priority'). ": $r[prio]"; diff --git a/src/includes/api/TasksController.php b/src/includes/api/TasksController.php index 0e726b9..9440e42 100644 --- a/src/includes/api/TasksController.php +++ b/src/includes/api/TasksController.php @@ -98,10 +98,14 @@ class TasksController extends ApiController { $t['total'] = 0; $t['list'] = array(); - $q = $db->dq("SELECT todo.*, duedate IS NULL AS ddn + $q = $db->dq(" + SELECT todo.*, todo.duedate IS NULL AS ddn, GROUP_CONCAT(tags.id) AS tags_ids, GROUP_CONCAT(tags.name) AS tags FROM {$db->prefix}todolist AS todo + LEFT JOIN {$db->prefix}tag2task AS t2t ON todo.id = t2t.task_id + LEFT JOIN {$db->prefix}tags AS tags ON t2t.tag_id = tags.id WHERE $sqlWhereListId $sqlWhere - GROUP BY todo.id $sqlSort"); + GROUP BY todo.id $sqlSort + "); while ($r = $q->fetchAssoc()) { @@ -187,6 +191,11 @@ class TasksController extends ApiController { checkWriteAccess(); $id = (int)$id; + if (!DBCore::defaultInstance()->taskExists($id)) { + $this->response->data = ['total' => 0]; + return; + } + $action = $this->req->jsonBody['action'] ?? ''; switch ($action) { case 'edit': $this->response->data = $this->editTask($id); break; @@ -258,14 +267,12 @@ class TasksController extends ApiController { $aTags = $this->prepareTags($tags); if ($aTags) { $this->addTaskTags($id, $aTags['ids'], $listId); - $db->ex("UPDATE {$db->prefix}todolist SET tags=?,tags_ids=? WHERE id=$id", array(implode(',',$aTags['tags']), implode(',',$aTags['ids']))); } } $db->ex("COMMIT"); - $r = $db->sqa("SELECT * FROM {$db->prefix}todolist WHERE id=$id"); - $oo = $this->prepareTaskRow($r); - MTTNotificationCenter::postNotification(MTTNotification::didCreateTask, $oo); - $t['list'][] = $oo; + $task = $this->getTaskRowById($id); + MTTNotificationCenter::postNotification(MTTNotification::didCreateTask, $task); + $t['list'][] = $task; $t['total'] = 1; return $t; } @@ -298,14 +305,12 @@ class TasksController extends ApiController { $aTags = $this->prepareTags($tags); if ($aTags) { $this->addTaskTags($id, $aTags['ids'], $listId); - $db->ex("UPDATE {$db->prefix}todolist SET tags=?,tags_ids=? WHERE id=$id", array(implode(',',$aTags['tags']), implode(',',$aTags['ids']))); } } $db->ex("COMMIT"); - $r = $db->sqa("SELECT * FROM {$db->prefix}todolist WHERE id=$id"); - $oo = $this->prepareTaskRow($r); - MTTNotificationCenter::postNotification(MTTNotification::didCreateTask, $oo); - $t['list'][] = $oo; + $task = $this->getTaskRowById($id); + MTTNotificationCenter::postNotification(MTTNotification::didCreateTask, $task); + $t['list'][] = $task; $t['total'] = 1; return $t; } @@ -329,31 +334,26 @@ class TasksController extends ApiController { $db->ex("BEGIN"); $db->ex("DELETE FROM {$db->prefix}tag2task WHERE task_id=$id"); $aTags = $this->prepareTags($tags); - if($aTags) { - $tags = implode(',', $aTags['tags']); - $tags_ids = implode(',',$aTags['ids']); + if ($aTags) { $this->addTaskTags($id, $aTags['ids'], $listId); } - $db->dq("UPDATE {$db->prefix}todolist SET title=?,note=?,prio=?,tags=?,tags_ids=?,duedate=?,d_edited=? WHERE id=$id", - array($title, $note, $prio, $tags, $tags_ids, $duedate, time()) ); + $db->dq("UPDATE {$db->prefix}todolist SET title=?,note=?,prio=?,duedate=?,d_edited=? WHERE id=$id", + array($title, $note, $prio, $duedate, time()) ); $db->ex("COMMIT"); - $r = $db->sqa("SELECT * FROM {$db->prefix}todolist WHERE id=$id"); - if ($r) { - $t['list'][] = $this->prepareTaskRow($r); - $t['total'] = 1; - } + $task = $this->getTaskRowById($id); + $t['list'][] = $task; + $t['total'] = 1; return $t; } private function moveTask(int $id): ?array { - $db = DBConnection::instance(); $fromId = (int)($this->req->jsonBody['from'] ?? 0); $toId = (int)($this->req->jsonBody['to'] ?? 0); $result = $this->doMoveTask($id, $toId); $t = array('total' => $result ? 1 : 0); - if ($fromId == -1 && $result && $r = $db->sqa("SELECT * FROM {$db->prefix}todolist WHERE id=$id")) { - $t['list'][] = $this->prepareTaskRow($r); + if ($fromId == -1 && $result) { + $t['list'][] = $this->getTaskRowById($id); } return $t; } @@ -392,8 +392,7 @@ class TasksController extends ApiController { array($dateCompleted, $date) ); $t = array(); $t['total'] = 1; - $r = $db->sqa("SELECT * FROM {$db->prefix}todolist WHERE id=$id"); - $t['list'][] = $this->prepareTaskRow($r); + $t['list'][] = $this->getTaskRowById($id); return $t; } @@ -460,6 +459,15 @@ class TasksController extends ApiController { return $a; } + private function getTaskRowById(int $id): ?array + { + $r = DBCore::defaultInstance()->getTaskById($id); + if (!$r) { + throw new Exception("Failed to fetch task data"); + } + return $this->prepareTaskRow($r); + } + private function prepareTaskRow(array $r): array { $lang = Lang::instance(); @@ -487,7 +495,7 @@ class TasksController extends ApiController { 'date' => htmlarray($dCreated), 'dateInt' => (int)$r['d_created'], 'dateFull' => htmlarray($dCreatedFull), - 'dateInlineTitle' => htmlarray(sprintf($lang->get('taskdate_inline_created'), $dCreated)), + 'dateInlineTitle' => htmlarray(sprintf($lang->get('taskdate_inline_created'), $dCreated)), //TODO: move preparing of *inlineTitle to js 'dateEdited' => htmlarray($dEdited), 'dateEditedInt' => (int)$r['d_edited'], 'dateEditedFull' => htmlarray($dEditedFull), @@ -501,8 +509,8 @@ class TasksController extends ApiController { 'note' => noteMarkup($r['note']), 'noteText' => (string)$r['note'], 'ow' => (int)$r['ow'], - 'tags' => htmlarray($r['tags']), - 'tags_ids' => htmlarray($r['tags_ids']), + 'tags' => htmlarray($r['tags'] ?? ''), + 'tags_ids' => htmlarray($r['tags_ids'] ?? ''), 'duedate' => htmlarray($dueA['formatted']), 'dueClass' => $dueA['class'], 'dueStr' => htmlarray($dueA['str']), diff --git a/src/includes/class.dbcore.php b/src/includes/class.dbcore.php index 8aaa47f..ef7581e 100644 --- a/src/includes/class.dbcore.php +++ b/src/includes/class.dbcore.php @@ -74,5 +74,84 @@ class DBCore return $listId; } + + public function taskExists(int $id): bool + { + $db = $this->db; + $count = (int) $db->sq("SELECT COUNT(*) FROM {$db->prefix}todolist WHERE id = $id"); + return ($count > 0) ? true : false; + } + + + public function getTaskById(int $id): ?array + { + $db = $this->db; + $r = $db->sqa(" + SELECT todo.*, GROUP_CONCAT(tags.id) AS tags_ids, GROUP_CONCAT(tags.name) AS tags + FROM {$db->prefix}todolist AS todo + LEFT JOIN {$db->prefix}tag2task AS t2t ON todo.id = t2t.task_id + LEFT JOIN {$db->prefix}tags AS tags ON t2t.tag_id = tags.id + WHERE todo.id = $id + "); + return $r; + } + + /** + * + * @param int $listId + * @param string $sqlWhere + * @param int|string $sort + * @param null|int $limit + * @return array + */ + public function getTasksByListId(int $listId, string $sqlWhere, /* int|string */ $sort, ?int $limit = null): array + { + $db = $this->db; + + if ($sqlWhere != '') { + $sqlWhere = "AND $sqlWhere"; + } + + $sqlSort = ''; + if (is_int($sort)) { + $sqlSort = "ORDER BY compl ASC, "; + if ($sort == 1) $sqlSort .= "prio DESC, ddn ASC, duedate ASC, ow ASC"; // byPrio + elseif ($sort == 101) $sqlSort .= "prio ASC, ddn DESC, duedate DESC, ow DESC"; // byPrio (reverse) + elseif ($sort == 2) $sqlSort .= "ddn ASC, duedate ASC, prio DESC, ow ASC"; // byDueDate + elseif ($sort == 102) $sqlSort .= "ddn DESC, duedate DESC, prio ASC, ow DESC"; // byDueDate (reverse) + elseif ($sort == 3) $sqlSort .= "d_created ASC, prio DESC, ow ASC"; // byDateCreated + elseif ($sort == 103) $sqlSort .= "d_created DESC, prio ASC, ow DESC"; // byDateCreated (reverse) + elseif ($sort == 4) $sqlSort .= "d_edited ASC, prio DESC, ow ASC"; // byDateModified + elseif ($sort == 104) $sqlSort .= "d_edited DESC, prio ASC, ow DESC"; // byDateModified (reverse) + elseif ($sort == 5) $sqlSort .= "title ASC, prio DESC, ow ASC"; // byTitle + elseif ($sort == 105) $sqlSort .= "title DESC, prio ASC, ow DESC"; // byTitle (reverse) + else $sqlSort .= "ow ASC"; + } + else if ($sort != '') { + $sqlSort = "ORDER BY $sort"; + } + + $sqlLimit = ''; + if (!is_null($limit)) { + $sqlLimit = "LIMIT $limit"; + } + + $q = $db->dq(" + SELECT todo.*, todo.duedate IS NULL AS ddn, GROUP_CONCAT(tags.id) AS tags_ids, GROUP_CONCAT(tags.name) AS tags + FROM {$db->prefix}todolist AS todo + LEFT JOIN {$db->prefix}tag2task AS t2t ON todo.id = t2t.task_id + LEFT JOIN {$db->prefix}tags AS tags ON t2t.tag_id = tags.id + WHERE todo.list_id = $listId $sqlWhere + GROUP BY todo.id + $sqlSort + $sqlLimit + "); + + $data = array(); + while ($r = $q->fetchAssoc()) { + $data[] = $r; + } + return $data; + } } diff --git a/src/setup.php b/src/setup.php index b7c72c9..51a854b 100644 --- a/src/setup.php +++ b/src/setup.php @@ -278,8 +278,6 @@ function createMysqlTables($db) `note` TEXT, `prio` TINYINT NOT NULL default 0, /* priority -,0,+ */ `ow` INT NOT NULL default 0, /* order weight */ - `tags` VARCHAR(600) NOT NULL default '', /* for fast access to task tags */ - `tags_ids` VARCHAR(250) NOT NULL default '', /* no more than 22 tags (x11 chars) */ `duedate` DATE default NULL, PRIMARY KEY(`id`), KEY(`list_id`), @@ -290,7 +288,7 @@ function createMysqlTables($db) $db->ex( "CREATE TABLE {$db->prefix}tags ( `id` INT UNSIGNED NOT NULL auto_increment, - `name` VARCHAR(50) NOT NULL, + `name` VARCHAR(250) NOT NULL default '', PRIMARY KEY(`id`), UNIQUE KEY `name` (`name`) ) CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci "); @@ -333,7 +331,7 @@ function createSqliteTables($db) id INTEGER PRIMARY KEY, uuid CHAR(36) NOT NULL, ow INTEGER NOT NULL default 0, - name VARCHAR(50) NOT NULL, + name VARCHAR(250) NOT NULL, d_created INTEGER UNSIGNED NOT NULL default 0, d_edited INTEGER UNSIGNED NOT NULL default 0, sorting TINYINT UNSIGNED NOT NULL default 0, @@ -357,8 +355,6 @@ function createSqliteTables($db) note TEXT COLLATE UTF8CI default NULL, prio TINYINT NOT NULL default 0, ow INTEGER NOT NULL default 0, - tags VARCHAR(600) NOT NULL default '', - tags_ids VARCHAR(250) NOT NULL default '', duedate DATE default NULL ) "); $db->ex("CREATE INDEX todo_list_id ON {$db->prefix}todolist (list_id)"); @@ -422,6 +418,8 @@ function databaseVersion(Database_Abstract $db): string $v = '1.4'; if ( !$db->tableExists($db->prefix.'settings') ) return $v; $v = '1.7'; + if ( $db->tableFieldExists($db->prefix.'todolist', 'tags') ) return $v; + $v = '1.8'; return $v; } @@ -579,7 +577,7 @@ function update_14_17(Database_Abstract $db, $dbtype) $db->ex("ALTER TABLE {$db->prefix}lists ADD `extra` TEXT"); # increase the length of list and tag name - # (not applicable to sqlite because it uses VARCHAR fields of eny length as TEXT) + # (not applicable to sqlite because it uses VARCHAR fields of any length as TEXT) $db->ex("ALTER TABLE {$db->prefix}todolist CHANGE `tags` `tags` VARCHAR(2000) NOT NULL default '' "); $db->ex("ALTER TABLE {$db->prefix}tags CHANGE `name` `name` VARCHAR(250) NOT NULL default '' "); $db->ex("ALTER TABLE {$db->prefix}lists CHANGE `name` `name` VARCHAR(250) NOT NULL default '' "); @@ -653,7 +651,7 @@ function update_17_18(Database_Abstract $db, $dbtype) if ($dbtype == 'sqlite') { - // Use UTF8CI collate. Old sqlite does not support DROP COLUMN + // Use UTF8CI collate. Old sqlite does not support DROP COLUMN (before v3.35.0 2021-03-12) $db->ex("DROP INDEX todo_list_id"); $db->ex("DROP INDEX todo_uuid"); $db->ex("ALTER TABLE {$db->prefix}todolist RENAME TO {$db->prefix}todolist_old"); @@ -670,11 +668,9 @@ function update_17_18(Database_Abstract $db, $dbtype) note TEXT COLLATE UTF8CI default NULL, prio TINYINT NOT NULL default 0, ow INTEGER NOT NULL default 0, - tags VARCHAR(600) NOT NULL default '', - tags_ids VARCHAR(250) NOT NULL default '', duedate DATE default NULL )" ); - $db->ex("INSERT INTO {$db->prefix}todolist SELECT * FROM {$db->prefix}todolist_old"); + $db->ex("INSERT INTO {$db->prefix}todolist SELECT id,uuid,list_id,d_created,d_completed,d_edited,compl,title,note,prio,ow,duedate FROM {$db->prefix}todolist_old"); $db->ex("CREATE INDEX todo_list_id ON {$db->prefix}todolist (list_id)"); $db->ex("CREATE UNIQUE INDEX todo_uuid ON {$db->prefix}todolist (uuid)"); $db->ex("DROP TABLE {$db->prefix}todolist_old"); @@ -684,12 +680,20 @@ function update_17_18(Database_Abstract $db, $dbtype) $db->ex( "CREATE TABLE {$db->prefix}tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, - name VARCHAR(50) NOT NULL DEFAULT '' COLLATE UTF8CI )" + name VARCHAR(250) NOT NULL DEFAULT '' COLLATE UTF8CI )" ); $db->ex("INSERT INTO {$db->prefix}tags SELECT * FROM {$db->prefix}tags_old"); $db->ex("CREATE INDEX tags_name ON {$db->prefix}tags (name)"); $db->ex("DROP TABLE {$db->prefix}tags_old"); } + else // mysql + { + $db->ex("ALTER TABLE {$db->prefix}todolist DROP COLUMN tags"); + $db->ex("ALTER TABLE {$db->prefix}todolist DROP COLUMN tags_ids"); + + // if mysql db was created in v1.7.x then tags.name field has length of 50 instead of 250 + $db->ex("ALTER TABLE {$db->prefix}tags CHANGE `name` `name` VARCHAR(250) NOT NULL default '' "); + } $db->ex("COMMIT"); From a41eae1ea770f06c0dd2721d89ee66867336a6f9 Mon Sep 17 00:00:00 2001 From: maxpozdeev Date: Fri, 24 Mar 2023 18:23:06 +0300 Subject: [PATCH 10/23] rename DBCore::defaultInstance() --- src/export.php | 2 +- src/feed.php | 2 +- src/includes/api/TasksController.php | 4 ++-- src/includes/class.dbcore.php | 2 +- src/index.php | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/export.php b/src/export.php index 091439f..d3cdca9 100644 --- a/src/export.php +++ b/src/export.php @@ -24,7 +24,7 @@ if (!$listData) { die("No list found."); } -$data = DBCore::defaultInstance()->getTasksByListId($listId, '', (int)$listData['sorting']); +$data = DBCore::default()->getTasksByListId($listId, '', (int)$listData['sorting']); if (_get('format') == 'ical') { printICal($listData, $data); diff --git a/src/feed.php b/src/feed.php index ebe2d61..369a461 100644 --- a/src/feed.php +++ b/src/feed.php @@ -64,7 +64,7 @@ printRss($data, $listData); function fillData(array &$data, int $listId, string $field, string $sqlWhere ) { - $tasks = DBCore::defaultInstance()->getTasksByListId($listId, $sqlWhere, "$field DESC", 100); + $tasks = DBCore::default()->getTasksByListId($listId, $sqlWhere, "$field DESC", 100); $lang = Lang::instance(); foreach ($tasks as $r) { diff --git a/src/includes/api/TasksController.php b/src/includes/api/TasksController.php index 9440e42..05c3218 100644 --- a/src/includes/api/TasksController.php +++ b/src/includes/api/TasksController.php @@ -191,7 +191,7 @@ class TasksController extends ApiController { checkWriteAccess(); $id = (int)$id; - if (!DBCore::defaultInstance()->taskExists($id)) { + if (!DBCore::default()->taskExists($id)) { $this->response->data = ['total' => 0]; return; } @@ -461,7 +461,7 @@ class TasksController extends ApiController { private function getTaskRowById(int $id): ?array { - $r = DBCore::defaultInstance()->getTaskById($id); + $r = DBCore::default()->getTaskById($id); if (!$r) { throw new Exception("Failed to fetch task data"); } diff --git a/src/includes/class.dbcore.php b/src/includes/class.dbcore.php index ef7581e..d1c5f63 100644 --- a/src/includes/class.dbcore.php +++ b/src/includes/class.dbcore.php @@ -44,7 +44,7 @@ class DBCore * @return DBCore * @throws Exception */ - public static function defaultInstance() : DBCore + public static function default() : DBCore { if (!isset(self::$defaultInstance)) { throw new Exception("DBCore defaultInstance is not initialized"); diff --git a/src/index.php b/src/index.php index 6e5d9f8..5b90d96 100644 --- a/src/index.php +++ b/src/index.php @@ -49,7 +49,7 @@ function parseRoute($queryString) redirectWithHashRoute($hash, $q); } else if (isset($q['task'])) { - $listId = (int)DBCore::defaultInstance()->getListIdByTaskId((int)$q['task']); + $listId = (int)DBCore::default()->getListIdByTaskId((int)$q['task']); if ($listId > 0) { $h = [ 'list', $listId, 'search', '#'. (int)$q['task']]; redirectWithHashRoute($h); From ced75b805a3a280e456298d7f23c8cd520b8bb67 Mon Sep 17 00:00:00 2001 From: maxpozdeev Date: Fri, 24 Mar 2023 18:51:46 +0300 Subject: [PATCH 11/23] can setup web api url to use PATH_INFO --- src/docker-config.php | 4 ++++ src/includes/classes.php | 2 +- src/index.php | 2 +- src/init.php | 7 ++++++- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/docker-config.php b/src/docker-config.php index 0d2d954..7b810be 100644 --- a/src/docker-config.php +++ b/src/docker-config.php @@ -17,3 +17,7 @@ else if (getenv('MTT_DB_TYPE') == 'sqlite') { } define("MTT_SALT", "Put Random Text Here"); +//define("MTT_DISABLE_EXT", 1); +if (getenv('MTT_API_USE_PATH_INFO')) { + define("MTT_API_USE_PATH_INFO", 1); +} diff --git a/src/includes/classes.php b/src/includes/classes.php index be2685a..dec7f1d 100644 --- a/src/includes/classes.php +++ b/src/includes/classes.php @@ -14,7 +14,7 @@ class ApiRequest public $jsonBody; function __construct() { - if (defined('MTT_API_USE_PATH_INFO') && MTT_API_USE_PATH_INFO) { + if (defined('MTT_API_USE_PATH_INFO')) { $this->path = $_SERVER['PATH_INFO']; } else { diff --git a/src/index.php b/src/index.php index 5b90d96..c5d0ecd 100644 --- a/src/index.php +++ b/src/index.php @@ -79,7 +79,7 @@ function js_options() "lang" => Lang::instance()->jsStrings(), "mttUrl" => get_mttinfo('mtt_url'), "homeUrl" => get_mttinfo('url'), -// "apiUrl" => get_mttinfo('api_url'), + "apiUrl" => get_mttinfo('api_url'), "needAuth" => need_auth() ? true : false, "isLogged" => is_logged() ? true : false, "showdate" => Config::get('showdate') ? true : false, diff --git a/src/init.php b/src/init.php index 1d9bac1..ea8a145 100644 --- a/src/init.php +++ b/src/init.php @@ -352,7 +352,12 @@ function get_unsafe_mttinfo($v) /* URL for API, like http://localhost/mytinytodo/api/. No need to set by default. */ $_mttinfo['api_url'] = Config::getUrl('api_url'); // need to have a trailing slash if ($_mttinfo['api_url'] == '') { - $_mttinfo['api_url'] = get_unsafe_mttinfo('mtt_url'). 'api.php?_path=/'; + if (defined('MTT_API_USE_PATH_INFO')) { + $_mttinfo['api_url'] = get_unsafe_mttinfo('mtt_url'). 'api/'; + } + else { + $_mttinfo['api_url'] = get_unsafe_mttinfo('mtt_url'). 'api.php?_path=/'; + } } return $_mttinfo['api_url']; case 'title': From 176b1e9c99bb233791dd60dc7ba8f456d2b7664f Mon Sep 17 00:00:00 2001 From: maxpozdeev Date: Fri, 24 Mar 2023 19:07:58 +0300 Subject: [PATCH 12/23] - fix deprecation notices in php 8.2 (cherry picked from commit 47972dce05b21e988086410977553b08c7fb01ae) --- src/ext/notifications/class.observer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ext/notifications/class.observer.php b/src/ext/notifications/class.observer.php index 96566df..b1b7f0c 100644 --- a/src/ext/notifications/class.observer.php +++ b/src/ext/notifications/class.observer.php @@ -59,7 +59,7 @@ class NotificationObserver implements \MTTNotificationObserverInterface private function init() { $this->prefs = NotificationsExtension::preferences(); - $this->token = $this->prefs['token'] ?? ''; + //$this->token = $this->prefs['token'] ?? ''; } } From 74e59f1ccf87d0f8b50a141a2c2614202ae8b040 Mon Sep 17 00:00:00 2001 From: maxpozdeev Date: Fri, 24 Mar 2023 23:21:37 +0300 Subject: [PATCH 13/23] add dev-docker for php-fpm 8.2 + nginx --- docker/dev/_nginx/Dockerfile-nginx-alpine | 5 +++++ docker/dev/mtt-php81-nginx/.env | 1 - docker/dev/mtt-php81-nginx/Dockerfile-fpm | 11 ----------- docker/dev/mtt-php82-nginx/.env | 1 + docker/dev/mtt-php82-nginx/Dockerfile-fpm | 12 ++++++++++++ .../{mtt-php81-nginx => mtt-php82-nginx}/compose.yml | 7 ++++--- docker/dev/mtt-php82-nginx/php-fpm-www.conf | 12 ++++++++++++ .../{mtt-php81-nginx => mtt-php82-nginx}/php-mtt.ini | 0 .../php-opcache.ini | 0 9 files changed, 34 insertions(+), 15 deletions(-) create mode 100644 docker/dev/_nginx/Dockerfile-nginx-alpine delete mode 100644 docker/dev/mtt-php81-nginx/.env delete mode 100644 docker/dev/mtt-php81-nginx/Dockerfile-fpm create mode 100644 docker/dev/mtt-php82-nginx/.env create mode 100644 docker/dev/mtt-php82-nginx/Dockerfile-fpm rename docker/dev/{mtt-php81-nginx => mtt-php82-nginx}/compose.yml (83%) create mode 100644 docker/dev/mtt-php82-nginx/php-fpm-www.conf rename docker/dev/{mtt-php81-nginx => mtt-php82-nginx}/php-mtt.ini (100%) rename docker/dev/{mtt-php81-nginx => mtt-php82-nginx}/php-opcache.ini (100%) diff --git a/docker/dev/_nginx/Dockerfile-nginx-alpine b/docker/dev/_nginx/Dockerfile-nginx-alpine new file mode 100644 index 0000000..1312e5b --- /dev/null +++ b/docker/dev/_nginx/Dockerfile-nginx-alpine @@ -0,0 +1,5 @@ +FROM nginx:alpine + +COPY default.conf /etc/nginx/conf.d/ + +VOLUME /var/www/html diff --git a/docker/dev/mtt-php81-nginx/.env b/docker/dev/mtt-php81-nginx/.env deleted file mode 100644 index f2087fd..0000000 --- a/docker/dev/mtt-php81-nginx/.env +++ /dev/null @@ -1 +0,0 @@ -PLATFORM_NAME=mtt-dev-php81-nginx diff --git a/docker/dev/mtt-php81-nginx/Dockerfile-fpm b/docker/dev/mtt-php81-nginx/Dockerfile-fpm deleted file mode 100644 index fbfefe4..0000000 --- a/docker/dev/mtt-php81-nginx/Dockerfile-fpm +++ /dev/null @@ -1,11 +0,0 @@ -FROM php:8.1-fpm - -RUN docker-php-ext-install mysqli && \ - mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" && \ - mkdir /var/www/phpinfo && \ - echo " /var/www/phpinfo/phpinfo.php - -#COPY php-mtt.ini /usr/local/etc/php/conf.d/ -#COPY php-opcache.ini /usr/local/etc/php/conf.d/ - -VOLUME /var/www/html diff --git a/docker/dev/mtt-php82-nginx/.env b/docker/dev/mtt-php82-nginx/.env new file mode 100644 index 0000000..aa353f2 --- /dev/null +++ b/docker/dev/mtt-php82-nginx/.env @@ -0,0 +1 @@ +PLATFORM_NAME=mtt-dev-php82-nginx-alpine diff --git a/docker/dev/mtt-php82-nginx/Dockerfile-fpm b/docker/dev/mtt-php82-nginx/Dockerfile-fpm new file mode 100644 index 0000000..3497a3a --- /dev/null +++ b/docker/dev/mtt-php82-nginx/Dockerfile-fpm @@ -0,0 +1,12 @@ +FROM php:8.2-fpm-alpine + +RUN docker-php-ext-install pdo_mysql && \ + mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" && \ + mkdir /var/www/phpinfo && \ + echo " /var/www/phpinfo/phpinfo.php + +#COPY php-mtt.ini /usr/local/etc/php/conf.d/ +#COPY php-opcache.ini /usr/local/etc/php/conf.d/ +COPY php-fpm-www.conf /usr/local/etc/php-fpm.d/www.conf + +VOLUME /var/www/html diff --git a/docker/dev/mtt-php81-nginx/compose.yml b/docker/dev/mtt-php82-nginx/compose.yml similarity index 83% rename from docker/dev/mtt-php81-nginx/compose.yml rename to docker/dev/mtt-php82-nginx/compose.yml index 7e8e41a..7a3a81a 100644 --- a/docker/dev/mtt-php81-nginx/compose.yml +++ b/docker/dev/mtt-php82-nginx/compose.yml @@ -8,8 +8,8 @@ services: web: build: context: ../_nginx - dockerfile: Dockerfile-nginx - image: mtt-dev/nginx + dockerfile: Dockerfile-nginx-alpine + image: mtt-dev/nginx:alpine container_name: ${PLATFORM_NAME}-web ports: - "8080:80" @@ -24,12 +24,13 @@ services: build: context: . dockerfile: Dockerfile-fpm - image: mtt-dev/php:8.1-fpm + image: mtt-dev/php:8.2-fpm-alpine container_name: ${PLATFORM_NAME}-fpm hostname: php-fpm environment: - MTT_ENABLE_DEBUG=YES - MTT_DB_TYPE=sqlite + - MTT_API_USE_PATH_INFO=YES volumes: - ../../../src:/var/www/html - ./php-mtt.ini:/usr/local/etc/php/conf.d/php-mtt.ini diff --git a/docker/dev/mtt-php82-nginx/php-fpm-www.conf b/docker/dev/mtt-php82-nginx/php-fpm-www.conf new file mode 100644 index 0000000..b162e94 --- /dev/null +++ b/docker/dev/mtt-php82-nginx/php-fpm-www.conf @@ -0,0 +1,12 @@ +[www] +user = www-data +group = www-data +listen = 127.0.0.1:9000 +pm = dynamic +pm.max_children = 5 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 +pm.max_requests = 500 + +access.format = "%R - %u [%t] \"%m %r\" %s %f" diff --git a/docker/dev/mtt-php81-nginx/php-mtt.ini b/docker/dev/mtt-php82-nginx/php-mtt.ini similarity index 100% rename from docker/dev/mtt-php81-nginx/php-mtt.ini rename to docker/dev/mtt-php82-nginx/php-mtt.ini diff --git a/docker/dev/mtt-php81-nginx/php-opcache.ini b/docker/dev/mtt-php82-nginx/php-opcache.ini similarity index 100% rename from docker/dev/mtt-php81-nginx/php-opcache.ini rename to docker/dev/mtt-php82-nginx/php-opcache.ini From 755d7662629970e382ea0319c91a20ede26f86ba Mon Sep 17 00:00:00 2001 From: maxpozdeev Date: Mon, 27 Mar 2023 15:45:53 +0300 Subject: [PATCH 14/23] add dev-docker for php 8.2 + angie --- docker/dev/_nginx/Dockerfile-angie-alpine | 23 +++++++++++++ docker/dev/mtt-php82-angie/.env | 1 + docker/dev/mtt-php82-angie/compose.yml | 40 +++++++++++++++++++++++ docker/dev/mtt-php82-nginx/Dockerfile-fpm | 5 ++- 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 docker/dev/_nginx/Dockerfile-angie-alpine create mode 100644 docker/dev/mtt-php82-angie/.env create mode 100644 docker/dev/mtt-php82-angie/compose.yml diff --git a/docker/dev/_nginx/Dockerfile-angie-alpine b/docker/dev/_nginx/Dockerfile-angie-alpine new file mode 100644 index 0000000..12250a6 --- /dev/null +++ b/docker/dev/_nginx/Dockerfile-angie-alpine @@ -0,0 +1,23 @@ +FROM alpine:latest + +RUN set -x \ + && apk add --no-cache ca-certificates curl \ + && curl -o /etc/apk/keys/angie-signing.rsa https://angie.software/keys/angie-signing.rsa \ + && echo "https://download.angie.software/angie/alpine/v$(egrep -o '[0-9]+\.[0-9]+' /etc/alpine-release)/main" >> /etc/apk/repositories \ + && apk add --no-cache angie \ + && rm /etc/apk/keys/angie-signing.rsa \ + && ln -sf /dev/stdout /var/log/angie/access.log \ + && ln -sf /dev/stderr /var/log/angie/error.log + +EXPOSE 80 +CMD ["angie", "-g", "daemon off;"] + + +RUN mv /etc/angie/http.d/default.conf /etc/angie/http.d/default.conf.bak \ + && mkdir /var/log/nginx \ + && ln -sf /var/log/angie/access.log /var/log/nginx/access.log + +# config from nginx +COPY default.conf /etc/angie/http.d/default.conf + +VOLUME /var/www/html diff --git a/docker/dev/mtt-php82-angie/.env b/docker/dev/mtt-php82-angie/.env new file mode 100644 index 0000000..0993892 --- /dev/null +++ b/docker/dev/mtt-php82-angie/.env @@ -0,0 +1 @@ +PLATFORM_NAME=mtt-dev-php82-angie-alpine diff --git a/docker/dev/mtt-php82-angie/compose.yml b/docker/dev/mtt-php82-angie/compose.yml new file mode 100644 index 0000000..d39bee2 --- /dev/null +++ b/docker/dev/mtt-php82-angie/compose.yml @@ -0,0 +1,40 @@ +version: "3.9" + +networks: + network: + name: "${PLATFORM_NAME}-network" + +services: + web: + build: + context: ../_nginx + dockerfile: Dockerfile-angie-alpine + image: mtt-dev/angie:alpine + container_name: ${PLATFORM_NAME}-web + ports: + - "8080:80" + volumes: + - ../../../src:/var/www/html + depends_on: + - fpm + networks: + - network + + fpm: + build: + context: ../mtt-php82-nginx + dockerfile: Dockerfile-fpm + image: mtt-dev/php:8.2-fpm-alpine + container_name: ${PLATFORM_NAME}-fpm + hostname: php-fpm + environment: + - MTT_ENABLE_DEBUG=YES + - MTT_DB_TYPE=sqlite + - MTT_API_USE_PATH_INFO=YES + volumes: + - ../../../src:/var/www/html + - ../../../tests:/var/www/tests + - ../mtt-php82-nginx/php-mtt.ini:/usr/local/etc/php/conf.d/php-mtt.ini + - ../mtt-php82-nginx/php-opcache.ini:/usr/local/etc/php/conf.d/php-opcache.ini + networks: + - network diff --git a/docker/dev/mtt-php82-nginx/Dockerfile-fpm b/docker/dev/mtt-php82-nginx/Dockerfile-fpm index 3497a3a..29e5f3c 100644 --- a/docker/dev/mtt-php82-nginx/Dockerfile-fpm +++ b/docker/dev/mtt-php82-nginx/Dockerfile-fpm @@ -3,7 +3,10 @@ FROM php:8.2-fpm-alpine RUN docker-php-ext-install pdo_mysql && \ mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" && \ mkdir /var/www/phpinfo && \ - echo " /var/www/phpinfo/phpinfo.php + echo " /var/www/phpinfo/phpinfo.php && \ + curl -o /usr/local/bin/phpunit -fL "https://phar.phpunit.de/phpunit-10.phar" && \ + chmod +x /usr/local/bin/phpunit && \ + ln -s html /var/www/src #COPY php-mtt.ini /usr/local/etc/php/conf.d/ #COPY php-opcache.ini /usr/local/etc/php/conf.d/ From ca3e332a2f7b1dc745d36e6afdf659f3ae93ee1f Mon Sep 17 00:00:00 2001 From: maxpozdeev Date: Tue, 28 Mar 2023 23:15:06 +0300 Subject: [PATCH 15/23] -fix: handle incorrect json in settings (cherry picked from commit 80f19f936ca41b91ee5a11ca9f545c30b63f1fad) --- src/includes/class.config.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/includes/class.config.php b/src/includes/class.config.php index 4159542..028e6d2 100644 --- a/src/includes/class.config.php +++ b/src/includes/class.config.php @@ -218,13 +218,13 @@ class Config * @return array * @throws Exception */ - public static function requestDomain(string $key) + public static function requestDomain(string $key): array { $db = DBConnection::instance(); $json = $db->sq("SELECT param_value FROM {$db->prefix}settings WHERE param_key = ?", array($key)); if (!$json) return array(); $j = json_decode($json, true, 100, JSON_INVALID_UTF8_SUBSTITUTE); - if ($j === false) { + if ($j === null) { error_log("MTT Error: Failed to decode JSON object with settings. Code: ". (int)json_last_error()); return array(); } @@ -237,7 +237,7 @@ class Config * @return array * @throws Exception */ - public static function requestDefaultDomain() + public static function requestDefaultDomain(): array { return self::requestDomain('config.json'); } From 772305dbedda2fd737d20cc7223f3676e38543c5 Mon Sep 17 00:00:00 2001 From: maxpozdeev Date: Thu, 3 Aug 2023 23:09:32 +0300 Subject: [PATCH 16/23] + can setup postgresql database --- src/docker-config.php | 16 +-- src/includes/class.db.postgres.php | 208 +++++++++++++++++++++++++++++ src/init.php | 25 ++++ src/setup.php | 157 +++++++++++++++++++--- 4 files changed, 378 insertions(+), 28 deletions(-) create mode 100644 src/includes/class.db.postgres.php diff --git a/src/docker-config.php b/src/docker-config.php index 7b810be..8d6dd9d 100644 --- a/src/docker-config.php +++ b/src/docker-config.php @@ -2,14 +2,14 @@ // Rename it to config.php before using in docker. -if (getenv('MTT_DB_TYPE') == 'mysql') { - define("MTT_DB_TYPE", "mysql"); - define("MTT_DB_HOST", getenv('MTT_DB_HOST')); - define("MTT_DB_NAME", getenv('MTT_DB_NAME')); - define("MTT_DB_USER", getenv('MTT_DB_USER')); - define("MTT_DB_PASSWORD", getenv('MTT_DB_PASSWORD')); - define("MTT_DB_PREFIX", getenv('MTT_DB_PREFIX')); - define("MTT_DB_DRIVER", getenv('MTT_DB_DRIVER')); +if (getenv('MTT_DB_TYPE') == 'mysql' || getenv('MTT_DB_TYPE') == 'postgres') { + define("MTT_DB_TYPE", getenv('MTT_DB_TYPE')); + define("MTT_DB_HOST", getenv('MTT_DB_HOST') ?: "undefined_host"); + define("MTT_DB_NAME", getenv('MTT_DB_NAME') ?: "undefined_db"); + define("MTT_DB_USER", getenv('MTT_DB_USER') ?: "undefined_user"); + define("MTT_DB_PASSWORD", getenv('MTT_DB_PASSWORD') ?: ""); + define("MTT_DB_PREFIX", getenv('MTT_DB_PREFIX') ?: "mtt_"); + define("MTT_DB_DRIVER", getenv('MTT_DB_DRIVER') ?: ""); } else if (getenv('MTT_DB_TYPE') == 'sqlite') { define("MTT_DB_TYPE", "sqlite"); diff --git a/src/includes/class.db.postgres.php b/src/includes/class.db.postgres.php new file mode 100644 index 0000000..776c557 --- /dev/null +++ b/src/includes/class.db.postgres.php @@ -0,0 +1,208 @@ + + Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. +*/ + +// ---------------------------------------------------------------------------- // +class DatabaseResult_Postgres extends DatabaseResult_Abstract +{ + /** @var PDOStatement */ + protected $q; + + /** @var int */ + protected $affected; + + /** @var string const */ + protected $schema = 'public'; + + function __construct(PDO $dbh, string $query, bool $resultless = false) + { + // use with DELETE, INSERT, UPDATE + if ($resultless) + { + $this->affected = (int) $dbh->exec($query); //throws PDOException + } + // SELECT + else + { + $this->q = $dbh->query($query); //throws PDOException + $this->affected = $this->q->rowCount(); + } + } + + function fetchRow(): ?array + { + $res = $this->q->fetch(PDO::FETCH_NUM); + if ($res === false || !is_array($res)) { + return null; + } + return $res; + } + + function fetchAssoc(): ?array + { + $res = $this->q->fetch(PDO::FETCH_ASSOC); + if ($res === false || !is_array($res)) { + return null; + } + return $res; + } + + function rowsAffected(): int + { + return $this->affected; + } +} + +// ---------------------------------------------------------------------------- // +class Database_Postgres extends Database_Abstract +{ + const DBTYPE = 'postgres'; + + /** @var PDO */ + protected $dbh; + + /** @var int */ + protected $affected = 0; + + protected $dbname; + + function __construct() + { + } + + function connect(array $params): void + { + $host = $params['host']; + $user = $params['user']; + $pass = $params['password']; + $db = $params['db']; + $options = array( + PDO::PGSQL_ATTR_DISABLE_PREPARES => 1, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + ); + $this->dbname = $db; + $this->dbh = new PDO("pgsql:host=$host;dbname=$db", $user, $pass, $options); + } + + + + /* + Returns single row of SELECT query as indexed array (FETCH_NUM). + Returns single field value if resulting array has only one field. + */ + function sq(string $query, ?array $values = null) + { + $q = $this->_dq($query, $values); + + $res = $q->fetchRow(); + if ($res === false || !is_array($res)) { + return null; + } + + if (sizeof($res) > 1) return $res; + else return $res[0]; + } + + /* + Returns single row of SELECT query as dictionary array (FETCH_ASSOC). + */ + function sqa(string $query, ?array $values = null): ?array + { + $q = $this->_dq($query, $values); + $res = $q->fetchAssoc(); + if ($res === false || !is_array($res)){ + return null; + } + return $res; + } + + function dq(string $query, ?array $values = null) : DatabaseResult_Abstract + { + return $this->_dq($query, $values); + } + + /* + for resultless queries like INSERT,UPDATE,DELETE + */ + function ex(string $query, ?array $values = null): void + { + $this->_dq($query, $values, true); + } + + private function _dq(string $query, ?array $values = null, bool $resultless = false) : DatabaseResult_Abstract + { + if (null !== $values && sizeof($values) > 0) + { + $m = explode('?', $query); + if (sizeof($m) < sizeof($values)+1) { + throw new Exception("params to set MORE than query params"); + } + if (sizeof($m) > sizeof($values)+1) { + throw new Exception("params to set LESS than query params"); + } + $query = ""; + for ($i=0; $iquote($values[$i]); + } + $query .= $m[$i]; + } + $this->lastQuery = $query; + $dbr = new DatabaseResult_Postgres($this->dbh, $query, $resultless); + $this->affected = $dbr->rowsAffected(); + return $dbr; + } + + function affected(): int + { + return $this->affected; + } + + function quote($value): string + { + if (null === $value) { + return 'null'; + } + return $this->dbh->quote((string) $value); + } + + function quoteForLike(string $format, string $string): string + { + $string = str_replace(array('\\','%','_'), array('\\\\','\%','\_'), $string); + return $this->dbh->quote(sprintf($format, $string)). " ESCAPE '\'"; + } + + function like(string $column, string $format, string $string): string + { + $column = str_replace('"', '""', $column); + return '"'. $column. '" ILIKE '. $this->quoteForLike($format, $string); + } + + function lastInsertId(?string $name = null): ?string + { + $ret = $this->dbh->lastInsertId(); + if (false === $ret) { + return null; + } + return (string) $ret; + } + + function tableExists(string $table): bool + { + $r = $this->sq("SELECT 1 FROM information_schema.tables WHERE table_catalog = ? AND table_name = ?", + array($this->dbname, $table) ); + if ($r === false || $r === null) return false; + return true; + } + + function tableFieldExists(string $table, string $field): bool + { + $r = $this->sq("SELECT 1 FROM information_schema.columns WHERE table_name = ? AND column_name = ? AND table_schema = ?", + array($table, $field, $this->schema) ); + if ($r === false || $r === null) return false; + return true; + } +} diff --git a/src/init.php b/src/init.php index ea8a145..89ca472 100644 --- a/src/init.php +++ b/src/init.php @@ -116,6 +116,31 @@ function configureDbConnection() $db->dq("SET NAMES utf8mb4"); } + # PostgreSQL Database + else if (MTT_DB_TYPE == 'postgres') + { + require_once(MTTINC. 'class.db.postgres.php'); + $db = DBConnection::init(new Database_Postgres()); + try { + $db->connect([ + 'host' => MTT_DB_HOST, + 'user' => MTT_DB_USER, + 'password' => MTT_DB_PASSWORD, + 'db' => MTT_DB_NAME, + ]); + } + catch(Exception $e) { + $errlog = "Failed to connect to PostgreSQL database: ". $e->getMessage(); + if (MTT_DEBUG) { + logAndDie($errlog); + } + else { + logAndDie("Failed to connect to database", $errlog); + } + } + $db->dq("SET NAMES 'utf8'"); + } + # SQLite3 Database elseif (MTT_DB_TYPE == 'sqlite') { diff --git a/src/setup.php b/src/setup.php index 51a854b..ea3bccc 100644 --- a/src/setup.php +++ b/src/setup.php @@ -77,22 +77,28 @@ if ($configExists) // Determine current installed db version $ver = databaseVersion($db); - if ($ver == '1.4') { + if ($ver == '') { + // clean install + // will not load settings from database in init.php + Config::$noDatabase = true; + } + else if (version_compare($ver, '1.4') < 0) { + // Very old or previously failed while install + exitMessage(htmlspecialchars("Can not update. Unsupported database version ($ver).")); + } + else if ($ver == '1.4') { // Need to upgrade. Do not ask for old password require_once(MTTPATH. 'db/config.php'); Config::loadConfigV14($config); unset($config); DBConnection::init($db); } - else { - if ($ver == '') { - Config::$noDatabase = true; //will not load settings from database in init.php - } - require_once('./init.php'); - if ( !is_logged() ) { - die("Access denied!
Disable password protection or Log in."); - } + + require_once('./init.php'); + if ( !is_logged() ) { + die("Access denied!
Disable password protection or Log in."); } + } if ($ver == '') @@ -102,7 +108,7 @@ if ($ver == '') if ($install == '' && $db !== null) { # We already have settings file and need to create tables. - exitMessage("
Click next to create tables in '". htmlspecialchars($dbtype). "' database.

+ exitMessage("Click next to create tables in ". htmlspecialchars(databaseTypeName($db)). " database.

"); @@ -114,9 +120,10 @@ if ($ver == '')
Select database type to use:

-

-
-