diff --git a/.gitignore b/.gitignore index 2554a0c..3cc6b84 100755 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ caches/last_settings_migrate_run /storage/caches/*.log /storage/caches/*/ /storage/logs/*.log -/frontend/node_modules \ No newline at end of file +/frontend/node_modules +/storage/backups/database/*.sql \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 9270d40..2825b4d 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,9 +14,11 @@ "ealready", "ERRMODE", "fklmnor", + "Ifsnop", "Inno", "jsyaml", "Kutzu", + "Mysqldump", "mythicalcore", "mythicalframework", "Nahh", diff --git a/.vscode/vscode.code-snippets b/.vscode/vscode.code-snippets index e5fa048..e6fceb8 100755 --- a/.vscode/vscode.code-snippets +++ b/.vscode/vscode.code-snippets @@ -119,4 +119,22 @@ ], "description": "Get a specific value from the config file!" }, + "AddRequirements": { + "prefix": "S_AddRequirements", + "scope": "php", + "body": [ + "use MythicalSystemsFramework\\Kernel\\LoggerTypes;", + "use MythicalSystemsFramework\\Kernel\\LoggerLevels;", + "use MythicalSystemsFramework\\Kernel\\Logger;", + "use MythicalSystemsFramework\\Kernel\\Config;", + "use MythicalSystemsFramework\\Encryption\\XChaCha20;", + "use MythicalSystemsFramework\\User\\UserHelper;", + "use MythicalSystemsFramework\\Kernel\\Debugger;", + "use MythicalSystemsFramework\\Mail\\MailService;", + "use MythicalSystemsFramework\\Handlers\\ActivityHandler;", + "use MythicalSystemsFramework\\Managers\\Settings as settings;", + "use MythicalSystemsFramework\\Managers\\ConfigManager as cfg;", + "use MythicalSystemsFramework\\Database\\MySQL;" + ] + } } \ No newline at end of file diff --git a/app/Backup/Backup.php b/app/Backup/Backup.php new file mode 100755 index 0000000..81100a5 --- /dev/null +++ b/app/Backup/Backup.php @@ -0,0 +1,96 @@ +start($path); + return self::registerBackup($path); + } catch (\Exception $e) { + Logger::log(LoggerLevels::CRITICAL, LoggerTypes::BACKUP, "(App/Backup/Backup.php) Failed to take MySQL backup: " . $e->getMessage()); + return 0; + } + } + /** + * Restore the backup from the database. + * @param int $backup_id + * @return void + */ + public static function restore(int $backup_id): void + { + try { + if (!self::doesBackupExist($backup_id)) { + Logger::log(LoggerLevels::CRITICAL, LoggerTypes::BACKUP, "(App/Backup/Backup.php) Failed to restore MySQL backup: Backup does not exist."); + return; + } + $path = self::getBackupPath($backup_id); + + $mysql = new MySQL(); + $db = $mysql->connectPDO(); + + $db->exec('SET FOREIGN_KEY_CHECKS = 0'); + $tables = $db->query('SHOW TABLES')->fetchAll(\PDO::FETCH_COLUMN); + foreach ($tables as $table) { + $db->exec("DROP TABLE IF EXISTS $table"); + } + $db->exec('SET FOREIGN_KEY_CHECKS = 1'); + + $db->exec('SET FOREIGN_KEY_CHECKS = 0'); + $sql = file_get_contents($path); + $db->exec($sql); + $db->exec('SET FOREIGN_KEY_CHECKS = 1'); + unlink($path); + } catch (\Exception $e) { + Logger::log(LoggerLevels::CRITICAL, LoggerTypes::BACKUP, "(App/Backup/Backup.php) Failed to restore MySQL backup: " . $e->getMessage()); + } + } + /** + * Remove a backup from the database / storage. + * + * @param int $backup_id + * @return void + */ + public static function remove(int $backup_id) { + try { + if (!self::doesBackupExist($backup_id)) { + Logger::log(LoggerLevels::CRITICAL, LoggerTypes::BACKUP, "(App/Backup/Backup.php) Failed to remove MySQL backup: Backup does not exist."); + return; + } + $path = self::getBackupPath($backup_id); + unlink($path); + self::markAsDeleted($backup_id); + } catch (\Exception $e) { + Logger::log(LoggerLevels::CRITICAL, LoggerTypes::BACKUP, "(App/Backup/Backup.php) Failed to remove MySQL backup: " . $e->getMessage()); + } + } +} diff --git a/app/Backup/Database.php b/app/Backup/Database.php new file mode 100755 index 0000000..0002285 --- /dev/null +++ b/app/Backup/Database.php @@ -0,0 +1,180 @@ +connectMYSQLI(); + $stmt = $conn->prepare("INSERT INTO framework_backups (backup_path) VALUES (?)"); + $stmt->bind_param('s',$path); + $stmt->execute(); + Logger::log(LoggerLevels::INFO, LoggerTypes::BACKUP, "A new backup has been started."); + return $stmt->insert_id; + } catch (Exception $e) { + Logger::log(LoggerLevels::CRITICAL, LoggerTypes::OTHER, "(App/Backup/Database.php) Failed to register backup: " . $e->getMessage()); + return 0; + } + } + + /** + * Does a backup exist in the database? + * + * @param int $id The id of the backup + * + * @return bool Does the backup exist? + */ + public static function doesBackupExist(int $id): bool + { + try { + $db = new MySQL(); + $conn = $db->connectMYSQLI(); + $stmt = $conn->prepare("SELECT * FROM framework_backups WHERE id = ?"); + $stmt->bind_param('i', $id); + $stmt->execute(); + $result = $stmt->get_result(); + if ($result->num_rows > 0) { + return true; + } else { + return false; + } + } catch (Exception $e) { + Logger::log(LoggerLevels::CRITICAL, LoggerTypes::OTHER, "(App/Backup/Database.php) Failed to check if backup exists: " . $e->getMessage()); + return false; + } + } + + /** + * Mark a backup as completed + * + * @param int $id The id of the backup + * + * @return void + */ + public static function setBackupStatus(int $id, Status|string $status): void + { + try { + $db = new MySQL(); + $conn = $db->connectMYSQLI(); + if (self::doesBackupExist($id)) { + if (MySQL::getLock('framework_backups', $id) == false) { + Logger::log(LoggerLevels::ERROR, LoggerTypes::OTHER, "(App/Backup/Database.php) Failed to mark backup as completed: Failed to get lock."); + return; + } + MySQL::requestLock('framework_backups', $id); + $date = MySQL::getDateLikeMySQL(); + $stmt = $conn->prepare("UPDATE framework_backups SET backup_status = ?, backup_date_end = ? WHERE id = ?"); + $stmt->bind_param('ssi', $status, $date, $id); + $stmt->execute(); + MySQL::requestUnLock('framework_backups', $id); + Logger::log(LoggerLevels::INFO, LoggerTypes::BACKUP, "Backup has been marked as completed."); + } else { + Logger::log(LoggerLevels::CRITICAL, LoggerTypes::OTHER, "(App/Backup/Database.php) Failed to mark backup as completed: Backup does not exist."); + } + } catch (Exception $e) { + Logger::log(LoggerLevels::CRITICAL, LoggerTypes::OTHER, "(App/Backup/Database.php) Failed to mark backup as completed: " . $e->getMessage()); + } + } + /** + * Get the status of a backup + * + * @param int $id + * @return string + */ + public static function getBackupStatus(int $id): string + { + try { + $db = new MySQL(); + $conn = $db->connectMYSQLI(); + $stmt = $conn->prepare("SELECT backup_status FROM framework_backups WHERE id = ?"); + $stmt->bind_param('i', $id); + $stmt->execute(); + $result = $stmt->get_result(); + $status = $result->fetch_assoc(); + return $status['backup_status']; + } catch (Exception $e) { + Logger::log(LoggerLevels::CRITICAL, LoggerTypes::OTHER, "(App/Backup/Database.php) Failed to get backup status: " . $e->getMessage()); + return null; + } + } + + /** + * Get all backups from the database + * + * @return array + */ + public static function getBackups(): array { + try { + $db = new MySQL(); + $conn = $db->connectMYSQLI(); + $result = $conn->query("SELECT * FROM framework_backups WHERE `deleted` = 'false'"); + if ($result->num_rows == 0) { + return []; + } else { + $backups = []; + while ($row = $result->fetch_assoc()) { + $backups[] = $row; + } + return $backups; + } + } catch (Exception $e) { + Logger::log(LoggerLevels::CRITICAL, LoggerTypes::OTHER, "(App/Backup/Database.php) Failed to get backups: " . $e->getMessage()); + return []; + } + } + /** + * Mark a backup as deleted + * + * @return void + */ + public static function markAsDeleted(int $id) : void { + try { + $db = new MySQL(); + $conn = $db->connectMYSQLI(); + $stmt = $conn->prepare("DELETE FROM framework_backups WHERE `framework_backups`.`id` = ?"); + $stmt->bind_param('i', $id); + $stmt->execute(); + } catch (Exception $e) { + Logger::log(LoggerLevels::CRITICAL, LoggerTypes::OTHER, "(App/Backup/Database.php) Failed to mark backup as deleted: " . $e->getMessage()); + } + } + /** + * Get the backup path + * @param int $id + * @return array|bool|null + */ + public static function getBackupPath(int $id) { + try { + $db = new MySQL(); + $conn = $db->connectMYSQLI(); + $stmt = $conn->prepare("SELECT backup_path FROM framework_backups WHERE id = ?"); + $stmt->bind_param("i", $id); + $stmt->execute(); + $result = $stmt->get_result(); + if ($result->num_rows > 0) { + $row = $result->fetch_assoc(); + return $row['backup_path']; + } else { + return null; + } + + } catch (Exception $e) { + Logger::log(LoggerLevels::CRITICAL, LoggerTypes::OTHER,"Failed to get the backup path". $e->getMessage()); + } + } +} diff --git a/app/Backup/Status.php b/app/Backup/Status.php new file mode 100755 index 0000000..6a36b61 --- /dev/null +++ b/app/Backup/Status.php @@ -0,0 +1,10 @@ + 0) { + BackupUtil::setBackupStatus($backup, \MythicalSystemsFramework\Backup\Status::DONE); + echo self::log_info("Backup created successfully!"); + } else { + BackupUtil::setBackupStatus($backup, \MythicalSystemsFramework\Backup\Status::FAILED); + echo self::log_info("Failed to create backup!"); + } + } + + public static function restore(): void + { + self::list(); + echo self::translateColorsCode("Enter the backup ID you want to restore: "); + $backup_id = (int) readline(''); + if ($backup_id <= 0) { + echo self::log_info("Invalid backup ID!"); + return; + } + if (empty($backup_id)) { + echo self::log_info("Backup ID cannot be empty!"); + return; + } + + if (Database::doesBackupExist($backup_id)) { + echo self::log_info("Backup exists!"); + echo self::NewLine(); + echo self::translateColorsCode('&4&lWARNING: &rThis option will wipe the database. &o'); + echo self::translateColorsCode('&4&lWARNING: &rOnly use this function if you know what you are doing &o'); + echo self::translateColorsCode('&4&lWARNING: &rOnce you wipe the database there is no going back! &o'); + echo self::translateColorsCode("&4&lWARNING: &rPlease be careful and don't play around with commands! &o"); + echo self::translateColorsCode('&4&lWARNING: &rThere is no other message then this so keep in mind! &o'); + echo self::translateColorsCode('&4&lWARNING: &rDo you really want to wipe the database? (&ey&r/&en&r): '); + + $confirm = readline(); + if (strtolower($confirm) == 'y') { + BackupUtil::restore($backup_id); + self::create(); + } else { + echo self::log_info('Restore cancelled!'); + } + } else { + echo self::log_info("Backup does not exist!"); + } + } + + public static function delete(): void + { + self::list(); + echo self::translateColorsCode("Enter the backup ID you want to delete: "); + $backup_id = (int) readline(''); + if ($backup_id <= 0) { + echo self::log_info("Invalid backup ID!"); + return; + } + if (empty($backup_id)) { + echo self::log_info("Backup ID cannot be empty!"); + return; + } + + if (Database::doesBackupExist($backup_id)) { + BackupUtil::remove($backup_id); + + } else { + echo self::log_info("Backup does not exist!"); + } + } + + public static function list(): void + { + $backups = BackupUtil::getBackups(); + foreach ($backups as $backup) { + echo self::NewLine(); + echo self::log_info("------ &7[&cBackup Info&7] ------"); + echo self::NewLine(); + echo self::log_info("Backup ID: &c" . $backup['id']); + echo self::log_info("Backup Status: &c" . $backup['backup_status']); + echo self::log_info("Backup Date: &c" . $backup['backup_date_end']); + echo self::NewLine(); + echo self::log_info("------ (&c" . $backup['id'] . "&7/&c" . count($backups) . "&7) ------"); + } } } diff --git a/app/Config/Handler.php b/app/Config/Handler.php deleted file mode 100755 index 05418c9..0000000 --- a/app/Config/Handler.php +++ /dev/null @@ -1,7 +0,0 @@ -connectMYSQLI(); - $stmt = $conn->prepare("UPDATE ? SET `locked` = 'true' WHERE `id` = ?;"); - $stmt->bind_param('si', $table, $id); + $stmt = $conn->prepare("UPDATE `$table` SET `locked` = 'true' WHERE `id` = ?;"); + $stmt->bind_param('s', $id); $stmt->execute(); $stmt->close(); } catch (\Exception $e) { @@ -137,8 +137,8 @@ public static function requestUnLock(string $table, string $id): void } $mysqli = new MySQL(); $conn = $mysqli->connectMYSQLI(); - $stmt = $conn->prepare("UPDATE ? SET `locked` = 'false' WHERE `id` = ?;"); - $stmt->bind_param('si', $table, $id); + $stmt = $conn->prepare("UPDATE `$table` SET `locked` = 'false' WHERE `id` = ?;"); + $stmt->bind_param('s', $id); $stmt->execute(); $stmt->close(); } catch (\Exception $e) { @@ -158,21 +158,24 @@ public static function getLock(string $table, string $id): bool { try { if (self::doesTableExist($table) === false) { - return false; + return false; } $mysqli = new MySQL(); $conn = $mysqli->connectMYSQLI(); - $stmt = $conn->prepare('SELECT `locked` FROM ? WHERE `id` = ?;'); - $stmt->bind_param('si', $table, $id); + $stmt = $conn->prepare('SELECT `locked` FROM `' . $table . '` WHERE `id` = ?;'); + $stmt->bind_param('s', $id); $stmt->execute(); + $result = $stmt->get_result(); + $row = $result->fetch_assoc(); $stmt->close(); - return $stmt->get_result()->fetch_assoc()['locked']; + return $row['locked']; } catch (\Exception $e) { Logger::log(LoggerLevels::CRITICAL, LoggerTypes::DATABASE, 'Failed to get lock status: ' . $e); return false; } + } /** @@ -292,4 +295,12 @@ public static function migrate(bool $isCli = false): void } } } + /** + * Get the date that can be used in MySQL. + * + * @return string + */ + public static function getDateLikeMySQL() : string { + return date('Y-m-d H:i:s'); + } } diff --git a/app/Kernel/Logger.php b/app/Kernel/Logger.php index be44fdf..34decba 100755 --- a/app/Kernel/Logger.php +++ b/app/Kernel/Logger.php @@ -10,7 +10,7 @@ class Logger * Log something inside the kernel framework_logs. * * @param LoggerTypes|string $level (INFO, WARNING, ERROR, CRITICAL, OTHER) - * @param LoggerLevels|string $type (CORE, DATABASE, PLUGIN, LOG, OTHER, LANGUAGE) + * @param LoggerLevels|string $type (CORE, DATABASE, PLUGIN, LOG, OTHER, LANGUAGE, BACKUP) * @param string $message The message you want to log * * @return int The log id! @@ -104,7 +104,7 @@ public static function getAllSortedById(): array * Get all framework_logs sorted by date in descending order. * * @param LoggerTypes|string $level (INFO, WARNING, ERROR, CRITICAL, OTHER) - * @param LoggerLevels|string $type (CORE, DATABASE, PLUGIN, LOG, OTHER, LANGUAGE) + * @param LoggerLevels|string $type (CORE, DATABASE, PLUGIN, LOG, OTHER, LANGUAGE,BACKUP) * @param int $limit The amount of logs you want to get (15 by default) * * @return array|null Returns the logs in an array diff --git a/app/Kernel/LoggerTypes.php b/app/Kernel/LoggerTypes.php index be5b68b..a4215cd 100755 --- a/app/Kernel/LoggerTypes.php +++ b/app/Kernel/LoggerTypes.php @@ -10,6 +10,7 @@ interface LoggerTypes public const PLUGIN = 'PLUGIN'; public const LOG = 'LOG'; public const LANGUAGE = 'LANGUAGE'; + public const BACKUP = 'BACKUP'; // Other public const OTHER = 'OTHER'; } diff --git a/app/User/UserDataHandler.php b/app/User/UserDataHandler.php index 0418e04..9bcbeb5 100755 --- a/app/User/UserDataHandler.php +++ b/app/User/UserDataHandler.php @@ -18,6 +18,7 @@ use MythicalSystemsFramework\Managers\SnowFlakeManager; use MythicalSystemsFramework\Encryption\XChaCha20 as enc; + class UserDataHandler { /** diff --git a/composer.json b/composer.json index 3b2e334..b933a98 100755 --- a/composer.json +++ b/composer.json @@ -9,7 +9,8 @@ "ext-json": "*", "ext-mbstring": "*", "gravatarphp/gravatar": "^1.0", - "twig/twig": "^3.10" + "twig/twig": "^3.10", + "ifsnop/mysqldump-php": "^2.12" }, "license": "MIT", "minimum-stability": "stable", diff --git a/composer.lock b/composer.lock index 3de11a6..ddd1767 100755 --- 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": "d0d736cc77e2510ef863bc93d4d6a57b", + "content-hash": "152943a06a84dd840a3d5f216491caed", "packages": [ { "name": "gravatarphp/gravatar", @@ -64,6 +64,65 @@ }, "time": "2016-06-25T11:20:11+00:00" }, + { + "name": "ifsnop/mysqldump-php", + "version": "v2.12", + "source": { + "type": "git", + "url": "https://github.com/ifsnop/mysqldump-php.git", + "reference": "2d3a43fc0c49f23bf7dee392b0dd1f8c799f89d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ifsnop/mysqldump-php/zipball/2d3a43fc0c49f23bf7dee392b0dd1f8c799f89d3", + "reference": "2d3a43fc0c49f23bf7dee392b0dd1f8c799f89d3", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "4.8.36", + "squizlabs/php_codesniffer": "1.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ifsnop\\": "src/Ifsnop/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-or-later" + ], + "authors": [ + { + "name": "Diego Torres", + "homepage": "https://github.com/ifsnop", + "role": "Developer" + } + ], + "description": "PHP version of mysqldump cli that comes with MySQL", + "homepage": "https://github.com/ifsnop/mysqldump-php", + "keywords": [ + "PHP7", + "database", + "hhvm", + "mariadb", + "mysql", + "mysql-backup", + "mysqldump", + "pdo", + "php", + "php5", + "sql" + ], + "support": { + "issues": "https://github.com/ifsnop/mysqldump-php/issues", + "source": "https://github.com/ifsnop/mysqldump-php/tree/v2.12" + }, + "time": "2023-04-12T07:43:14+00:00" + }, { "name": "mythicalsystems/core", "version": "1.0.0.10", diff --git a/storage/backups/.gitkeep b/storage/backups/.gitkeep new file mode 100755 index 0000000..e69de29 diff --git a/storage/backups/database/.gitkeep b/storage/backups/database/.gitkeep new file mode 100755 index 0000000..e69de29 diff --git a/storage/migrate/database/20.sql b/storage/migrate/database/20.sql new file mode 100755 index 0000000..248f772 --- /dev/null +++ b/storage/migrate/database/20.sql @@ -0,0 +1 @@ +CREATE TABLE `framework_backups` (`id` INT NOT NULL AUTO_INCREMENT , `backup_name` TEXT NOT NULL , `backup_type` ENUM('database','app','all') NOT NULL , `backup_path` TEXT NOT NULL , `backup_status` ENUM('IN_PROGRESS','DONE','FAILED') NOT NULL DEFAULT 'IN_PROGRESS' , `backup_date_start` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP , `backup_date_end` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP , PRIMARY KEY (`id`)) ENGINE = InnoDB; \ No newline at end of file diff --git a/storage/migrate/database/21.sql b/storage/migrate/database/21.sql new file mode 100755 index 0000000..b219151 --- /dev/null +++ b/storage/migrate/database/21.sql @@ -0,0 +1 @@ +ALTER TABLE `framework_logs` CHANGE `l_type` `l_type` ENUM('OTHER','CORE','DATABASE','PLUGIN','LOG','LANGUAGE','BACKUP') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'OTHER'; \ No newline at end of file diff --git a/storage/migrate/database/22.sql b/storage/migrate/database/22.sql new file mode 100755 index 0000000..ccf904d --- /dev/null +++ b/storage/migrate/database/22.sql @@ -0,0 +1 @@ +ALTER TABLE `framework_backups` ADD `deleted` ENUM('false','true') NOT NULL DEFAULT 'false' AFTER `backup_status`, ADD `locked` ENUM('false','true') NOT NULL DEFAULT 'false' AFTER `deleted`; \ No newline at end of file diff --git a/storage/migrate/database/23.sql b/storage/migrate/database/23.sql new file mode 100755 index 0000000..435c34c --- /dev/null +++ b/storage/migrate/database/23.sql @@ -0,0 +1 @@ +ALTER TABLE `framework_backups` DROP `backup_type`; \ No newline at end of file diff --git a/storage/migrate/database/24.sql b/storage/migrate/database/24.sql new file mode 100755 index 0000000..b52fbfc --- /dev/null +++ b/storage/migrate/database/24.sql @@ -0,0 +1 @@ +ALTER TABLE `framework_backups` DROP `backup_name`; \ No newline at end of file diff --git a/storage/settings.json b/storage/settings.json index 73a91da..3f36b5c 100755 --- a/storage/settings.json +++ b/storage/settings.json @@ -1,5 +1,5 @@ { - "__last_updated": "2024-08-19 21:10:55", + "__last_updated": "2024-08-20 22:05:05", "framework": { "version": "1.0.1", "branch": "develop", @@ -15,7 +15,7 @@ }, "encryption": { "method": "MythicalCore", - "key": "0pDYJ1PoL6Mv1rLmjQHPikUgPIbuCCVFXUQDUXwwPjw=" + "key": "7HA3jhOQEoicCD8krFPl8kzdXCmWTkRfH3q0BjkeiRQ=" }, "app": { "maintenance": "false" diff --git a/storage/trash/.gitkeep b/storage/trash/.gitkeep new file mode 100755 index 0000000..e69de29