diff --git a/CLI/FetchToncoinTransactions.php b/CLI/FetchToncoinTransactions.php index 9de2c998..171ea091 100755 --- a/CLI/FetchToncoinTransactions.php +++ b/CLI/FetchToncoinTransactions.php @@ -17,13 +17,16 @@ define("NANOTON", 1000000000); class FetchToncoinTransactions extends Command { - private $images; + private $transactions; protected static $defaultName = "fetch-ton"; public function __construct() { - $this->transactions = DatabaseConnection::i()->getContext()->table("cryptotransactions"); + $ctx = DatabaseConnection::i()->getContext(); + if (in_array("cryptotransactions", $ctx->getStructure()->getTables())) { + $this->transactions = $ctx->table("cryptotransactions"); + } parent::__construct(); } @@ -76,7 +79,6 @@ class FetchToncoinTransactions extends Command $header->writeln("Gonna up the balance of users"); foreach ($response["result"] as $transfer) { - $outputArray; preg_match('/' . OPENVK_ROOT_CONF["openvk"]["preferences"]["ton"]["regex"] . '/', $transfer["in_msg"]["message"], $outputArray); $userId = ctype_digit($outputArray[1]) ? intval($outputArray[1]) : null; if (is_null($userId)) { diff --git a/CLI/RebuildImagesCommand.php b/CLI/RebuildImagesCommand.php index cbc9e49c..6340743c 100644 --- a/CLI/RebuildImagesCommand.php +++ b/CLI/RebuildImagesCommand.php @@ -20,7 +20,10 @@ class RebuildImagesCommand extends Command public function __construct() { - $this->images = DatabaseConnection::i()->getContext()->table("photos"); + $ctx = DatabaseConnection::i()->getContext(); + if (in_array("photos", $ctx->getStructure()->getTables())) { + $this->images = $ctx->table("photos"); + } parent::__construct(); } diff --git a/CLI/UpgradeCommand.php b/CLI/UpgradeCommand.php new file mode 100644 index 00000000..f0ef8f5d --- /dev/null +++ b/CLI/UpgradeCommand.php @@ -0,0 +1,355 @@ +db = DatabaseConnection::i()->getConnection(); + $this->eventDb = eventdb()->getConnection(); + + parent::__construct(); + } + + protected function configure(): void + { + $this->setDescription("Upgrade OpenVK installation") + ->setHelp("This command upgrades database schema after OpenVK was updated") + ->addOption( + "quick", + "Q", + InputOption::VALUE_NEGATABLE, + "Don't display warning before migrating database", + false + ) + ->addOption( + "repair", + "R", + InputOption::VALUE_NEGATABLE, + "Attempt to repair database schema if tables are missing", + false + ) + ->addOption( + "oneshot", + "O", + InputOption::VALUE_NONE, + "Only execute one operation" + ) + ->addArgument( + "chandler", + InputArgument::OPTIONAL, + "Location of Chandler installation" + ); + } + + protected function checkDatabaseReadiness(bool &$chandlerOk, bool &$ovkOk, bool &$eventOk, bool &$migrationsOk): void + { + $tables = $this->db->query("SHOW TABLES")->fetchAll(); + $tables = array_map(fn($x) => strtoupper($x->offsetGet(0)), $tables); + + $missingTables = array_diff($this->chandlerTables, $tables); + if (sizeof($missingTables) == 0) { + $chandlerOk = true; + } elseif (sizeof($missingTables) == sizeof($this->chandlerTables)) { + $chandlerOk = null; + } else { + $chandlerOk = false; + } + + if (is_null($this->eventDb)) { + $eventOk = false; + } elseif (is_null($this->eventDb->query("SHOW TABLES LIKE \"notifications\"")->fetch())) { + $eventOk = null; + } else { + $eventOk = true; + } + + $ovkOk = in_array("PROFILES", $tables); + $migrationsOk = in_array("OVK_UPGRADE_HISTORY", $tables); + } + + protected function executeSqlScript( + int $errCode, + string $script, + SymfonyStyle $io, + bool $transaction = false, + bool $eventDb = false + ): int { + $pdo = ($eventDb ? $this->eventDb : $this->db)->getPdo(); + + $res = false; + try { + if ($transaction) { + $res = $pdo->beginTransaction(); + } + + $res = $pdo->exec($script); + + if ($transaction) { + $res = $pdo->commit(); + } + } catch (\PDOException $e) { + } + + if ($res === false) { + goto error; + } + + return 0; + + error: + $io->getErrorStyle()->error([ + "Failed to execute SQL statement:", + implode("\t", $pdo->errorInfo()), + ]); + + return $errCode; + } + + protected function getNextLevel(bool $eventDb = false): int + { + $tbl = $eventDb ? "ovk_events_upgrade_history" : "ovk_upgrade_history"; + $record = $this->db->query("SELECT level FROM $tbl ORDER BY level DESC LIMIT 1"); + if (!$record->getRowCount()) { + return 0; + } + + return $record->fetchField() + 1; + } + + protected function getMigrationFiles(bool $eventDb = false): array + { + $files = []; + $root = dirname(__DIR__ . "/../install/init-static-db.sql"); + $dir = $eventDb ? "sqls/eventdb" : "sqls"; + + foreach (glob("$root/$dir/*.sql") as $file) { + $files[(int) basename($file)] = basename($file); + } + + ksort($files); + + return $files; + } + + protected function installChandler(InputInterface $input, SymfonyStyle $io, bool $drop = false): int + { + $chandlerLocation = $input->getArgument("chandler") ?? (__DIR__ . "/../../../../"); + $chandlerConfigLocation = "$chandlerLocation/chandler.yml"; + + if (!file_exists($chandlerConfigLocation)) { + $err = ["Could not find chandler location. Perhaps your config is too unique?"]; + if (!$input->getOption("chandler")) { + $err[] = "Specify absolute path to your chandler installation using the --chandler option."; + } + + $io->getErrorStyle()->error($err); + + return 21; + } + + if ($drop) { + $bar = new ProgressBar($io, sizeof($this->chandlerTables)); + $io->writeln("Dropping chandler tables..."); + + foreach ($bar->iterate($this->chandlerTables) as $table) { + $this->db->query("DROP TABLE IF EXISTS $table;"); + } + + $io->newLine(); + } + + $installFile = file_get_contents("$chandlerLocation/install/init-db.sql"); + + return $this->executeSqlScript(22, $installFile, $io); + } + + protected function initSchema(SymfonyStyle $io): int + { + $installFile = file_get_contents(__DIR__ . "/../install/init-static-db.sql"); + + return $this->executeSqlScript(31, $installFile, $io); + } + + protected function initEventSchema(SymfonyStyle $io): int + { + $installFile = file_get_contents(__DIR__ . "/../install/init-event-db.sql"); + + return $this->executeSqlScript(31, $installFile, $io, true, true); + } + + protected function initUpgradeLog(SymfonyStyle $io): int + { + $installFile = file_get_contents(__DIR__ . "/../install/init-migration-table.sql"); + + return $this->executeSqlScript(31, $installFile, $io, true); + } + + protected function runMigrations(SymfonyStyle $io, bool $eventDb, bool $oneshot): int + { + $dir = $eventDb ? "sqls/eventdb" : "sqls"; + $tbl = $eventDb ? "ovk_events_upgrade_history" : "ovk_upgrade_history"; + $nextLevel = $this->getNextLevel($eventDb); + $migrations = array_filter( + $this->getMigrationFiles($eventDb), + fn($id) => $id >= $nextLevel, + ARRAY_FILTER_USE_KEY + ); + + if (!sizeof($migrations)) { + return 24; + } + + $uname = addslashes(`whoami`); + $bar = new ProgressBar($io, sizeof($migrations)); + $bar->setFormat("very_verbose"); + + foreach ($bar->iterate($migrations) as $num => $migration) { + $script = file_get_contents(__DIR__ . "/../install/$dir/$migration"); + $res = $this->executeSqlScript(100 + $num, $script, $io, true, $eventDb); + if ($res != 0) { + $io->getErrorStyle()->error("Error while executing migration №$num"); + + return $res; + } + + $t = time(); + $this->db->query("INSERT INTO $tbl VALUES ($num, $t, \"$uname\");"); + + if ($oneshot) { + return 5; + } + } + + $io->newLine(); + + return 0; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $oneShotMode = $input->getOption("oneshot"); + $io = new SymfonyStyle($input, $output); + + if (!$input->getOption("quick")) { + $io->writeln("Do full backup of the database before executing this command!"); + $io->writeln("Command will resume execution after 5 seconds."); + $io->writeln("You can skip this warning with --quick option."); + sleep(5); + } + + $migrationsOk = false; + $chandlerOk = false; + $eventOk = false; + $ovkOk = false; + + $this->checkDatabaseReadiness($chandlerOk, $ovkOk, $eventOk, $migrationsOk); + + $res = -1; + if ($chandlerOk === null) { + $io->writeln("Chandler schema not detected, attempting to install..."); + + $res = $this->installChandler($input, $io); + } elseif ($chandlerOk === false) { + if ($input->getOption("repair")) { + $io->warning("Chandler schema detected but is broken, attempting to repair..."); + + $res = $this->installChandler($input, $io, true); + } else { + $io->writeln("Chandler schema detected but is broken"); + $io->writeln("Run command with --repair to repair (PERMISSIONS WILL BE LOST)"); + + return 1; + } + } + + if ($res > 0) { + return $res; + } elseif ($res == 0 && $oneShotMode) { + return 5; + } + + if (!$ovkOk) { + $io->writeln("Initializing OpenVK schema..."); + $res = $this->initSchema($io); + if ($res > 0) { + return $res; + } elseif ($oneShotMode) { + return 5; + } + } + + if (!$migrationsOk) { + $io->writeln("Initializing upgrade log..."); + $res = $this->initUpgradeLog($io); + if ($res > 0) { + return $res; + } elseif ($oneShotMode) { + return 5; + } + } + + if ($eventOk !== false) { + if ($eventOk === null) { + $io->writeln("Initializing event database..."); + $res = $this->initEventSchema($io); + if ($res > 0) { + return $res; + } elseif ($oneShotMode) { + return 5; + } + } + + $io->writeln("Upgrading event database..."); + $res = $this->runMigrations($io, true, $oneShotMode); + if ($res == 24) { + $output->writeln("Event database already up to date."); + } elseif ($res > 0) { + return $res; + } + } + + $io->writeln("Upgrading database..."); + $res = $this->runMigrations($io, false, $oneShotMode); + + if (!$res) { + $io->success("Database has been upgraded!"); + + return 0; + } elseif ($res != 24) { + return $res; + } + + $io->writeln("Database up to date. Nothing left to do."); + + return 0; + } +} diff --git a/bootstrap.php b/bootstrap.php index 42241ca4..21fdc65b 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -443,7 +443,10 @@ function downloadable_name(string $text): string } return (function () { - _ovk_check_environment(); + if (php_sapi_name() != "cli") { + _ovk_check_environment(); + } + require __DIR__ . "/vendor/autoload.php"; setlocale(LC_TIME, "POSIX"); diff --git a/install/init-migration-table.sql b/install/init-migration-table.sql new file mode 100644 index 00000000..e26afcbb --- /dev/null +++ b/install/init-migration-table.sql @@ -0,0 +1,15 @@ +START TRANSACTION; + +CREATE TABLE `ovk_upgrade_history` ( + `level` smallint UNSIGNED NOT NULL, + `timestamp` bigint(20) UNSIGNED NOT NULL, + `operator` varchar(256) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT "Maintenance Script" +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +CREATE TABLE `ovk_events_upgrade_history` ( + `level` smallint UNSIGNED NOT NULL, + `timestamp` bigint(20) UNSIGNED NOT NULL, + `operator` varchar(256) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT "Maintenance Script" +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +COMMIT; \ No newline at end of file diff --git a/install/sqls/00000-gifts.sql b/install/sqls/00000-gifts.sql index 0ee54f14..59a5c46d 100644 --- a/install/sqls/00000-gifts.sql +++ b/install/sqls/00000-gifts.sql @@ -10,13 +10,10 @@ CREATE TABLE IF NOT EXISTS `coin_vouchers` ( `deleted` tinyint(1) NOT NULL DEFAULT '0' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -DELIMITER $$ CREATE TRIGGER `coinVoucherTokenAutoGen` BEFORE INSERT ON `coin_vouchers` FOR EACH ROW IF NEW.token IS NULL THEN SET NEW.token = SUBSTRING(UPPER(REPLACE(UUID(), "-", "")), 1, 24); -END IF -$$ -DELIMITER ; +END IF; CREATE TABLE IF NOT EXISTS `gifts` ( `id` bigint(20) unsigned NOT NULL, diff --git a/install/sqls/00026-toncoin-fetching.sql b/install/sqls/00026-toncoin-fetching.sql index 6de0c925..d88381ff 100644 --- a/install/sqls/00026-toncoin-fetching.sql +++ b/install/sqls/00026-toncoin-fetching.sql @@ -1,6 +1,6 @@ CREATE TABLE `cryptotransactions` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, - `hash` varchar(45) COLLATE utf8mb4_general_nopad_ci NOT NULL, + `hash` varchar(45) COLLATE utf8mb4_unicode_520_ci NOT NULL, `lt` bigint(20) NOT NULL, PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_nopad_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; diff --git a/openvkctl b/openvkctl index 1ed5f3be..005de9c8 100755 --- a/openvkctl +++ b/openvkctl @@ -12,6 +12,7 @@ $bootstrap = require(__DIR__ . "/../../../chandler/Bootstrap.php"); $bootstrap->ignite(true); $application = new Application(); +$application->add(new CLI\UpgradeCommand()); $application->add(new CLI\RebuildImagesCommand()); $application->add(new CLI\FetchToncoinTransactions()); diff --git a/openvkctl.cmd b/openvkctl.cmd new file mode 100644 index 00000000..926b63e8 --- /dev/null +++ b/openvkctl.cmd @@ -0,0 +1,2 @@ +@echo off +php openvkctl %* \ No newline at end of file