From d9b4c73baac5618f319ca896d227bdb75b0bed36 Mon Sep 17 00:00:00 2001 From: "Elias F." Date: Wed, 14 Jan 2026 23:04:53 +0100 Subject: [PATCH] Initial commit --- application/config/config.development.php | 4 +- application/controller/DatabaseController.php | 21 +- application/controller/DbUserController.php | 22 +- application/controller/SqlController.php | 5 +- application/controller/TableController.php | 161 ++- application/core/View.php | 60 +- application/model/DatabaseModel.php | 102 +- application/model/DbUserModel.php | 202 ++++ application/model/TableModel.php | 165 ++- .../view/_templates/dbmanager_footer.php | 105 ++ .../view/_templates/dbmanager_header.php | 107 ++ application/view/_templates/header.php | 5 + application/view/database/index.php | 86 ++ application/view/database/show.php | 76 ++ application/view/dbuser/create.php | 68 ++ application/view/dbuser/edit.php | 92 ++ application/view/dbuser/index.php | 101 ++ application/view/dbuser/privileges.php | 37 + application/view/sql/index.php | 282 ++++++ application/view/table/add_column.php | 68 ++ application/view/table/create.php | 122 +++ application/view/table/show.php | 435 ++++++++ application/view/table/structure.php | 112 +++ public/css/dbmanager.css | 945 ++++++++++++++++++ public/js/dbmanager.js | 389 +++++++ 25 files changed, 3742 insertions(+), 30 deletions(-) create mode 100644 application/model/DbUserModel.php create mode 100644 application/view/_templates/dbmanager_footer.php create mode 100644 application/view/_templates/dbmanager_header.php create mode 100644 application/view/database/index.php create mode 100644 application/view/database/show.php create mode 100644 application/view/dbuser/create.php create mode 100644 application/view/dbuser/edit.php create mode 100644 application/view/dbuser/index.php create mode 100644 application/view/dbuser/privileges.php create mode 100644 application/view/sql/index.php create mode 100644 application/view/table/add_column.php create mode 100644 application/view/table/create.php create mode 100644 application/view/table/show.php create mode 100644 application/view/table/structure.php create mode 100644 public/css/dbmanager.css create mode 100644 public/js/dbmanager.js diff --git a/application/config/config.development.php b/application/config/config.development.php index e667dde..71ce7ef 100644 --- a/application/config/config.development.php +++ b/application/config/config.development.php @@ -72,8 +72,8 @@ return array( /** * Configuration for: Google reCAPTCHA v2 */ - 'RECAPTCHA_SITE_KEY' => 'recaptcha-site-key', - 'RECAPTCHA_SECRET_KEY' => 'recaptcha-secret-key', + 'RECAPTCHA_SITE_KEY' => '6Lfl-EcsAAAAAG9svnagihb5y6HCNK2cd5W9jQm-', + 'RECAPTCHA_SECRET_KEY' => '6Lfl-EcsAAAAADusuMYTprgTZ42BVIWPsF_jVtk6', /** * Configuration for: Cookies * 1209600 seconds = 2 weeks diff --git a/application/controller/DatabaseController.php b/application/controller/DatabaseController.php index 5c76c30..bb078c4 100644 --- a/application/controller/DatabaseController.php +++ b/application/controller/DatabaseController.php @@ -14,8 +14,9 @@ class DatabaseController extends Controller { parent::__construct(); - // Only logged-in users can access the database manager + // Only admin users can access the database manager Auth::checkAuthentication(); + Auth::checkAdminAuthentication(); } /** @@ -23,7 +24,7 @@ class DatabaseController extends Controller */ public function index() { - $this->View->render('database/index', array( + $this->View->renderDbManager('database/index', array( 'databases' => DatabaseModel::getAllDatabases(), 'current_db' => Config::get('DB_NAME') )); @@ -39,7 +40,7 @@ class DatabaseController extends Controller $database_name = Config::get('DB_NAME'); } - $this->View->render('database/show', array( + $this->View->renderDbManager('database/show', array( 'tables' => DatabaseModel::getTablesInDatabase($database_name), 'database_name' => $database_name, 'table_info' => DatabaseModel::getTableDetails($database_name) @@ -133,12 +134,24 @@ class DatabaseController extends Controller ]); } + /** + * Export database as raw SQL text + * @param string $database_name + */ + public function export($database_name) + { + header('Content-Type: text/plain; charset=utf-8'); + header('Content-Disposition: inline; filename="' . $database_name . '.sql"'); + + echo DatabaseModel::exportDatabase($database_name); + } + /** * Check if the request is an AJAX request */ private function isAjaxRequest() { - return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && + return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'; } } \ No newline at end of file diff --git a/application/controller/DbUserController.php b/application/controller/DbUserController.php index ccbc07f..896fb38 100644 --- a/application/controller/DbUserController.php +++ b/application/controller/DbUserController.php @@ -24,7 +24,7 @@ class DbUserController extends Controller */ public function index() { - $this->View->render('dbuser/index', array( + $this->View->renderDbManager('dbuser/index', array( 'users' => DbUserModel::getAllUsers(), 'current_user' => Config::get('DB_USER') )); @@ -39,11 +39,15 @@ class DbUserController extends Controller $username = Request::post('username'); $password = Request::post('password'); $host = Request::post('host'); - + $privileges = Request::post('privileges'); + if ($this->isAjaxRequest()) { header('Content-Type: application/json'); - + if (DbUserModel::createUser($username, $password, $host)) { + if (!empty($privileges)) { + DbUserModel::updateUserPrivileges($username, $host, $privileges); + } echo json_encode([ 'success' => true, 'message' => 'User created successfully', @@ -57,8 +61,11 @@ class DbUserController extends Controller } return; } - + if (DbUserModel::createUser($username, $password, $host)) { + if (!empty($privileges)) { + DbUserModel::updateUserPrivileges($username, $host, $privileges); + } Redirect::to('dbuser'); } else { Redirect::to('dbuser'); @@ -66,8 +73,7 @@ class DbUserController extends Controller return; } - // Show create user form - $this->View->render('dbuser/create'); + $this->View->renderDbManager('dbuser/create'); } /** @@ -127,7 +133,7 @@ class DbUserController extends Controller } // Show edit user form - $this->View->render('dbuser/edit', array( + $this->View->renderDbManager('dbuser/edit', array( 'user' => DbUserModel::getUserDetails($username, $host), 'privileges' => DbUserModel::getUserPrivileges($username, $host), 'databases' => DatabaseModel::getAllDatabases() @@ -184,7 +190,7 @@ class DbUserController extends Controller */ public function privileges($username, $host) { - $this->View->render('dbuser/privileges', array( + $this->View->renderDbManager('dbuser/privileges', array( 'user' => DbUserModel::getUserDetails($username, $host), 'privileges' => DbUserModel::getUserPrivileges($username, $host) )); diff --git a/application/controller/SqlController.php b/application/controller/SqlController.php index 4e60191..5bb0acb 100644 --- a/application/controller/SqlController.php +++ b/application/controller/SqlController.php @@ -14,8 +14,9 @@ class SqlController extends Controller { parent::__construct(); - // Only logged-in users can access the SQL console + // Only admin users can access the SQL console Auth::checkAuthentication(); + Auth::checkAdminAuthentication(); } /** @@ -28,7 +29,7 @@ class SqlController extends Controller $database_name = Config::get('DB_NAME'); } - $this->View->render('sql/index', array( + $this->View->renderDbManager('sql/index', array( 'database_name' => $database_name, 'databases' => DatabaseModel::getAllDatabases(), 'history' => SqlModel::getQueryHistory(Session::get('user_id')) diff --git a/application/controller/TableController.php b/application/controller/TableController.php index def585f..0aac645 100644 --- a/application/controller/TableController.php +++ b/application/controller/TableController.php @@ -14,8 +14,9 @@ class TableController extends Controller { parent::__construct(); - // Only logged-in users can access the table manager + // Only admin users can access the table manager Auth::checkAuthentication(); + Auth::checkAdminAuthentication(); } /** @@ -38,7 +39,7 @@ class TableController extends Controller $page = (int)$page; $per_page = 20; - $this->View->render('table/show', array( + $this->View->renderDbManager('table/show', array( 'database_name' => $database_name, 'table_name' => $table_name, 'columns' => TableModel::getTableColumns($database_name, $table_name), @@ -91,7 +92,7 @@ class TableController extends Controller } // Show create table form - $this->View->render('table/create', array( + $this->View->renderDbManager('table/create', array( 'database_name' => $database_name )); } @@ -112,7 +113,7 @@ class TableController extends Controller return; } - $this->View->render('table/structure', array( + $this->View->renderDbManager('table/structure', array( 'database_name' => $database_name, 'table_name' => $table_name, 'columns' => TableModel::getTableColumns($database_name, $table_name), @@ -172,7 +173,7 @@ class TableController extends Controller } // Show add column form - $this->View->render('table/add_column', array( + $this->View->renderDbManager('table/add_column', array( 'database_name' => $database_name, 'table_name' => $table_name )); @@ -237,12 +238,160 @@ class TableController extends Controller Redirect::to('database/show/' . urlencode($database_name)); } + /** + * Update a row in the table (AJAX) + * @param string $database_name + * @param string $table_name + */ + public function updateRow($database_name = null, $table_name = null) + { + if (!$database_name || !$table_name) { + if ($this->isAjaxRequest()) { + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'message' => 'Invalid parameters']); + return; + } + Redirect::to('database/index'); + return; + } + + $pk_value = Request::post('pk_value'); + $data = Request::post('data'); + + if ($this->isAjaxRequest()) { + header('Content-Type: application/json'); + + if (!$pk_value || !$data) { + echo json_encode(['success' => false, 'message' => 'Missing required data']); + return; + } + + if (TableModel::updateRow($database_name, $table_name, $pk_value, $data)) { + echo json_encode([ + 'success' => true, + 'message' => 'Row updated successfully' + ]); + } else { + echo json_encode([ + 'success' => false, + 'message' => 'Failed to update row' + ]); + } + return; + } + + Redirect::to('table/show/' . urlencode($database_name) . '/' . urlencode($table_name)); + } + + /** + * Delete a row from the table (AJAX) + * @param string $database_name + * @param string $table_name + */ + public function deleteRow($database_name = null, $table_name = null) + { + if (!$database_name || !$table_name) { + if ($this->isAjaxRequest()) { + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'message' => 'Invalid parameters']); + return; + } + Redirect::to('database/index'); + return; + } + + $pk_value = Request::post('pk_value'); + + if ($this->isAjaxRequest()) { + header('Content-Type: application/json'); + + if (!$pk_value) { + echo json_encode(['success' => false, 'message' => 'Missing primary key value']); + return; + } + + if (TableModel::deleteRow($database_name, $table_name, $pk_value)) { + echo json_encode([ + 'success' => true, + 'message' => 'Row deleted successfully' + ]); + } else { + echo json_encode([ + 'success' => false, + 'message' => 'Failed to delete row' + ]); + } + return; + } + + Redirect::to('table/show/' . urlencode($database_name) . '/' . urlencode($table_name)); + } + + /** + * Insert a new row into the table (AJAX) + * @param string $database_name + * @param string $table_name + */ + public function insertRow($database_name = null, $table_name = null) + { + if (!$database_name || !$table_name) { + if ($this->isAjaxRequest()) { + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'message' => 'Invalid parameters']); + return; + } + Redirect::to('database/index'); + return; + } + + $data = Request::post('data'); + + if ($this->isAjaxRequest()) { + header('Content-Type: application/json'); + + if (!$data) { + echo json_encode(['success' => false, 'message' => 'Missing row data']); + return; + } + + $insertId = TableModel::insertRow($database_name, $table_name, $data); + if ($insertId !== false) { + echo json_encode([ + 'success' => true, + 'message' => 'Row inserted successfully', + 'insert_id' => $insertId + ]); + } else { + echo json_encode([ + 'success' => false, + 'message' => 'Failed to insert row' + ]); + } + return; + } + + Redirect::to('table/show/' . urlencode($database_name) . '/' . urlencode($table_name)); + } + /** * Check if the request is an AJAX request */ private function isAjaxRequest() { - return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && + return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'; } + + /** + * Export table as raw SQL text + * @param string $database_name + * @param string $table_name + */ + public function export($database_name, $table_name) + { + header('Content-Type: text/plain; charset=utf-8'); + header('Content-Disposition: inline; filename="' . $table_name . '.sql"'); + + echo DatabaseModel::exportTable($database_name, $table_name); + } } \ No newline at end of file diff --git a/application/core/View.php b/application/core/View.php index 962466b..cabc254 100644 --- a/application/core/View.php +++ b/application/core/View.php @@ -25,7 +25,28 @@ class View /* Note properties */ public $messages; public $other_user; - + + /* Group properties */ + public $groups; + + /* Database Manager properties */ + public $databases; + public $database_name; + public $current_db; + public $tables; + public $table_name; + public $table_info; + public $columns; + public $rows; + public $indexes; + public $total_rows; + public $current_page; + public $per_page; + public $history; + public $user; + public $privileges; + public $current_user; + /** * Static property to track if header has been rendered */ @@ -115,6 +136,24 @@ class View header("Content-Type: application/json"); echo json_encode($data); } + + /** + * Render a view using the database manager layout + * @param string $filename Path of the to-be-rendered view + * @param array $data Data to be used in the view + */ + public function renderDbManager($filename, $data = null) + { + if ($data) { + foreach ($data as $key => $value) { + $this->{$key} = $value; + } + } + + require Config::get('PATH_VIEW') . '_templates/dbmanager_header.php'; + require Config::get('PATH_VIEW') . $filename . '.php'; + require Config::get('PATH_VIEW') . '_templates/dbmanager_footer.php'; + } /** * Reset header render flag at start of request @@ -206,6 +245,25 @@ class View return false; } + /** + * Checks if the passed array of controllers contains the currently active controller. + * Useful for navigation items that span multiple controllers (e.g., database manager). + * + * @param string $filename + * @param array $controllers Array of controller names to check + * + * @return bool Shows if any of the controllers is active + */ + public static function checkForActiveControllers($filename, $controllers) + { + foreach ($controllers as $controller) { + if (self::checkForActiveController($filename, $controller)) { + return true; + } + } + return false; + } + /** * Converts characters to HTML entities * This is important to avoid XSS attacks, and attempts to inject malicious code in your page. diff --git a/application/model/DatabaseModel.php b/application/model/DatabaseModel.php index 82ab616..4cd9f5e 100644 --- a/application/model/DatabaseModel.php +++ b/application/model/DatabaseModel.php @@ -21,7 +21,6 @@ class DatabaseModel $databases = $query->fetchAll(PDO::FETCH_COLUMN); - // Filter out system databases $system_dbs = ['information_schema', 'performance_schema', 'mysql', 'sys']; return array_diff($databases, $system_dbs); } @@ -35,7 +34,7 @@ class DatabaseModel { $database = DatabaseFactory::getFactory()->getConnection(); - $sql = "SHOW TABLES FROM " . $database_name; + $sql = "SHOW TABLES FROM `" . $database_name . "`"; $query = $database->prepare($sql); $query->execute(); @@ -54,7 +53,7 @@ class DatabaseModel $table_details = array(); foreach ($tables as $table) { - $sql = "SHOW TABLE STATUS FROM " . $database_name . " LIKE :table_name"; + $sql = "SHOW TABLE STATUS FROM `" . $database_name . "` LIKE :table_name"; $query = $database->prepare($sql); $query->execute(array(':table_name' => $table)); @@ -87,7 +86,7 @@ class DatabaseModel $tables = self::getTablesInDatabase($database_name); foreach ($tables as $table) { - $sql = "DESCRIBE " . $database_name . "." . $table; + $sql = "DESCRIBE `" . $database_name . "`.`" . $table . "`"; $query = $database->prepare($sql); $query->execute(); @@ -152,13 +151,106 @@ class DatabaseModel { $database = DatabaseFactory::getFactory()->getConnection(); - $sql = "SHOW COLUMNS FROM " . $database_name . "." . $table_name; + $sql = "SHOW COLUMNS FROM `" . $database_name . "`.`" . $table_name . "`"; $query = $database->prepare($sql); $query->execute(); return $query->fetchAll(PDO::FETCH_ASSOC); } + /** + * Export database as SQL dump + * @param string $database_name + * @return string + */ + public static function exportDatabase($database_name) + { + $database = DatabaseFactory::getFactory()->getConnection(); + $output = "-- Database Export: " . $database_name . "\n"; + $output .= "-- Generated: " . date('Y-m-d H:i:s') . "\n\n"; + $output .= "SET FOREIGN_KEY_CHECKS=0;\n"; + $output .= "SET SQL_MODE = \"NO_AUTO_VALUE_ON_ZERO\";\n\n"; + + $tables = self::getTablesInDatabase($database_name); + + foreach ($tables as $table) { + $sql = "SHOW CREATE TABLE `" . $database_name . "`.`" . $table . "`"; + $query = $database->prepare($sql); + $query->execute(); + $row = $query->fetch(PDO::FETCH_NUM); + + $output .= "DROP TABLE IF EXISTS `" . $table . "`;\n"; + $output .= $row[1] . ";\n\n"; + + $sql = "SELECT * FROM `" . $database_name . "`.`" . $table . "`"; + $query = $database->prepare($sql); + $query->execute(); + $rows = $query->fetchAll(PDO::FETCH_ASSOC); + + if (!empty($rows)) { + foreach ($rows as $dataRow) { + $columns = array_keys($dataRow); + $values = array_map(function($val) use ($database) { + if ($val === null) { + return 'NULL'; + } + return $database->quote($val); + }, array_values($dataRow)); + + $output .= "INSERT INTO `" . $table . "` (`" . implode("`, `", $columns) . "`) VALUES (" . implode(", ", $values) . ");\n"; + } + $output .= "\n"; + } + } + + $output .= "SET FOREIGN_KEY_CHECKS=1;\n"; + return $output; + } + + /** + * Export single table as SQL dump + * @param string $database_name + * @param string $table_name + * @return string + */ + public static function exportTable($database_name, $table_name) + { + $database = DatabaseFactory::getFactory()->getConnection(); + $output = "-- Table Export: " . $table_name . " from " . $database_name . "\n"; + $output .= "-- Generated: " . date('Y-m-d H:i:s') . "\n\n"; + $output .= "SET FOREIGN_KEY_CHECKS=0;\n\n"; + + $sql = "SHOW CREATE TABLE `" . $database_name . "`.`" . $table_name . "`"; + $query = $database->prepare($sql); + $query->execute(); + $row = $query->fetch(PDO::FETCH_NUM); + + $output .= "DROP TABLE IF EXISTS `" . $table_name . "`;\n"; + $output .= $row[1] . ";\n\n"; + + $sql = "SELECT * FROM `" . $database_name . "`.`" . $table_name . "`"; + $query = $database->prepare($sql); + $query->execute(); + $rows = $query->fetchAll(PDO::FETCH_ASSOC); + + if (!empty($rows)) { + foreach ($rows as $dataRow) { + $columns = array_keys($dataRow); + $values = array_map(function($val) use ($database) { + if ($val === null) { + return 'NULL'; + } + return $database->quote($val); + }, array_values($dataRow)); + + $output .= "INSERT INTO `" . $table_name . "` (`" . implode("`, `", $columns) . "`) VALUES (" . implode(", ", $values) . ");\n"; + } + } + + $output .= "\nSET FOREIGN_KEY_CHECKS=1;\n"; + return $output; + } + /** * Format bytes to human readable format * @param int $bytes diff --git a/application/model/DbUserModel.php b/application/model/DbUserModel.php new file mode 100644 index 0000000..4583b22 --- /dev/null +++ b/application/model/DbUserModel.php @@ -0,0 +1,202 @@ +getConnection(); + + try { + $sql = "SELECT User, Host FROM mysql.user ORDER BY User, Host"; + $query = $database->prepare($sql); + $query->execute(); + + return $query->fetchAll(PDO::FETCH_OBJ); + } catch (PDOException $e) { + return array(); + } + } + + /** + * Get user details + * @param string $username + * @param string $host + * @return object|null + */ + public static function getUserDetails($username, $host) + { + $database = DatabaseFactory::getFactory()->getConnection(); + + try { + $sql = "SELECT * FROM mysql.user WHERE User = :username AND Host = :host"; + $query = $database->prepare($sql); + $query->execute(array(':username' => $username, ':host' => $host)); + + return $query->fetch(PDO::FETCH_OBJ); + } catch (PDOException $e) { + return null; + } + } + + /** + * Get user privileges + * @param string $username + * @param string $host + * @return array + */ + public static function getUserPrivileges($username, $host) + { + $database = DatabaseFactory::getFactory()->getConnection(); + + try { + // Escape username and host for SHOW GRANTS + $sql = "SHOW GRANTS FOR " . $database->quote($username) . "@" . $database->quote($host); + $query = $database->prepare($sql); + $query->execute(); + + $grants = array(); + while ($row = $query->fetch(PDO::FETCH_NUM)) { + $grants[] = $row[0]; + } + + return $grants; + } catch (PDOException $e) { + return array(); + } + } + + /** + * Create a new database user + * @param string $username + * @param string $password + * @param string $host + * @return bool + */ + public static function createUser($username, $password, $host) + { + if (!self::validateUsername($username) || empty($password) || empty($host)) { + return false; + } + + $database = DatabaseFactory::getFactory()->getConnection(); + + try { + $sql = "CREATE USER " . $database->quote($username) . "@" . $database->quote($host) . + " IDENTIFIED BY " . $database->quote($password); + $database->exec($sql); + + return true; + } catch (PDOException $e) { + return false; + } + } + + /** + * Update user password + * @param string $username + * @param string $host + * @param string $password + * @return bool + */ + public static function updateUserPassword($username, $host, $password) + { + if (empty($password)) { + return false; + } + + $database = DatabaseFactory::getFactory()->getConnection(); + + try { + $sql = "ALTER USER " . $database->quote($username) . "@" . $database->quote($host) . + " IDENTIFIED BY " . $database->quote($password); + $database->exec($sql); + + return true; + } catch (PDOException $e) { + return false; + } + } + + /** + * Update user privileges + * @param string $username + * @param string $host + * @param array $privileges + * @return bool + */ + public static function updateUserPrivileges($username, $host, $privileges) + { + $database = DatabaseFactory::getFactory()->getConnection(); + + try { + $sql = "REVOKE ALL PRIVILEGES, GRANT OPTION FROM " . + $database->quote($username) . "@" . $database->quote($host); + $database->exec($sql); + + if (!empty($privileges) && is_array($privileges)) { + if (in_array('ALL PRIVILEGES', $privileges)) { + $sql = "GRANT ALL PRIVILEGES ON *.* TO " . + $database->quote($username) . "@" . $database->quote($host); + $database->exec($sql); + } else { + $valid_privs = array('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'CREATE', 'DROP', 'ALTER', 'INDEX', + 'REFERENCES', 'CREATE TEMPORARY TABLES', 'LOCK TABLES', 'EXECUTE', + 'CREATE VIEW', 'SHOW VIEW', 'CREATE ROUTINE', 'ALTER ROUTINE', 'EVENT', 'TRIGGER'); + $privileges = array_intersect($privileges, $valid_privs); + + if (!empty($privileges)) { + $priv_string = implode(', ', $privileges); + $sql = "GRANT " . $priv_string . " ON *.* TO " . + $database->quote($username) . "@" . $database->quote($host); + $database->exec($sql); + } + } + } + + $database->exec("FLUSH PRIVILEGES"); + + return true; + } catch (PDOException $e) { + return false; + } + } + + /** + * Delete a database user + * @param string $username + * @param string $host + * @return bool + */ + public static function deleteUser($username, $host) + { + $database = DatabaseFactory::getFactory()->getConnection(); + + try { + $sql = "DROP USER " . $database->quote($username) . "@" . $database->quote($host); + $database->exec($sql); + + return true; + } catch (PDOException $e) { + return false; + } + } + + /** + * Validate username format + * @param string $username + * @return bool + */ + private static function validateUsername($username) + { + return !empty($username) && preg_match('/^[a-zA-Z0-9_]+$/', $username); + } +} diff --git a/application/model/TableModel.php b/application/model/TableModel.php index ca1a628..2f849dd 100644 --- a/application/model/TableModel.php +++ b/application/model/TableModel.php @@ -131,7 +131,6 @@ class TableModel $database = DatabaseFactory::getFactory()->getConnection(); - // Build column definitions $column_definitions = array(); foreach ($columns as $column) { $definition = "`" . $column['name'] . "` " . $column['type']; @@ -151,7 +150,6 @@ class TableModel $column_definitions[] = $definition; } - // Handle primary key foreach ($columns as $column) { if (isset($column['key']) && $column['key'] === 'PRI') { $column_definitions[] = "PRIMARY KEY (`" . $column['name'] . "`)"; @@ -260,6 +258,169 @@ class TableModel } } + /** + * Get the primary key column name for a table + * @param string $database_name + * @param string $table_name + * @return string|null + */ + public static function getPrimaryKeyColumn($database_name, $table_name) + { + $columns = self::getTableColumns($database_name, $table_name); + foreach ($columns as $column) { + if ($column['Key'] === 'PRI') { + return $column['Field']; + } + } + return null; + } + + /** + * Get a single row by primary key + * @param string $database_name + * @param string $table_name + * @param mixed $pk_value + * @return array|null + */ + public static function getRow($database_name, $table_name, $pk_value) + { + $pk_column = self::getPrimaryKeyColumn($database_name, $table_name); + if (!$pk_column) { + return null; + } + + $database = DatabaseFactory::getFactory()->getConnection(); + + $sql = "SELECT * FROM `" . $database_name . "`.`" . $table_name . "` WHERE `" . $pk_column . "` = :pk_value LIMIT 1"; + $query = $database->prepare($sql); + $query->execute(array(':pk_value' => $pk_value)); + + return $query->fetch(PDO::FETCH_ASSOC); + } + + /** + * Update a row in the table + * @param string $database_name + * @param string $table_name + * @param mixed $pk_value + * @param array $data - associative array of column => value + * @return bool + */ + public static function updateRow($database_name, $table_name, $pk_value, $data) + { + if (!$database_name || !$table_name || !$pk_value || empty($data)) { + return false; + } + + $pk_column = self::getPrimaryKeyColumn($database_name, $table_name); + if (!$pk_column) { + return false; + } + + $database = DatabaseFactory::getFactory()->getConnection(); + + $set_parts = array(); + $params = array(); + $i = 0; + foreach ($data as $column => $value) { + if ($column === $pk_column) { + continue; + } + $param_name = ':param_' . $i; + $set_parts[] = "`" . $column . "` = " . $param_name; + $params[$param_name] = $value === '' ? null : $value; + $i++; + } + + if (empty($set_parts)) { + return false; + } + + $params[':pk_value'] = $pk_value; + + try { + $sql = "UPDATE `" . $database_name . "`.`" . $table_name . "` SET " . implode(', ', $set_parts) . " WHERE `" . $pk_column . "` = :pk_value"; + $query = $database->prepare($sql); + return $query->execute($params); + } catch (PDOException $e) { + return false; + } + } + + /** + * Delete a row from the table + * @param string $database_name + * @param string $table_name + * @param mixed $pk_value + * @return bool + */ + public static function deleteRow($database_name, $table_name, $pk_value) + { + if (!$database_name || !$table_name || !$pk_value) { + return false; + } + + $pk_column = self::getPrimaryKeyColumn($database_name, $table_name); + if (!$pk_column) { + return false; + } + + $database = DatabaseFactory::getFactory()->getConnection(); + + try { + $sql = "DELETE FROM `" . $database_name . "`.`" . $table_name . "` WHERE `" . $pk_column . "` = :pk_value"; + $query = $database->prepare($sql); + return $query->execute(array(':pk_value' => $pk_value)); + } catch (PDOException $e) { + return false; + } + } + + /** + * Insert a new row into the table + * @param string $database_name + * @param string $table_name + * @param array $data - associative array of column => value + * @return bool|int - returns insert ID on success, false on failure + */ + public static function insertRow($database_name, $table_name, $data) + { + if (!$database_name || !$table_name || empty($data)) { + return false; + } + + $database = DatabaseFactory::getFactory()->getConnection(); + + $columns = array(); + $placeholders = array(); + $params = array(); + $i = 0; + + foreach ($data as $column => $value) { + if ($value === '' || $value === null) { + continue; + } + $columns[] = "`" . $column . "`"; + $param_name = ':param_' . $i; + $placeholders[] = $param_name; + $params[$param_name] = $value; + $i++; + } + + if (empty($columns)) { + return false; + } + + try { + $sql = "INSERT INTO `" . $database_name . "`.`" . $table_name . "` (" . implode(', ', $columns) . ") VALUES (" . implode(', ', $placeholders) . ")"; + $query = $database->prepare($sql); + $query->execute($params); + return $database->lastInsertId(); + } catch (PDOException $e) { + return false; + } + } + /** * Format bytes to human readable format * @param int $bytes diff --git a/application/view/_templates/dbmanager_footer.php b/application/view/_templates/dbmanager_footer.php new file mode 100644 index 0000000..924fa47 --- /dev/null +++ b/application/view/_templates/dbmanager_footer.php @@ -0,0 +1,105 @@ + + + + +
+
+
+ + SQL Console +
+ +
+ +
+
+ database_name) ? $this->database_name : Config::get('DB_NAME'); ?> + + +
+
+ +
+ +
+ + + +
+
+ +
+ '; + echo '
'; + echo ''; + echo htmlspecialchars($result['message']); + echo '' . $result['execution_time'] . 'ms'; + echo '
'; + + if (!empty($result['result'])) { + echo '
'; + foreach (array_keys($result['result'][0]) as $col) { + echo ''; + } + echo ''; + foreach ($result['result'] as $row) { + echo ''; + foreach ($row as $value) { + echo ''; + } + echo ''; + } + echo '
' . htmlspecialchars($col) . '
' . ($value === null ? 'NULL' : htmlspecialchars(substr($value, 0, 100))) . '
'; + } + echo '
'; + } else { + echo '
'; + echo '
'; + echo ''; + echo htmlspecialchars($result['message']); + echo '
'; + if (!empty($result['error'])) { + echo '
' . htmlspecialchars($result['error']) . '
'; + } + echo '
'; + } + } + ?> +
+
+ + + + + + + + + + diff --git a/application/view/_templates/dbmanager_header.php b/application/view/_templates/dbmanager_header.php new file mode 100644 index 0000000..830cf94 --- /dev/null +++ b/application/view/_templates/dbmanager_header.php @@ -0,0 +1,107 @@ + + + + Database Manager - HUGE + + + + + + + + +
+ + + + + +
+
+ + +
diff --git a/application/view/_templates/header.php b/application/view/_templates/header.php index 65fa2c7..7631eb8 100644 --- a/application/view/_templates/header.php +++ b/application/view/_templates/header.php @@ -80,6 +80,11 @@ +
  • > + Database +
  • > diff --git a/application/view/database/index.php b/application/view/database/index.php new file mode 100644 index 0000000..c67cf4c --- /dev/null +++ b/application/view/database/index.php @@ -0,0 +1,86 @@ +
    +
    + Databases +
    +
    +

    All Databases

    + databases); ?> total +
    +
    + +
    +
    + +
    +
    +
    +
    databases); ?>
    +
    Databases
    +
    +
    +
    current_db); ?>
    +
    Current Database
    +
    +
    + +
    + + + + + + + + + + databases as $db): + $tables = DatabaseModel::getTablesInDatabase($db); + ?> + + + + + + + +
    Database NameTablesActions
    + + + + current_db): ?> + ACTIVE + + + Browse + current_db): ?> + Drop + +
    +
    +
    + + + diff --git a/application/view/database/show.php b/application/view/database/show.php new file mode 100644 index 0000000..35bad96 --- /dev/null +++ b/application/view/database/show.php @@ -0,0 +1,76 @@ +
    +
    + Databases + / + database_name); ?> +
    +
    +

    database_name); ?>

    + tables); ?> tables +
    + +
    + +
    + tables)): ?> +
    + +

    No tables yet

    +

    This database is empty. Create your first table to get started.

    + Create Table +
    + +
    + + + + + + + + + + + + + tables as $table): + $info = isset($this->table_info[$table]) ? $this->table_info[$table] : []; + ?> + + + + + + + + + + +
    Table NameEngineRowsSizeCollationActions
    + + + + + Browse + Structure + Export + Drop +
    +
    + +
    diff --git a/application/view/dbuser/create.php b/application/view/dbuser/create.php new file mode 100644 index 0000000..f08b6c0 --- /dev/null +++ b/application/view/dbuser/create.php @@ -0,0 +1,68 @@ +
    +
    + Users + / + Create User +
    +
    +

    Create New User

    +
    +
    + +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    + +
    +
    + + Grant all privileges on all databases +
    +
    + + + +
    +
    +
    + +
    + + Cancel +
    +
    +
    +
    +
    diff --git a/application/view/dbuser/edit.php b/application/view/dbuser/edit.php new file mode 100644 index 0000000..206ab73 --- /dev/null +++ b/application/view/dbuser/edit.php @@ -0,0 +1,92 @@ +
    +
    + Users + / + user->User); ?>@user->Host); ?> +
    +
    +

    Edit User

    +
    +
    + +
    +
    +
    +
    +

    User Details

    +
    +
    +
    + + +
    +
    + + +
    +
    + + + Only fill this if you want to change the password +
    +
    +
    + +
    +
    +

    Global Privileges

    +
    +
    + privileges); + $has_all = stripos($current_grants, 'ALL PRIVILEGES') !== false; + ?> +
    + + Grant all privileges on all databases +
    +
    + + + +
    +
    +
    + +
    +
    +

    Current Grants

    +
    +
    + privileges)): ?> + privileges as $grant): ?> +
    + +
    + + +

    No grants found

    + +
    +
    + +
    + + Cancel +
    +
    +
    diff --git a/application/view/dbuser/index.php b/application/view/dbuser/index.php new file mode 100644 index 0000000..00092c6 --- /dev/null +++ b/application/view/dbuser/index.php @@ -0,0 +1,101 @@ +
    +
    + Users +
    +
    +

    MySQL Users

    + users); ?> users +
    +
    + +
    +
    + +
    +
    +
    + Connected as: + current_user); ?> +
    +
    + +
    + + + + + + + + + + users)): ?> + users as $user): ?> + + + + + + + + + + + + +
    UsernameHostActions
    + + User); ?> + User === $this->current_user): ?> + current + + Host); ?> + + + Edit + + User !== $this->current_user): ?> + + + Delete + + +
    + No users found +
    +
    +
    + + diff --git a/application/view/dbuser/privileges.php b/application/view/dbuser/privileges.php new file mode 100644 index 0000000..ba9fb59 --- /dev/null +++ b/application/view/dbuser/privileges.php @@ -0,0 +1,37 @@ +
    +
    + Users + / + user->User); ?>@user->Host); ?> + / + Privileges +
    +
    +

    User Privileges

    +
    + +
    + +
    +
    +
    +

    Grant Statements

    +
    +
    + privileges)): ?> + privileges as $grant): ?> +
    + +
    + + +

    No privileges found for this user.

    + +
    +
    +
    diff --git a/application/view/sql/index.php b/application/view/sql/index.php new file mode 100644 index 0000000..b29404a --- /dev/null +++ b/application/view/sql/index.php @@ -0,0 +1,282 @@ +
    +
    +

    Databases

    +
      + databases as $db): ?> +
    • + + + +
    • + +
    + + + + history)): ?> +
    +

    Query History

    +
      + history, 0, 10) as $item): ?> +
    • + ... +
    • + +
    + Clear History +
    + +
    + +
    +

    SQL Console

    +

    Database: database_name); ?>

    + +
    + + +
    + +
    + +
    + + +
    +
    + +
    + '; + echo '

    ' . htmlspecialchars($result['message']) . '

    '; + echo '

    Execution time: ' . $result['execution_time'] . ' ms

    '; + + if (!empty($result['result'])) { + echo '
    '; + foreach (array_keys($result['result'][0]) as $col) { + echo ''; + } + echo ''; + foreach ($result['result'] as $row) { + echo ''; + foreach ($row as $value) { + echo ''; + } + echo ''; + } + echo '
    ' . htmlspecialchars($col) . '
    ' . ($value === null ? 'NULL' : htmlspecialchars(substr($value, 0, 100))) . '
    '; + } + echo '
    '; + } else { + echo '
    '; + echo '

    Error: ' . htmlspecialchars($result['message']) . '

    '; + if (!empty($result['error'])) { + echo '

    ' . htmlspecialchars($result['error']) . '

    '; + } + echo '
    '; + } + } + ?> +
    +
    +
  • + + diff --git a/application/view/table/add_column.php b/application/view/table/add_column.php new file mode 100644 index 0000000..c0d821e --- /dev/null +++ b/application/view/table/add_column.php @@ -0,0 +1,68 @@ +
    +
    + Databases + / + database_name); ?> + / + table_name); ?> + / + Add Column +
    +
    +

    Add Column

    +
    +
    + +
    +
    +
    +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + + + +
    + + Cancel +
    +
    +
    +
    +
    diff --git a/application/view/table/create.php b/application/view/table/create.php new file mode 100644 index 0000000..6c8f5b1 --- /dev/null +++ b/application/view/table/create.php @@ -0,0 +1,122 @@ +
    +
    + Databases + / + database_name); ?> + / + New Table +
    +
    +

    Create New Table

    +
    +
    + +
    +
    +
    +
    +
    + + +
    + +

    Columns

    + +
    +
    + + + + + + + +
    +
    + + + +
    + + Cancel +
    +
    +
    +
    +
    + + diff --git a/application/view/table/show.php b/application/view/table/show.php new file mode 100644 index 0000000..8727ad8 --- /dev/null +++ b/application/view/table/show.php @@ -0,0 +1,435 @@ +total_rows / $this->per_page); +$pk_column = null; +foreach ($this->columns as $col) { + if ($col['Key'] === 'PRI') { + $pk_column = $col['Field']; + break; + } +} +?> +
    +
    + Databases + / + database_name); ?> + / + table_name); ?> +
    +
    +

    table_name); ?>

    + total_rows); ?> rows +
    + +
    + +
    + table_info)): ?> +
    +
    +
    total_rows); ?>
    +
    Total Rows
    +
    +
    +
    columns); ?>
    +
    Columns
    +
    +
    +
    table_info['total_size'] ?? '-'; ?>
    +
    Size
    +
    +
    +
    table_info['engine'] ?? '-'; ?>
    +
    Engine
    +
    +
    + + +
    + + + + columns as $col): ?> + + + + + + + rows)): ?> + + + + + rows as $row): ?> + + columns as $col): ?> + + + + + + + +
    + + + + + + + Actions
    + No data in this table. Click "Add Row" to insert data. +
    + NULL'; + } else { + $display = htmlspecialchars(substr($value, 0, 100)); + if (strlen($value) > 100) $display .= '...'; + echo $display; + } + ?> + + +
    + + +
    + +
    +
    + + 1): ?> +
    +
    + Showing current_page - 1) * $this->per_page) + 1; ?> - current_page * $this->per_page, $this->total_rows); ?> of total_rows); ?> +
    +
    + current_page > 1): ?> + First + Prev + + + current_page - 2); + $end = min($total_pages, $this->current_page + 2); + for ($i = $start; $i <= $end; $i++): + ?> + + + + current_page < $total_pages): ?> + Next + Last + +
    +
    + +
    + + diff --git a/application/view/table/structure.php b/application/view/table/structure.php new file mode 100644 index 0000000..d183287 --- /dev/null +++ b/application/view/table/structure.php @@ -0,0 +1,112 @@ +
    +
    + Databases + / + database_name); ?> + / + table_name); ?> + / + Structure +
    +
    +

    Structure: table_name); ?>

    + columns); ?> columns +
    + +
    + +
    +
    +
    +

    Columns

    +
    +
    + + + + + + + + + + + + + + columns as $col): ?> + + + + + + + + + + + +
    FieldTypeNullKeyDefaultExtraActions
    + + + + + + + YES' : 'NO'; ?> + + PRIMARY + + UNIQUE + + INDEX + + - + + NULL'; ?> + Drop +
    +
    +
    + + indexes)): ?> +
    +
    +

    Indexes

    +
    +
    + + + + + + + + + + + indexes as $idx): ?> + + + + + + + + +
    Key NameColumnUniqueType
    No' : 'Yes'; ?>
    +
    +
    + +
    diff --git a/public/css/dbmanager.css b/public/css/dbmanager.css new file mode 100644 index 0000000..256c8b2 --- /dev/null +++ b/public/css/dbmanager.css @@ -0,0 +1,945 @@ +:root { + --dbm-bg: #ffffff; + --dbm-bg-secondary: #fafafa; + --dbm-bg-tertiary: #f0f0f0; + --dbm-border: #e0e0e0; + --dbm-text: #333333; + --dbm-text-secondary: #666666; + --dbm-text-muted: #999999; + --dbm-accent: #555555; + --dbm-accent-light: #888888; + --dbm-success: #6b9b6b; + --dbm-danger: #b57575; + --dbm-warning: #a89a6b; + --dbm-info: #6b8a9b; + --dbm-shadow: 0 1px 3px rgba(0, 0, 0, 0.06); + --dbm-radius: 4px; + --dbm-transition: all 0.15s ease; +} + +[data-lucide] { + width: 14px; + height: 14px; + vertical-align: middle; + display: inline-block; +} + +.dbm-page-wrapper { + max-width: 1600px; + width: 95%; + background: #f5f5f5; +} + +.dbm-wrapper { + display: flex; + flex-direction: column; + min-height: calc(100vh - 120px); + background: var(--dbm-bg); + border: 1px solid var(--dbm-border); + border-radius: var(--dbm-radius); + overflow: hidden; + margin: 20px 0; + box-shadow: var(--dbm-shadow); +} + +.dbm-main { + display: flex; + flex: 1; + overflow: hidden; +} + +.dbm-sidebar { + width: 260px; + background: var(--dbm-bg-secondary); + border-right: 1px solid var(--dbm-border); + display: flex; + flex-direction: column; + flex-shrink: 0; +} + +.dbm-sidebar-header { + padding: 14px 16px; + border-bottom: 1px solid var(--dbm-border); + display: flex; + align-items: center; + gap: 8px; + background: var(--dbm-bg); +} + +.dbm-sidebar-header h3 { + margin: 0; + font-size: 12px; + font-weight: 600; + color: var(--dbm-text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.dbm-sidebar-header .icon { + width: 14px; + height: 14px; + color: var(--dbm-text-muted); + flex-shrink: 0; +} + +.dbm-sidebar-header .icon svg { + width: 14px; + height: 14px; +} + +.dbm-tree { + flex: 1; + overflow-y: auto; + padding: 8px 0; +} + +.dbm-tree::-webkit-scrollbar { + width: 6px; +} + +.dbm-tree::-webkit-scrollbar-track { + background: transparent; +} + +.dbm-tree::-webkit-scrollbar-thumb { + background: var(--dbm-border); + border-radius: 3px; +} + +.tree-item { + user-select: none; +} + +.tree-header { + display: flex; + align-items: center; + padding: 6px 12px; + cursor: pointer; + transition: var(--dbm-transition); + border-left: 2px solid transparent; + text-decoration: none; +} + +.tree-header:hover { + background: var(--dbm-bg-tertiary); +} + +.tree-header.active { + background: var(--dbm-bg-tertiary); + border-left-color: var(--dbm-accent); +} + +.tree-toggle { + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 4px; + color: var(--dbm-text-muted); + transition: var(--dbm-transition); + flex-shrink: 0; +} + +.tree-toggle svg, +.tree-toggle [data-lucide] { + width: 10px; + height: 10px; + transition: transform 0.15s ease; +} + +.tree-item.expanded > .tree-header .tree-toggle svg, +.tree-item.expanded > .tree-header .tree-toggle [data-lucide] { + transform: rotate(90deg); +} + +.tree-icon { + width: 12px; + height: 12px; + margin-right: 6px; + flex-shrink: 0; + color: var(--dbm-text-muted); + display: flex; + align-items: center; +} + +.tree-icon [data-lucide] { + width: 12px; + height: 12px; +} + +.tree-icon.database { color: var(--dbm-accent-light); } +.tree-icon.table { color: var(--dbm-accent-light); } +.tree-icon.column { color: var(--dbm-text-muted); } +.tree-icon.key { color: var(--dbm-success); } + +.tree-label { + font-size: 12px; + color: var(--dbm-text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + text-decoration: none; +} + +a.tree-label:hover { + color: var(--dbm-accent); +} + +.tree-badge { + font-size: 10px; + padding: 1px 5px; + background: var(--dbm-bg); + color: var(--dbm-text-muted); + border-radius: 8px; + margin-left: 6px; + border: 1px solid var(--dbm-border); +} + +.tree-children { + display: none; + padding-left: 16px; +} + +.tree-item.expanded > .tree-children { + display: block; +} + +.tree-children .tree-header { + padding-left: 20px; +} + +.tree-children .tree-children .tree-header { + padding-left: 28px; +} + +.dbm-sidebar-actions { + padding: 12px; + border-top: 1px solid var(--dbm-border); + background: var(--dbm-bg); +} + +.dbm-content { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + background: var(--dbm-bg); +} + +.dbm-content-header { + padding: 16px 20px; + border-bottom: 1px solid var(--dbm-border); + background: var(--dbm-bg-secondary); +} + +.dbm-breadcrumb { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--dbm-text-secondary); + margin-bottom: 6px; +} + +.dbm-breadcrumb a { + color: var(--dbm-text-secondary); + text-decoration: none; +} + +.dbm-breadcrumb a:hover { + color: var(--dbm-text); + text-decoration: underline; +} + +.dbm-breadcrumb .separator { + color: var(--dbm-text-muted); +} + +.dbm-title { + display: flex; + align-items: center; + gap: 10px; +} + +.dbm-title h1 { + margin: 0; + font-size: 18px; + font-weight: 600; + color: var(--dbm-text); +} + +.dbm-title .badge { + font-size: 11px; + padding: 3px 8px; + background: var(--dbm-bg); + color: var(--dbm-text-muted); + border-radius: 10px; + border: 1px solid var(--dbm-border); +} + +.dbm-actions { + display: flex; + gap: 8px; + margin-top: 12px; +} + +.dbm-content-body { + flex: 1; + overflow: auto; + padding: 20px; +} + +.dbm-table-wrapper { + background: var(--dbm-bg); + border-radius: var(--dbm-radius); + border: 1px solid var(--dbm-border); + overflow: hidden; +} + +.dbm-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.dbm-table th { + background: var(--dbm-bg-secondary); + padding: 10px 14px; + text-align: left; + font-weight: 600; + color: var(--dbm-text-secondary); + border-bottom: 1px solid var(--dbm-border); + white-space: nowrap; + position: sticky; + top: 0; + z-index: 10; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.3px; +} + +.dbm-table td { + padding: 8px 14px; + color: var(--dbm-text-secondary); + border-bottom: 1px solid var(--dbm-border); + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dbm-table tr:last-child td { + border-bottom: none; +} + +.dbm-table tr:hover td { + background: var(--dbm-bg-secondary); +} + +.dbm-table .null-value { + color: var(--dbm-text-muted); + font-style: italic; + font-size: 11px; +} + +.dbm-table .key-column { + color: var(--dbm-success); +} + +.dbm-table .type-column { + color: var(--dbm-text-muted); + font-family: 'Consolas', 'Monaco', monospace; + font-size: 11px; +} + +.dbm-pagination { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + background: var(--dbm-bg-secondary); + border-top: 1px solid var(--dbm-border); + margin-top: 16px; + border-radius: var(--dbm-radius); +} + +.dbm-pagination-info { + font-size: 12px; + color: var(--dbm-text-muted); +} + +.dbm-pagination-controls { + display: flex; + align-items: center; + gap: 4px; +} + +.dbm-pagination-btn { + padding: 5px 10px; + background: var(--dbm-bg); + border: 1px solid var(--dbm-border); + border-radius: var(--dbm-radius); + color: var(--dbm-text-secondary); + font-size: 12px; + cursor: pointer; + transition: var(--dbm-transition); + text-decoration: none; +} + +.dbm-pagination-btn:hover:not(:disabled) { + background: var(--dbm-bg-secondary); + border-color: var(--dbm-accent-light); +} + +.dbm-pagination-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.dbm-pagination-btn.active { + background: var(--dbm-accent); + border-color: var(--dbm-accent); + color: #fff; +} + +.dbm-console { + background: var(--dbm-bg); + border-top: 1px solid var(--dbm-border); + display: flex; + flex-direction: column; +} + +.dbm-console-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + background: var(--dbm-bg-secondary); + border-bottom: 1px solid var(--dbm-border); + cursor: pointer; + transition: var(--dbm-transition); +} + +.dbm-console-header:hover { + background: var(--dbm-bg-tertiary); +} + +.dbm-console-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + font-weight: 600; + color: var(--dbm-text-secondary); +} + +.dbm-console-title .icon { + width: 14px; + height: 14px; + color: var(--dbm-text-muted); +} + +.dbm-console-toggle { + width: 14px; + height: 14px; + color: var(--dbm-text-muted); + transition: var(--dbm-transition); +} + +.dbm-console.expanded .dbm-console-toggle { + transform: rotate(180deg); +} + +.dbm-console-body { + display: none; + padding: 16px; +} + +.dbm-console.expanded .dbm-console-body { + display: block; +} + +.dbm-sql-editor { + position: relative; + background: var(--dbm-bg-secondary); + border: 1px solid var(--dbm-border); + border-radius: var(--dbm-radius); + overflow: hidden; +} + +.dbm-sql-editor textarea { + width: 100%; + min-height: 100px; + padding: 12px; + background: transparent; + border: none; + color: var(--dbm-text); + font-family: 'Consolas', 'Monaco', monospace; + font-size: 13px; + line-height: 1.5; + resize: vertical; + outline: none; + box-sizing: border-box; +} + +.dbm-sql-editor textarea::placeholder { + color: var(--dbm-text-muted); +} + +.dbm-sql-highlight { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + padding: 12px; + font-family: 'Consolas', 'Monaco', monospace; + font-size: 13px; + line-height: 1.5; + white-space: pre-wrap; + word-wrap: break-word; + pointer-events: none; + color: transparent; + overflow: hidden; +} + +.sql-keyword { color: #555; font-weight: 600; } +.sql-function { color: #666; } +.sql-string { color: #888; } +.sql-number { color: #777; } +.sql-operator { color: #444; } +.sql-comment { color: #aaa; font-style: italic; } + +.dbm-sql-actions { + display: flex; + align-items: center; + gap: 10px; + margin-top: 10px; +} + +.dbm-sql-actions .db-select { + padding: 6px 10px; + background: var(--dbm-bg); + border: 1px solid var(--dbm-border); + border-radius: var(--dbm-radius); + color: var(--dbm-text); + font-size: 12px; + outline: none; +} + +.dbm-sql-actions .db-select:focus { + border-color: var(--dbm-accent-light); +} + +.dbm-sql-result { + margin-top: 16px; + border-radius: var(--dbm-radius); + overflow: hidden; +} + +.dbm-sql-result.success { + background: #f5f8f5; + border: 1px solid #dde5dd; +} + +.dbm-sql-result.error { + background: #f9f5f5; + border: 1px solid #e5dddd; +} + +.dbm-sql-result-header { + padding: 10px 14px; + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; +} + +.dbm-sql-result-header i { + width: 14px; + height: 14px; +} + +.dbm-sql-result.success .dbm-sql-result-header { + color: #5a7a5a; +} + +.dbm-sql-result.error .dbm-sql-result-header { + color: #8a5a5a; +} + +.dbm-sql-result-body { + padding: 0 14px 14px; + overflow-x: auto; +} + +.dbm-sql-result-body .dbm-table-wrapper { + background: transparent; + border: none; +} + +.dbm-btn { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 7px 14px; + font-size: 12px; + font-weight: 500; + border: 1px solid transparent; + border-radius: var(--dbm-radius); + cursor: pointer; + transition: var(--dbm-transition); + text-decoration: none; + white-space: nowrap; + font-family: Arial, sans-serif; +} + +.dbm-btn svg, +.dbm-btn i { + width: 14px; + height: 14px; +} + +.dbm-btn-primary { + background: var(--dbm-accent); + color: #fff; + border-color: var(--dbm-accent); +} + +.dbm-btn-primary:hover { + background: #444; + border-color: #444; +} + +.dbm-btn-success { + background: var(--dbm-success); + color: #fff; + border-color: var(--dbm-success); +} + +.dbm-btn-success:hover { + background: #5a8a5a; + border-color: #5a8a5a; +} + +.dbm-btn-danger { + background: var(--dbm-danger); + color: #fff; + border-color: var(--dbm-danger); +} + +.dbm-btn-danger:hover { + background: #a56565; + border-color: #a56565; +} + +.dbm-btn-secondary { + background: var(--dbm-bg); + border-color: var(--dbm-border); + color: var(--dbm-text-secondary); +} + +.dbm-btn-secondary:hover { + background: var(--dbm-bg-secondary); + border-color: var(--dbm-accent-light); + color: var(--dbm-text); +} + +.dbm-btn-sm { + padding: 4px 8px; + font-size: 11px; +} + +.dbm-btn-sm svg, +.dbm-btn-sm i { + width: 12px; + height: 12px; +} + +.dbm-form-group { + margin-bottom: 16px; +} + +.dbm-form-label { + display: block; + margin-bottom: 6px; + font-size: 12px; + font-weight: 600; + color: var(--dbm-text-secondary); +} + +.dbm-form-input, +.dbm-form-select { + width: 100%; + max-width: 350px; + padding: 8px 12px; + background: var(--dbm-bg); + border: 1px solid var(--dbm-border); + border-radius: var(--dbm-radius); + color: var(--dbm-text); + font-size: 13px; + outline: none; + transition: var(--dbm-transition); + font-family: Arial, sans-serif; +} + +.dbm-form-input:focus, +.dbm-form-select:focus { + border-color: var(--dbm-accent-light); + box-shadow: 0 0 0 2px rgba(85, 85, 85, 0.1); +} + +.dbm-form-input::placeholder { + color: var(--dbm-text-muted); +} + +.dbm-card { + background: var(--dbm-bg); + border: 1px solid var(--dbm-border); + border-radius: var(--dbm-radius); + overflow: hidden; +} + +.dbm-card-header { + padding: 12px 16px; + border-bottom: 1px solid var(--dbm-border); + background: var(--dbm-bg-secondary); +} + +.dbm-card-header h3 { + margin: 0; + font-size: 12px; + font-weight: 600; + color: var(--dbm-text-secondary); + text-transform: uppercase; + letter-spacing: 0.3px; +} + +.dbm-card-body { + padding: 16px; +} + +.dbm-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); + gap: 12px; + margin-bottom: 20px; +} + +.dbm-stat { + background: var(--dbm-bg-secondary); + border: 1px solid var(--dbm-border); + border-radius: var(--dbm-radius); + padding: 14px 16px; +} + +.dbm-stat-value { + font-size: 18px; + font-weight: 600; + color: var(--dbm-text); + margin-bottom: 2px; +} + +.dbm-stat-label { + font-size: 10px; + color: var(--dbm-text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.dbm-empty { + text-align: center; + padding: 50px 20px; + color: var(--dbm-text-secondary); +} + +.dbm-empty-icon { + width: 40px; + height: 40px; + margin: 0 auto 16px; + color: var(--dbm-border); + display: block; +} + +.dbm-empty h3 { + margin: 0 0 6px; + font-size: 15px; + color: var(--dbm-text-secondary); + font-weight: 500; +} + +.dbm-empty p { + margin: 0; + font-size: 13px; + color: var(--dbm-text-muted); +} + +.dbm-loading { + display: flex; + align-items: center; + justify-content: center; + padding: 30px; +} + +.dbm-spinner { + width: 20px; + height: 20px; + border: 2px solid var(--dbm-border); + border-top-color: var(--dbm-accent-light); + border-radius: 50%; + animation: spin 0.7s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +@media (max-width: 1024px) { + .dbm-sidebar { + width: 220px; + } +} + +@media (max-width: 768px) { + .dbm-main { + flex-direction: column; + } + + .dbm-sidebar { + width: 100%; + max-height: 250px; + border-right: none; + border-bottom: 1px solid var(--dbm-border); + } + + .dbm-content-body { + padding: 14px; + } +} + +.data-row.editing { + background: #fafaf5 !important; +} + +.data-row.new-row { + background: #f5faf5 !important; +} + +.editable-cell { + position: relative; +} + +.editable-cell .cell-input { + border: 1px solid var(--dbm-accent-light); + border-radius: 3px; + background: var(--dbm-bg); +} + +.editable-cell .cell-input:focus { + outline: none; + box-shadow: 0 0 0 2px rgba(85, 85, 85, 0.1); +} + +.view-actions, +.edit-actions { + display: inline-flex; + gap: 4px; +} + +.actions-cell .dbm-btn-sm { + padding: 4px 6px; +} + +.data-row:hover { + background: var(--dbm-bg-secondary); +} + +.data-row.editing:hover { + background: #fafaf5 !important; +} + +.data-row.new-row:hover { + background: #f5faf5 !important; +} + +.dbm-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.4); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.dbm-modal-content { + background: var(--dbm-bg); + border-radius: var(--dbm-radius); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + width: 100%; + max-width: 420px; + max-height: 90vh; + overflow: hidden; +} + +.dbm-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 18px; + border-bottom: 1px solid var(--dbm-border); + background: var(--dbm-bg-secondary); +} + +.dbm-modal-header h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: var(--dbm-text); +} + +.dbm-modal-close { + background: none; + border: none; + font-size: 20px; + color: var(--dbm-text-muted); + cursor: pointer; + line-height: 1; + padding: 0; +} + +.dbm-modal-close:hover { + color: var(--dbm-text); +} + +.dbm-modal-body { + padding: 18px; +} + +.dbm-modal-body .dbm-form-input, +.dbm-modal-body .dbm-form-select { + max-width: 100%; +} + +.dbm-modal-footer { + display: flex; + justify-content: flex-end; + gap: 8px; + padding: 14px 18px; + border-top: 1px solid var(--dbm-border); + background: var(--dbm-bg-secondary); +} + +.badge { + font-size: 10px; + padding: 2px 6px; + background: var(--dbm-bg-tertiary); + color: var(--dbm-text-muted); + border-radius: 8px; + border: 1px solid var(--dbm-border); +} diff --git a/public/js/dbmanager.js b/public/js/dbmanager.js new file mode 100644 index 0000000..698724f --- /dev/null +++ b/public/js/dbmanager.js @@ -0,0 +1,389 @@ +const DBManager = { + baseUrl: '', + + init(baseUrl) { + this.baseUrl = baseUrl; + this.initTree(); + this.initConsole(); + this.initSqlHighlighting(); + this.initAjaxForms(); + }, + + initTree() { + document.querySelectorAll('.tree-header').forEach(header => { + header.addEventListener('click', (e) => { + const item = header.closest('.tree-item'); + const children = item.querySelector('.tree-children'); + const href = header.dataset.href; + const toggle = e.target.closest('.tree-toggle'); + + // If clicking on toggle icon, just expand/collapse + if (toggle) { + e.preventDefault(); + e.stopPropagation(); + + if (item.dataset.db && children && !children.dataset.loaded) { + this.loadTables(item.dataset.db, children); + } + this.toggleTreeItem(item); + return; + } + + // If it's a table or database item with href, navigate directly + if (href) { + // For databases, also load tables if not loaded + if (item.dataset.db && children && !children.dataset.loaded) { + this.loadTables(item.dataset.db, children); + } + window.location.href = href; + return; + } + }); + }); + }, + + toggleTreeItem(item) { + item.classList.toggle('expanded'); + }, + + async loadTables(dbName, container) { + container.innerHTML = '
    Loading...
    '; + container.dataset.loaded = 'true'; + + try { + const response = await fetch(`${this.baseUrl}database/getStructure/${encodeURIComponent(dbName)}`); + const data = await response.json(); + + if (data.success && data.structure) { + let html = ''; + for (const [table, columns] of Object.entries(data.structure)) { + html += this.renderTableTreeItem(dbName, table, columns); + } + container.innerHTML = html || '
    No tables
    '; + this.initTree(); + this.refreshIcons(); + } + } catch (error) { + container.innerHTML = '
    Failed to load
    '; + } + }, + + renderTableTreeItem(dbName, tableName, columns) { + const columnsHtml = columns.map(col => ` +
    +
    + + ${col.Key === 'PRI' ? this.icons.key : this.icons.column} + + ${this.escapeHtml(col.Field)} + ${this.escapeHtml(col.Type)} +
    +
    + `).join(''); + + return ` +
    +
    + ${this.icons.chevron} + ${this.icons.table} + ${this.escapeHtml(tableName)} +
    +
    + ${columnsHtml} +
    +
    + `; + }, + + initConsole() { + const consoleHeader = document.querySelector('.dbm-console-header'); + if (consoleHeader) { + consoleHeader.addEventListener('click', () => { + const console = consoleHeader.closest('.dbm-console'); + console.classList.toggle('expanded'); + localStorage.setItem('dbm-console-expanded', console.classList.contains('expanded')); + }); + + const wasExpanded = localStorage.getItem('dbm-console-expanded') === 'true'; + if (wasExpanded) { + consoleHeader.closest('.dbm-console').classList.add('expanded'); + } + } + + const sqlForm = document.getElementById('sql-form'); + if (sqlForm) { + sqlForm.addEventListener('submit', async (e) => { + e.preventDefault(); + await this.executeQuery(sqlForm); + }); + } + }, + + async executeQuery(form) { + const formData = new FormData(form); + const resultContainer = document.getElementById('sql-result'); + + resultContainer.innerHTML = '
    '; + + try { + const response = await fetch(`${this.baseUrl}sql/execute`, { + method: 'POST', + body: formData, + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }); + + const data = await response.json(); + this.renderSqlResult(data, resultContainer); + } catch (error) { + resultContainer.innerHTML = ` +
    +
    + ${this.icons.error} Error: Failed to execute query +
    +
    + `; + } + }, + + renderSqlResult(data, container) { + if (data.success) { + let tableHtml = ''; + + if (data.result && data.result.length > 0) { + const columns = Object.keys(data.result[0]); + tableHtml = ` +
    + + + ${columns.map(col => ``).join('')} + + + ${data.result.map(row => ` + ${columns.map(col => ` + + `).join('')} + `).join('')} + +
    ${this.escapeHtml(col)}
    ${row[col] === null ? 'NULL' : this.escapeHtml(String(row[col]).substring(0, 100))}
    +
    + `; + } + + container.innerHTML = ` +
    +
    + ${this.icons.success} ${this.escapeHtml(data.message)} + + ${data.execution_time}ms + +
    + ${tableHtml ? `
    ${tableHtml}
    ` : ''} +
    + `; + } else { + container.innerHTML = ` +
    +
    + ${this.icons.error} ${this.escapeHtml(data.message)} +
    + ${data.error ? `
    ${this.escapeHtml(data.error)}
    ` : ''} +
    + `; + } + this.refreshIcons(); + }, + + initSqlHighlighting() { + const textarea = document.getElementById('sql_query'); + if (!textarea) return; + + textarea.addEventListener('input', () => this.highlightSql(textarea)); + textarea.addEventListener('scroll', () => this.syncScroll(textarea)); + this.highlightSql(textarea); + }, + + highlightSql(textarea) { + const highlight = document.getElementById('sql-highlight'); + if (!highlight) return; + + let code = textarea.value; + code = this.escapeHtml(code); + code = this.applySqlSyntax(code); + highlight.innerHTML = code + '\n'; + }, + + applySqlSyntax(code) { + const keywords = [ + 'SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'NOT', 'IN', 'LIKE', 'BETWEEN', + 'ORDER BY', 'GROUP BY', 'HAVING', 'LIMIT', 'OFFSET', 'JOIN', 'INNER JOIN', + 'LEFT JOIN', 'RIGHT JOIN', 'OUTER JOIN', 'ON', 'AS', 'DISTINCT', 'ALL', + 'INSERT', 'INTO', 'VALUES', 'UPDATE', 'SET', 'DELETE', 'CREATE', 'TABLE', + 'DATABASE', 'INDEX', 'VIEW', 'DROP', 'ALTER', 'ADD', 'COLUMN', 'PRIMARY KEY', + 'FOREIGN KEY', 'REFERENCES', 'CONSTRAINT', 'DEFAULT', 'NULL', 'NOT NULL', + 'AUTO_INCREMENT', 'UNIQUE', 'ENGINE', 'CHARSET', 'COLLATE', 'IF', 'EXISTS', + 'SHOW', 'DESCRIBE', 'EXPLAIN', 'USE', 'GRANT', 'REVOKE', 'UNION', 'CASE', + 'WHEN', 'THEN', 'ELSE', 'END', 'IS', 'TRUE', 'FALSE', 'ASC', 'DESC' + ]; + + const functions = [ + 'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'CONCAT', 'SUBSTRING', 'LENGTH', + 'UPPER', 'LOWER', 'TRIM', 'REPLACE', 'NOW', 'CURDATE', 'DATE', 'YEAR', + 'MONTH', 'DAY', 'HOUR', 'MINUTE', 'COALESCE', 'IFNULL', 'NULLIF', 'CAST', + 'CONVERT', 'FORMAT', 'ROUND', 'FLOOR', 'CEIL', 'ABS', 'MOD', 'RAND' + ]; + + code = code.replace(/'([^'\\]|\\.)*'/g, '$&'); + code = code.replace(/"([^"\\]|\\.)*"/g, '$&'); + code = code.replace(/\b(\d+\.?\d*)\b/g, '$1'); + + functions.forEach(func => { + const regex = new RegExp(`\\b(${func})\\s*\\(`, 'gi'); + code = code.replace(regex, '$1('); + }); + + keywords.forEach(keyword => { + const regex = new RegExp(`\\b(${keyword.replace(' ', '\\s+')})\\b`, 'gi'); + code = code.replace(regex, '$1'); + }); + + code = code.replace(/(--[^\n]*)/g, '$1'); + code = code.replace(/(\/\*[\s\S]*?\*\/)/g, '$1'); + + return code; + }, + + syncScroll(textarea) { + const highlight = document.getElementById('sql-highlight'); + if (highlight) { + highlight.scrollTop = textarea.scrollTop; + highlight.scrollLeft = textarea.scrollLeft; + } + }, + + initAjaxForms() { + document.querySelectorAll('[data-ajax-form]').forEach(form => { + form.addEventListener('submit', async (e) => { + e.preventDefault(); + await this.submitAjaxForm(form); + }); + }); + + document.querySelectorAll('[data-confirm]').forEach(el => { + el.addEventListener('click', (e) => { + if (!confirm(el.dataset.confirm)) { + e.preventDefault(); + } + }); + }); + }, + + async submitAjaxForm(form) { + const formData = new FormData(form); + const submitBtn = form.querySelector('[type="submit"]'); + const originalText = submitBtn?.innerHTML; + + if (submitBtn) { + submitBtn.disabled = true; + submitBtn.innerHTML = ''; + } + + try { + const response = await fetch(form.action, { + method: 'POST', + body: formData, + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }); + + const data = await response.json(); + + if (data.success) { + if (data.redirect) { + window.location.href = data.redirect; + } else if (data.reload) { + window.location.reload(); + } else { + this.showNotification(data.message, 'success'); + } + } else { + this.showNotification(data.message || 'An error occurred', 'error'); + } + } catch (error) { + this.showNotification('Request failed', 'error'); + } finally { + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.innerHTML = originalText; + } + } + }, + + showNotification(message, type) { + const notification = document.createElement('div'); + notification.className = `dbm-notification ${type}`; + notification.innerHTML = ` + ${type === 'success' ? this.icons.success : this.icons.error} + ${this.escapeHtml(message)} + `; + notification.style.cssText = ` + position: fixed; + bottom: 20px; + right: 20px; + padding: 12px 20px; + background: ${type === 'success' ? 'var(--accent-green)' : 'var(--accent-red)'}; + color: #fff; + border-radius: var(--radius); + display: flex; + align-items: center; + gap: 10px; + font-size: 14px; + box-shadow: var(--shadow); + z-index: 1000; + animation: slideIn 0.3s ease; + `; + + document.body.appendChild(notification); + this.refreshIcons(); + + setTimeout(() => { + notification.style.animation = 'slideOut 0.3s ease'; + setTimeout(() => notification.remove(), 300); + }, 3000); + }, + + escapeHtml(str) { + if (!str) return ''; + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; + }, + + icons: { + chevron: '', + database: '', + table: '', + column: '', + key: '', + success: '', + error: '', + terminal: '' + }, + + refreshIcons() { + if (typeof lucide !== 'undefined') { + lucide.createIcons(); + } + } +}; + +const style = document.createElement('style'); +style.textContent = ` + @keyframes slideIn { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } + } + @keyframes slideOut { + from { transform: translateX(0); opacity: 1; } + to { transform: translateX(100%); opacity: 0; } + } +`; +document.head.appendChild(style);