Compare commits

..

2 Commits

Author SHA1 Message Date
33243fbe4a Added galery 2026-01-26 10:37:06 +01:00
d9b4c73baa Initial commit 2026-01-14 23:04:53 +01:00
48 changed files with 6028 additions and 37 deletions

View File

@@ -41,12 +41,9 @@ if (!$response_data->success || $response_data->score < 0.5) {
- Bessere User Experience - Bessere User Experience
- Intelligente Bot-Erkennung durch Verhaltensanalyse - Intelligente Bot-Erkennung durch Verhaltensanalyse
``` ![img.png](assets/screenshots/img.png)
<!-- Screenshot Platzhalter --> ![img.png](assets/screenshots/img2.png)
[📸 Screenshot: Registrierungsformular] ![img.png](assets/screenshots/img3.png)
[📸 Screenshot: reCAPTCHA Badge]
```
--- ---
## Erweiterung der Admin-Funktionen ## Erweiterung der Admin-Funktionen
@@ -123,6 +120,79 @@ Implementierung einer vollständigen Notizen-Anwendung mit CRUD-Funktionalität
--- ---
## Bildergalerie mit Verschlüsselung
#### Sichere Bildergalerie mit AES-256 Verschlüsselung
**Beschreibung:**
Implementierung einer vollständigen Bildergalerie mit clientseitiger Vorschau und serverseitiger AES-256-CBC Verschlüsselung. Alle hochgeladenen Bilder werden verschlüsselt gespeichert und erst beim Abruf entschlüsselt.
**Features:**
- **AES-256-CBC Verschlüsselung**: Jedes Bild wird mit einem einzigartigen Schlüssel verschlüsselt
- **Automatische Thumbnail-Generierung**: Optimierte Vorschaubilder für schnelles Laden
- **Drag & Drop Upload**: Moderne Upload-Oberfläche mit Datei-Drag & Drop
- **AJAX-Upload mit Fortschrittsanzeige**: Echtzeit-Feedback während des Uploads
- **Öffentliche/Private Bilder**: Benutzer können Sichtbarkeit ihrer Bilder steuern
- **Bildvorschau vor Upload**: Clientseitige Vorschau des ausgewählten Bildes
**Technische Umsetzung:**
*Verschlüsselung (Upload):*
```php
$encryption_key = bin2hex(random_bytes(32));
$iv = random_bytes(openssl_cipher_iv_length('AES-256-CBC'));
$encrypted_image = openssl_encrypt($image_data, 'AES-256-CBC', $encryption_key, OPENSSL_RAW_DATA, $iv);
$encrypted_image = base64_encode($iv . $encrypted_image);
```
*Entschlüsselung (Anzeige):*
```php
$encrypted_data = base64_decode(file_get_contents($filepath));
$iv = substr($encrypted_data, 0, $iv_length);
$encrypted_data = substr($encrypted_data, $iv_length);
$decrypted = openssl_decrypt($encrypted_data, 'AES-256-CBC', $encryption_key, OPENSSL_RAW_DATA, $iv);
```
**Datenbankstruktur:**
```sql
CREATE TABLE `gallery` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`filename` varchar(255) NOT NULL,
`thumb_filename` varchar(255) NOT NULL,
`title` varchar(255) NOT NULL,
`description` text,
`is_public` tinyint(1) NOT NULL DEFAULT 1,
`file_size` int(11) NOT NULL DEFAULT 0,
`mime_type` varchar(100) NOT NULL,
`encryption_key` varchar(64) NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
);
```
**Unterstützte Formate:**
- JPEG / JPG
- PNG (mit Transparenz-Erhaltung)
- GIF
- WebP
**Zugriffsrechte:**
- **Öffentliche Galerie**: Alle öffentlichen Bilder für jeden sichtbar
- **Meine Bilder**: Nur eigene Bilder (öffentlich und privat)
- **Upload/Bearbeiten/Löschen**: Nur für angemeldete Benutzer
```
<!-- Screenshot Platzhalter -->
[📸 Screenshot: Galerie-Übersicht mit Bildraster]
[📸 Screenshot: Upload-Formular mit Drag & Drop Zone]
[📸 Screenshot: Upload-Fortschrittsanzeige]
[📸 Screenshot: Einzelbildansicht]
[📸 Screenshot: Meine Bilder - Verwaltung]
```
---
## jQuery - Einführung und Grundlagen ## jQuery - Einführung und Grundlagen
#### JavaScript Basics #### JavaScript Basics

View File

@@ -0,0 +1,25 @@
-- Gallery table for encrypted image uploads
-- Run this SQL to create/update the gallery table
-- Drop old table if exists and recreate
DROP TABLE IF EXISTS `gallery`;
CREATE TABLE `gallery` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`filename` varchar(255) NOT NULL,
`thumb_filename` varchar(255) NOT NULL,
`title` varchar(255) NOT NULL,
`description` text,
`is_public` tinyint(1) NOT NULL DEFAULT 1,
`file_size` int(11) NOT NULL DEFAULT 0,
`mime_type` varchar(100) NOT NULL,
`encryption_key` varchar(64) NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `is_public` (`is_public`),
KEY `created_at` (`created_at`),
CONSTRAINT `gallery_user_fk` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -47,6 +47,8 @@ return array(
*/ */
'PATH_AVATARS' => realpath(dirname(__FILE__).'/../../') . '/public/avatars/', 'PATH_AVATARS' => realpath(dirname(__FILE__).'/../../') . '/public/avatars/',
'PATH_AVATARS_PUBLIC' => 'avatars/', 'PATH_AVATARS_PUBLIC' => 'avatars/',
'PATH_GALLERY' => dirname(__FILE__) . '/../../public/gallery_uploads/',
'PATH_GALLERY_PUBLIC' => 'gallery_uploads/',
/** /**
* Configuration for: Default controller and action * Configuration for: Default controller and action
*/ */
@@ -72,8 +74,8 @@ return array(
/** /**
* Configuration for: Google reCAPTCHA v2 * Configuration for: Google reCAPTCHA v2
*/ */
'RECAPTCHA_SITE_KEY' => 'recaptcha-site-key', 'RECAPTCHA_SITE_KEY' => '6Lfl-EcsAAAAAG9svnagihb5y6HCNK2cd5W9jQm-',
'RECAPTCHA_SECRET_KEY' => 'recaptcha-secret-key', 'RECAPTCHA_SECRET_KEY' => '6Lfl-EcsAAAAADusuMYTprgTZ42BVIWPsF_jVtk6',
/** /**
* Configuration for: Cookies * Configuration for: Cookies
* 1209600 seconds = 2 weeks * 1209600 seconds = 2 weeks

View File

@@ -14,8 +14,9 @@ class DatabaseController extends Controller
{ {
parent::__construct(); parent::__construct();
// Only logged-in users can access the database manager // Only admin users can access the database manager
Auth::checkAuthentication(); Auth::checkAuthentication();
Auth::checkAdminAuthentication();
} }
/** /**
@@ -23,7 +24,7 @@ class DatabaseController extends Controller
*/ */
public function index() public function index()
{ {
$this->View->render('database/index', array( $this->View->renderDbManager('database/index', array(
'databases' => DatabaseModel::getAllDatabases(), 'databases' => DatabaseModel::getAllDatabases(),
'current_db' => Config::get('DB_NAME') 'current_db' => Config::get('DB_NAME')
)); ));
@@ -39,7 +40,7 @@ class DatabaseController extends Controller
$database_name = Config::get('DB_NAME'); $database_name = Config::get('DB_NAME');
} }
$this->View->render('database/show', array( $this->View->renderDbManager('database/show', array(
'tables' => DatabaseModel::getTablesInDatabase($database_name), 'tables' => DatabaseModel::getTablesInDatabase($database_name),
'database_name' => $database_name, 'database_name' => $database_name,
'table_info' => DatabaseModel::getTableDetails($database_name) 'table_info' => DatabaseModel::getTableDetails($database_name)
@@ -125,7 +126,7 @@ class DatabaseController extends Controller
} }
$structure = DatabaseModel::getDatabaseStructure($database_name); $structure = DatabaseModel::getDatabaseStructure($database_name);
header('Content-Type: application/json'); header('Content-Type: application/json');
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
@@ -133,12 +134,40 @@ class DatabaseController extends Controller
]); ]);
} }
/**
* Get columns for a specific table (AJAX endpoint)
* @param string $database_name
* @param string $table_name
*/
public function getColumns($database_name, $table_name)
{
$columns = TableModel::getTableColumns($database_name, $table_name);
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'columns' => $columns
]);
}
/**
* 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 * Check if the request is an AJAX request
*/ */
private function isAjaxRequest() private function isAjaxRequest()
{ {
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'; strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
} }
} }

View File

@@ -24,7 +24,7 @@ class DbUserController extends Controller
*/ */
public function index() public function index()
{ {
$this->View->render('dbuser/index', array( $this->View->renderDbManager('dbuser/index', array(
'users' => DbUserModel::getAllUsers(), 'users' => DbUserModel::getAllUsers(),
'current_user' => Config::get('DB_USER') 'current_user' => Config::get('DB_USER')
)); ));
@@ -39,11 +39,15 @@ class DbUserController extends Controller
$username = Request::post('username'); $username = Request::post('username');
$password = Request::post('password'); $password = Request::post('password');
$host = Request::post('host'); $host = Request::post('host');
$privileges = Request::post('privileges');
if ($this->isAjaxRequest()) { if ($this->isAjaxRequest()) {
header('Content-Type: application/json'); header('Content-Type: application/json');
if (DbUserModel::createUser($username, $password, $host)) { if (DbUserModel::createUser($username, $password, $host)) {
if (!empty($privileges)) {
DbUserModel::updateUserPrivileges($username, $host, $privileges);
}
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
'message' => 'User created successfully', 'message' => 'User created successfully',
@@ -57,8 +61,11 @@ class DbUserController extends Controller
} }
return; return;
} }
if (DbUserModel::createUser($username, $password, $host)) { if (DbUserModel::createUser($username, $password, $host)) {
if (!empty($privileges)) {
DbUserModel::updateUserPrivileges($username, $host, $privileges);
}
Redirect::to('dbuser'); Redirect::to('dbuser');
} else { } else {
Redirect::to('dbuser'); Redirect::to('dbuser');
@@ -66,8 +73,7 @@ class DbUserController extends Controller
return; return;
} }
// Show create user form $this->View->renderDbManager('dbuser/create');
$this->View->render('dbuser/create');
} }
/** /**
@@ -127,7 +133,7 @@ class DbUserController extends Controller
} }
// Show edit user form // Show edit user form
$this->View->render('dbuser/edit', array( $this->View->renderDbManager('dbuser/edit', array(
'user' => DbUserModel::getUserDetails($username, $host), 'user' => DbUserModel::getUserDetails($username, $host),
'privileges' => DbUserModel::getUserPrivileges($username, $host), 'privileges' => DbUserModel::getUserPrivileges($username, $host),
'databases' => DatabaseModel::getAllDatabases() 'databases' => DatabaseModel::getAllDatabases()
@@ -184,7 +190,7 @@ class DbUserController extends Controller
*/ */
public function privileges($username, $host) public function privileges($username, $host)
{ {
$this->View->render('dbuser/privileges', array( $this->View->renderDbManager('dbuser/privileges', array(
'user' => DbUserModel::getUserDetails($username, $host), 'user' => DbUserModel::getUserDetails($username, $host),
'privileges' => DbUserModel::getUserPrivileges($username, $host) 'privileges' => DbUserModel::getUserPrivileges($username, $host)
)); ));

View File

@@ -0,0 +1,226 @@
<?php
class GalleryController extends Controller
{
public function __construct()
{
parent::__construct();
}
public function index($page = 1)
{
$page = (int)$page;
$per_page = 24;
$this->View->render('gallery/index', array(
'images' => GalleryModel::getAllImages(null, $page, $per_page),
'total_images' => GalleryModel::getImageCount(),
'current_page' => $page,
'per_page' => $per_page
));
}
public function my($page = 1)
{
Auth::checkAuthentication();
$page = (int)$page;
$per_page = 24;
$user_id = Session::get('user_id');
$this->View->render('gallery/my', array(
'images' => GalleryModel::getAllImages($user_id, $page, $per_page),
'total_images' => GalleryModel::getImageCount($user_id),
'current_page' => $page,
'per_page' => $per_page
));
}
public function view($image_id)
{
$image = GalleryModel::getImage($image_id);
if (!$image) {
Redirect::to('gallery');
return;
}
if (!$image->is_public && $image->user_id != Session::get('user_id')) {
Session::add('feedback_negative', 'This image is private');
Redirect::to('gallery');
return;
}
$this->View->render('gallery/view', array(
'image' => $image
));
}
public function upload()
{
// Check if AJAX request first
$isAjax = $this->isAjaxRequest();
// Check authentication - return JSON error for AJAX
if (!Session::userIsLoggedIn()) {
if ($isAjax) {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => 'You must be logged in to upload']);
return;
}
Redirect::to('login/index');
return;
}
// Handle POST request (form submission)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Check if image was uploaded
if (!isset($_FILES['image']) || $_FILES['image']['error'] !== UPLOAD_ERR_OK) {
$errorMsg = 'Please select an image to upload';
if (isset($_FILES['image'])) {
switch ($_FILES['image']['error']) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
$errorMsg = 'File is too large';
break;
case UPLOAD_ERR_NO_FILE:
$errorMsg = 'No file was uploaded';
break;
}
}
if ($isAjax) {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => $errorMsg]);
return;
}
Session::add('feedback_negative', $errorMsg);
$this->View->render('gallery/upload');
return;
}
$title = Request::post('title');
$description = Request::post('description');
$is_public = Request::post('is_public') ? 1 : 0;
$image_id = GalleryModel::uploadImage($_FILES['image'], $title, $description, $is_public);
if ($isAjax) {
header('Content-Type: application/json');
if ($image_id) {
echo json_encode([
'success' => true,
'message' => 'Image uploaded successfully',
'image_id' => $image_id
]);
} else {
echo json_encode([
'success' => false,
'message' => Session::get('feedback_negative')[0] ?? 'Failed to upload image'
]);
}
return;
}
if ($image_id) {
Redirect::to('gallery/success/' . $image_id);
return;
}
}
$this->View->render('gallery/upload');
}
public function success($image_id)
{
Auth::checkAuthentication();
$image = GalleryModel::getImage($image_id);
if (!$image || $image->user_id != Session::get('user_id')) {
Redirect::to('gallery');
return;
}
$this->View->render('gallery/success', array(
'image' => $image
));
}
public function edit($image_id)
{
Auth::checkAuthentication();
$image = GalleryModel::getImage($image_id);
if (!$image || $image->user_id != Session::get('user_id')) {
Session::add('feedback_negative', 'Image not found or access denied');
Redirect::to('gallery/my');
return;
}
if (Request::post('submit_edit')) {
$title = Request::post('title');
$description = Request::post('description');
$is_public = Request::post('is_public') ? 1 : 0;
if (GalleryModel::updateImage($image_id, $title, $description, $is_public)) {
Session::add('feedback_positive', 'Image updated successfully');
Redirect::to('gallery/view/' . $image_id);
return;
}
}
$this->View->render('gallery/edit', array(
'image' => $image
));
}
public function delete($image_id)
{
Auth::checkAuthentication();
$success = GalleryModel::deleteImage($image_id);
if ($this->isAjaxRequest()) {
header('Content-Type: application/json');
echo json_encode([
'success' => $success,
'message' => $success ? 'Image deleted successfully' : 'Failed to delete image'
]);
return;
}
if ($success) {
Session::add('feedback_positive', 'Image deleted successfully');
} else {
Session::add('feedback_negative', 'Failed to delete image');
}
Redirect::to('gallery/my');
}
public function image($image_id, $type = 'full')
{
$thumbnail = ($type === 'thumb');
$result = GalleryModel::getDecryptedImage($image_id, $thumbnail);
if (!$result || !$result['data']) {
header('HTTP/1.0 404 Not Found');
exit;
}
header('Content-Type: ' . $result['mime_type']);
header('Content-Length: ' . strlen($result['data']));
header('Cache-Control: public, max-age=31536000');
echo $result['data'];
exit;
}
private function isAjaxRequest()
{
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
}
}

View File

@@ -14,8 +14,9 @@ class SqlController extends Controller
{ {
parent::__construct(); parent::__construct();
// Only logged-in users can access the SQL console // Only admin users can access the SQL console
Auth::checkAuthentication(); Auth::checkAuthentication();
Auth::checkAdminAuthentication();
} }
/** /**
@@ -28,7 +29,7 @@ class SqlController extends Controller
$database_name = Config::get('DB_NAME'); $database_name = Config::get('DB_NAME');
} }
$this->View->render('sql/index', array( $this->View->renderDbManager('sql/index', array(
'database_name' => $database_name, 'database_name' => $database_name,
'databases' => DatabaseModel::getAllDatabases(), 'databases' => DatabaseModel::getAllDatabases(),
'history' => SqlModel::getQueryHistory(Session::get('user_id')) 'history' => SqlModel::getQueryHistory(Session::get('user_id'))

View File

@@ -14,8 +14,9 @@ class TableController extends Controller
{ {
parent::__construct(); parent::__construct();
// Only logged-in users can access the table manager // Only admin users can access the table manager
Auth::checkAuthentication(); Auth::checkAuthentication();
Auth::checkAdminAuthentication();
} }
/** /**
@@ -38,7 +39,7 @@ class TableController extends Controller
$page = (int)$page; $page = (int)$page;
$per_page = 20; $per_page = 20;
$this->View->render('table/show', array( $this->View->renderDbManager('table/show', array(
'database_name' => $database_name, 'database_name' => $database_name,
'table_name' => $table_name, 'table_name' => $table_name,
'columns' => TableModel::getTableColumns($database_name, $table_name), 'columns' => TableModel::getTableColumns($database_name, $table_name),
@@ -91,7 +92,7 @@ class TableController extends Controller
} }
// Show create table form // Show create table form
$this->View->render('table/create', array( $this->View->renderDbManager('table/create', array(
'database_name' => $database_name 'database_name' => $database_name
)); ));
} }
@@ -112,7 +113,7 @@ class TableController extends Controller
return; return;
} }
$this->View->render('table/structure', array( $this->View->renderDbManager('table/structure', array(
'database_name' => $database_name, 'database_name' => $database_name,
'table_name' => $table_name, 'table_name' => $table_name,
'columns' => TableModel::getTableColumns($database_name, $table_name), 'columns' => TableModel::getTableColumns($database_name, $table_name),
@@ -172,7 +173,7 @@ class TableController extends Controller
} }
// Show add column form // Show add column form
$this->View->render('table/add_column', array( $this->View->renderDbManager('table/add_column', array(
'database_name' => $database_name, 'database_name' => $database_name,
'table_name' => $table_name 'table_name' => $table_name
)); ));
@@ -237,12 +238,160 @@ class TableController extends Controller
Redirect::to('database/show/' . urlencode($database_name)); 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 * Check if the request is an AJAX request
*/ */
private function isAjaxRequest() private function isAjaxRequest()
{ {
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'; 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);
}
} }

View File

@@ -25,7 +25,33 @@ class View
/* Note properties */ /* Note properties */
public $messages; public $messages;
public $other_user; 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;
/* Gallery properties */
public $images;
public $total_images;
public $image;
/** /**
* Static property to track if header has been rendered * Static property to track if header has been rendered
*/ */
@@ -115,6 +141,24 @@ class View
header("Content-Type: application/json"); header("Content-Type: application/json");
echo json_encode($data); 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 * Reset header render flag at start of request
@@ -206,6 +250,25 @@ class View
return false; 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 * Converts characters to HTML entities
* This is important to avoid XSS attacks, and attempts to inject malicious code in your page. * This is important to avoid XSS attacks, and attempts to inject malicious code in your page.

View File

@@ -21,7 +21,6 @@ class DatabaseModel
$databases = $query->fetchAll(PDO::FETCH_COLUMN); $databases = $query->fetchAll(PDO::FETCH_COLUMN);
// Filter out system databases
$system_dbs = ['information_schema', 'performance_schema', 'mysql', 'sys']; $system_dbs = ['information_schema', 'performance_schema', 'mysql', 'sys'];
return array_diff($databases, $system_dbs); return array_diff($databases, $system_dbs);
} }
@@ -35,7 +34,7 @@ class DatabaseModel
{ {
$database = DatabaseFactory::getFactory()->getConnection(); $database = DatabaseFactory::getFactory()->getConnection();
$sql = "SHOW TABLES FROM " . $database_name; $sql = "SHOW TABLES FROM `" . $database_name . "`";
$query = $database->prepare($sql); $query = $database->prepare($sql);
$query->execute(); $query->execute();
@@ -54,7 +53,7 @@ class DatabaseModel
$table_details = array(); $table_details = array();
foreach ($tables as $table) { 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 = $database->prepare($sql);
$query->execute(array(':table_name' => $table)); $query->execute(array(':table_name' => $table));
@@ -87,7 +86,7 @@ class DatabaseModel
$tables = self::getTablesInDatabase($database_name); $tables = self::getTablesInDatabase($database_name);
foreach ($tables as $table) { foreach ($tables as $table) {
$sql = "DESCRIBE " . $database_name . "." . $table; $sql = "DESCRIBE `" . $database_name . "`.`" . $table . "`";
$query = $database->prepare($sql); $query = $database->prepare($sql);
$query->execute(); $query->execute();
@@ -152,13 +151,106 @@ class DatabaseModel
{ {
$database = DatabaseFactory::getFactory()->getConnection(); $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 = $database->prepare($sql);
$query->execute(); $query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC); 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 * Format bytes to human readable format
* @param int $bytes * @param int $bytes

View File

@@ -0,0 +1,202 @@
<?php
/**
* Class DbUserModel
*
* Model for managing MySQL database users
*/
class DbUserModel
{
/**
* Get all database users
* @return array
*/
public static function getAllUsers()
{
$database = DatabaseFactory::getFactory()->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);
}
}

View File

@@ -0,0 +1,323 @@
<?php
class GalleryModel
{
private static $cipher = 'AES-256-CBC';
public static function getAllImages($user_id = null, $page = 1, $per_page = 20)
{
$database = DatabaseFactory::getFactory()->getConnection();
$offset = ($page - 1) * $per_page;
if ($user_id) {
$sql = "SELECT g.*, u.user_name
FROM gallery g
JOIN users u ON g.user_id = u.user_id
WHERE g.user_id = :user_id
ORDER BY g.created_at DESC
LIMIT :offset, :per_page";
$query = $database->prepare($sql);
$query->bindParam(':user_id', $user_id, PDO::PARAM_INT);
} else {
$sql = "SELECT g.*, u.user_name
FROM gallery g
JOIN users u ON g.user_id = u.user_id
WHERE g.is_public = 1
ORDER BY g.created_at DESC
LIMIT :offset, :per_page";
$query = $database->prepare($sql);
}
$query->bindParam(':offset', $offset, PDO::PARAM_INT);
$query->bindParam(':per_page', $per_page, PDO::PARAM_INT);
$query->execute();
return $query->fetchAll(PDO::FETCH_OBJ);
}
public static function getImageCount($user_id = null)
{
$database = DatabaseFactory::getFactory()->getConnection();
if ($user_id) {
$sql = "SELECT COUNT(*) as count FROM gallery WHERE user_id = :user_id";
$query = $database->prepare($sql);
$query->execute(array(':user_id' => $user_id));
} else {
$sql = "SELECT COUNT(*) as count FROM gallery WHERE is_public = 1";
$query = $database->prepare($sql);
$query->execute();
}
$result = $query->fetch(PDO::FETCH_ASSOC);
return (int)$result['count'];
}
public static function getImage($image_id)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT g.*, u.user_name
FROM gallery g
JOIN users u ON g.user_id = u.user_id
WHERE g.id = :image_id LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(':image_id' => $image_id));
return $query->fetch(PDO::FETCH_OBJ);
}
public static function uploadImage($file, $title, $description, $is_public)
{
$user_id = Session::get('user_id');
if (!$user_id) {
Session::add('feedback_negative', 'You must be logged in to upload');
return false;
}
$allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($file['type'], $allowed_types)) {
Session::add('feedback_negative', 'Invalid file type. Allowed: JPG, PNG, GIF, WebP');
return false;
}
$max_size = 10 * 1024 * 1024;
if ($file['size'] > $max_size) {
Session::add('feedback_negative', 'File too large. Maximum size: 10MB');
return false;
}
$image_data = file_get_contents($file['tmp_name']);
if ($image_data === false) {
Session::add('feedback_negative', 'Failed to read uploaded file');
return false;
}
$thumb_data = self::createThumbnailData($file['tmp_name'], $file['type'], 300);
if (!$thumb_data) {
Session::add('feedback_negative', 'Failed to create thumbnail');
return false;
}
$encryption_key = bin2hex(random_bytes(32));
$iv = random_bytes(openssl_cipher_iv_length(self::$cipher));
$encrypted_image = openssl_encrypt($image_data, self::$cipher, $encryption_key, OPENSSL_RAW_DATA, $iv);
$encrypted_thumb = openssl_encrypt($thumb_data, self::$cipher, $encryption_key, OPENSSL_RAW_DATA, $iv);
$encrypted_image = base64_encode($iv . $encrypted_image);
$encrypted_thumb = base64_encode($iv . $encrypted_thumb);
$upload_dir = dirname(__FILE__) . '/../../public/gallery_uploads/';
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0755, true);
}
$filename = uniqid('enc_') . '_' . time() . '.bin';
$thumb_filename = 'thumb_' . $filename;
if (file_put_contents($upload_dir . $filename, $encrypted_image) === false) {
Session::add('feedback_negative', 'Failed to save encrypted image');
return false;
}
if (file_put_contents($upload_dir . $thumb_filename, $encrypted_thumb) === false) {
unlink($upload_dir . $filename);
Session::add('feedback_negative', 'Failed to save encrypted thumbnail');
return false;
}
try {
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "INSERT INTO gallery (user_id, filename, thumb_filename, title, description, is_public, file_size, mime_type, encryption_key)
VALUES (:user_id, :filename, :thumb_filename, :title, :description, :is_public, :file_size, :mime_type, :encryption_key)";
$query = $database->prepare($sql);
$result = $query->execute(array(
':user_id' => $user_id,
':filename' => $filename,
':thumb_filename' => $thumb_filename,
':title' => $title ?: pathinfo($file['name'], PATHINFO_FILENAME),
':description' => $description,
':is_public' => $is_public ? 1 : 0,
':file_size' => $file['size'],
':mime_type' => $file['type'],
':encryption_key' => $encryption_key
));
if ($result) {
return $database->lastInsertId();
}
unlink($upload_dir . $filename);
unlink($upload_dir . $thumb_filename);
Session::add('feedback_negative', 'Failed to save to database');
return false;
} catch (PDOException $e) {
unlink($upload_dir . $filename);
unlink($upload_dir . $thumb_filename);
Session::add('feedback_negative', 'Database error: ' . $e->getMessage());
return false;
}
}
public static function getDecryptedImage($image_id, $thumbnail = false)
{
$image = self::getImage($image_id);
if (!$image) {
return null;
}
$upload_dir = dirname(__FILE__) . '/../../public/gallery_uploads/';
$filename = $thumbnail ? $image->thumb_filename : $image->filename;
$filepath = $upload_dir . $filename;
if (!file_exists($filepath)) {
return null;
}
$encrypted_data = file_get_contents($filepath);
if ($encrypted_data === false) {
return null;
}
$encrypted_data = base64_decode($encrypted_data);
$iv_length = openssl_cipher_iv_length(self::$cipher);
$iv = substr($encrypted_data, 0, $iv_length);
$encrypted_data = substr($encrypted_data, $iv_length);
$decrypted = openssl_decrypt($encrypted_data, self::$cipher, $image->encryption_key, OPENSSL_RAW_DATA, $iv);
return array(
'data' => $decrypted,
'mime_type' => $image->mime_type
);
}
public static function updateImage($image_id, $title, $description, $is_public)
{
$user_id = Session::get('user_id');
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE gallery SET title = :title, description = :description, is_public = :is_public
WHERE id = :image_id AND user_id = :user_id";
$query = $database->prepare($sql);
return $query->execute(array(
':image_id' => $image_id,
':user_id' => $user_id,
':title' => $title,
':description' => $description,
':is_public' => $is_public ? 1 : 0
));
}
public static function deleteImage($image_id)
{
$user_id = Session::get('user_id');
$image = self::getImage($image_id);
if (!$image || $image->user_id != $user_id) {
return false;
}
$upload_dir = dirname(__FILE__) . '/../../public/gallery_uploads/';
$filepath = $upload_dir . $image->filename;
$thumbpath = $upload_dir . $image->thumb_filename;
if (file_exists($filepath)) {
unlink($filepath);
}
if (file_exists($thumbpath)) {
unlink($thumbpath);
}
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "DELETE FROM gallery WHERE id = :image_id AND user_id = :user_id";
$query = $database->prepare($sql);
return $query->execute(array(':image_id' => $image_id, ':user_id' => $user_id));
}
private static function createThumbnailData($source, $mime_type, $max_size)
{
$image_info = getimagesize($source);
if (!$image_info) {
return false;
}
$width = $image_info[0];
$height = $image_info[1];
$type = $image_info[2];
switch ($type) {
case IMAGETYPE_JPEG:
$image = imagecreatefromjpeg($source);
break;
case IMAGETYPE_PNG:
$image = imagecreatefrompng($source);
break;
case IMAGETYPE_GIF:
$image = imagecreatefromgif($source);
break;
case IMAGETYPE_WEBP:
$image = imagecreatefromwebp($source);
break;
default:
return false;
}
if (!$image) {
return false;
}
$ratio = min($max_size / $width, $max_size / $height);
if ($ratio >= 1) {
$new_width = $width;
$new_height = $height;
} else {
$new_width = (int)($width * $ratio);
$new_height = (int)($height * $ratio);
}
$thumb = imagecreatetruecolor($new_width, $new_height);
if ($type == IMAGETYPE_PNG || $type == IMAGETYPE_GIF) {
imagealphablending($thumb, false);
imagesavealpha($thumb, true);
$transparent = imagecolorallocatealpha($thumb, 0, 0, 0, 127);
imagefilledrectangle($thumb, 0, 0, $new_width, $new_height, $transparent);
}
imagecopyresampled($thumb, $image, 0, 0, 0, 0, $new_width, $new_height, $width, $height);
ob_start();
switch ($type) {
case IMAGETYPE_JPEG:
imagejpeg($thumb, null, 85);
break;
case IMAGETYPE_PNG:
imagepng($thumb, null, 8);
break;
case IMAGETYPE_GIF:
imagegif($thumb);
break;
case IMAGETYPE_WEBP:
imagewebp($thumb, null, 85);
break;
}
$thumb_data = ob_get_clean();
return $thumb_data;
}
public static function formatFileSize($bytes)
{
if ($bytes >= 1048576) {
return number_format($bytes / 1048576, 2) . ' MB';
} elseif ($bytes >= 1024) {
return number_format($bytes / 1024, 2) . ' KB';
}
return $bytes . ' bytes';
}
}

View File

@@ -131,7 +131,6 @@ class TableModel
$database = DatabaseFactory::getFactory()->getConnection(); $database = DatabaseFactory::getFactory()->getConnection();
// Build column definitions
$column_definitions = array(); $column_definitions = array();
foreach ($columns as $column) { foreach ($columns as $column) {
$definition = "`" . $column['name'] . "` " . $column['type']; $definition = "`" . $column['name'] . "` " . $column['type'];
@@ -151,7 +150,6 @@ class TableModel
$column_definitions[] = $definition; $column_definitions[] = $definition;
} }
// Handle primary key
foreach ($columns as $column) { foreach ($columns as $column) {
if (isset($column['key']) && $column['key'] === 'PRI') { if (isset($column['key']) && $column['key'] === 'PRI') {
$column_definitions[] = "PRIMARY KEY (`" . $column['name'] . "`)"; $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 * Format bytes to human readable format
* @param int $bytes * @param int $bytes

View File

@@ -0,0 +1,105 @@
</main>
</div><!-- end dbm-main -->
<!-- SQL Console (Full Width at Bottom) -->
<div class="dbm-console expanded">
<div class="dbm-console-header">
<div class="dbm-console-title">
<i data-lucide="terminal" class="icon"></i>
SQL Console
</div>
<i data-lucide="chevron-up" class="dbm-console-toggle"></i>
</div>
<div class="dbm-console-body">
<form id="sql-form" method="post" action="<?php echo Config::get('URL'); ?>sql/execute">
<?php $current_database = isset($this->database_name) ? $this->database_name : Config::get('DB_NAME'); ?>
<input type="hidden" name="database_name" value="<?php echo htmlspecialchars($current_database); ?>">
<div class="dbm-sql-editor">
<div id="sql-highlight" class="dbm-sql-highlight"></div>
<textarea name="sql_query" id="sql_query" placeholder="SELECT * FROM table_name LIMIT 10;
-- Write your SQL query here
-- Press Execute or Ctrl+Enter to run"></textarea>
</div>
<div class="dbm-sql-actions">
<button type="submit" class="dbm-btn dbm-btn-success">
<i data-lucide="play"></i>
Execute
</button>
<button type="button" class="dbm-btn dbm-btn-secondary" onclick="document.getElementById('sql_query').value = ''; document.getElementById('sql-highlight').innerHTML = '';">
Clear
</button>
<select class="db-select" onchange="document.querySelector('input[name=database_name]').value = this.value;">
<?php foreach (DatabaseModel::getAllDatabases() as $db): ?>
<option value="<?php echo htmlspecialchars($db); ?>" <?php echo $db === $current_database ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($db); ?>
</option>
<?php endforeach; ?>
</select>
</div>
</form>
<div id="sql-result" class="dbm-sql-result">
<?php
// Check for session result
$result = Session::get('sql_result');
if ($result) {
Session::set('sql_result', null);
if ($result['success']) {
echo '<div class="dbm-sql-result success">';
echo '<div class="dbm-sql-result-header">';
echo '<i data-lucide="check-circle"></i>';
echo htmlspecialchars($result['message']);
echo '<span style="margin-left: auto; color: var(--text-muted); font-size: 12px;">' . $result['execution_time'] . 'ms</span>';
echo '</div>';
if (!empty($result['result'])) {
echo '<div class="dbm-sql-result-body"><div class="dbm-table-wrapper"><table class="dbm-table"><thead><tr>';
foreach (array_keys($result['result'][0]) as $col) {
echo '<th>' . htmlspecialchars($col) . '</th>';
}
echo '</tr></thead><tbody>';
foreach ($result['result'] as $row) {
echo '<tr>';
foreach ($row as $value) {
echo '<td>' . ($value === null ? '<span class="null-value">NULL</span>' : htmlspecialchars(substr($value, 0, 100))) . '</td>';
}
echo '</tr>';
}
echo '</tbody></table></div></div>';
}
echo '</div>';
} else {
echo '<div class="dbm-sql-result error">';
echo '<div class="dbm-sql-result-header">';
echo '<i data-lucide="x-circle"></i>';
echo htmlspecialchars($result['message']);
echo '</div>';
if (!empty($result['error'])) {
echo '<div class="dbm-sql-result-body" style="padding: 16px; font-family: monospace; font-size: 13px; color: var(--accent-red);">' . htmlspecialchars($result['error']) . '</div>';
}
echo '</div>';
}
}
?>
</div>
</div>
</div>
</div><!-- end dbm-wrapper -->
</div><!-- end wrapper -->
<script src="<?php echo Config::get('URL'); ?>js/dbmanager.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
lucide.createIcons();
DBManager.init('<?php echo Config::get('URL'); ?>');
});
</script>
</body>
</html>

View File

@@ -0,0 +1,107 @@
<!doctype html>
<html>
<head>
<title>Database Manager - HUGE</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:;base64,=">
<link rel="stylesheet" href="<?php echo Config::get('URL'); ?>css/style.css" />
<link rel="stylesheet" href="<?php echo Config::get('URL'); ?>css/dbmanager.css" />
<script src="https://unpkg.com/lucide@latest"></script>
</head>
<body>
<div class="wrapper dbm-page-wrapper">
<?php
$uri = trim($_SERVER['REQUEST_URI'], '/');
$uri_parts = explode('/', $uri);
$current_controller = isset($uri_parts[0]) ? strtolower($uri_parts[0]) : 'database';
$is_db_page = in_array($current_controller, ['database', 'table', 'sql']);
$is_user_page = ($current_controller === 'dbuser');
?>
<ul class="navigation">
<li><a href="<?php echo Config::get('URL'); ?>index/index">Home</a></li>
<li class="<?php echo $is_db_page ? 'active' : ''; ?>"><a href="<?php echo Config::get('URL'); ?>database/index">Database</a></li>
<li class="<?php echo $is_user_page ? 'active' : ''; ?>"><a href="<?php echo Config::get('URL'); ?>dbuser/index">Users</a></li>
</ul>
<ul class="navigation right">
<li><a href="<?php echo Config::get('URL'); ?>admin/">Admin</a></li>
<li><a href="<?php echo Config::get('URL'); ?>login/logout">Logout</a></li>
</ul>
<div class="dbm-wrapper">
<div class="dbm-main">
<aside class="dbm-sidebar">
<div class="dbm-sidebar-header">
<i data-lucide="database" class="icon"></i>
<h3>Databases</h3>
</div>
<nav class="dbm-tree">
<?php
$all_databases = DatabaseModel::getAllDatabases();
$current_database = isset($this->database_name) ? $this->database_name : Config::get('DB_NAME');
$current_table = isset($this->table_name) ? $this->table_name : null;
foreach ($all_databases as $db):
$is_current_db = ($db === $current_database);
$tables = $is_current_db ? DatabaseModel::getTablesInDatabase($db) : [];
?>
<div class="tree-item <?php echo $is_current_db ? 'expanded' : ''; ?>" data-db="<?php echo htmlspecialchars($db); ?>">
<div class="tree-header <?php echo $is_current_db ? 'active' : ''; ?>" data-href="<?php echo Config::get('URL'); ?>database/show/<?php echo urlencode($db); ?>">
<span class="tree-toggle">
<i data-lucide="chevron-right"></i>
</span>
<span class="tree-icon database">
<i data-lucide="database"></i>
</span>
<span class="tree-label"><?php echo htmlspecialchars($db); ?></span>
<?php if (!empty($tables)): ?>
<span class="tree-badge"><?php echo count($tables); ?></span>
<?php endif; ?>
</div>
<div class="tree-children" <?php echo $is_current_db ? 'data-loaded="true"' : ''; ?>>
<?php if ($is_current_db && !empty($tables)): ?>
<?php foreach ($tables as $table):
$is_current_table = ($table === $current_table);
$columns = $is_current_table ? TableModel::getTableColumns($db, $table) : [];
?>
<div class="tree-item <?php echo $is_current_table ? 'expanded' : ''; ?>" data-table="<?php echo htmlspecialchars($table); ?>">
<div class="tree-header <?php echo $is_current_table ? 'active' : ''; ?>" data-href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($db); ?>/<?php echo urlencode($table); ?>">
<span class="tree-toggle">
<i data-lucide="chevron-right"></i>
</span>
<span class="tree-icon table">
<i data-lucide="table"></i>
</span>
<span class="tree-label"><?php echo htmlspecialchars($table); ?></span>
</div>
<div class="tree-children"<?php echo $is_current_table ? ' data-loaded="true"' : ''; ?>>
<?php if ($is_current_table && !empty($columns)): ?>
<?php foreach ($columns as $col): ?>
<div class="tree-item">
<div class="tree-header">
<span class="tree-icon <?php echo $col['Key'] === 'PRI' ? 'key' : 'column'; ?>">
<?php if ($col['Key'] === 'PRI'): ?>
<i data-lucide="key-round"></i>
<?php else: ?>
<i data-lucide="columns-2"></i>
<?php endif; ?>
</span>
<span class="tree-label"><?php echo htmlspecialchars($col['Field']); ?></span>
<span class="tree-badge"><?php echo htmlspecialchars($col['Type']); ?></span>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</nav>
</aside>
<main class="dbm-content">

View File

@@ -8,6 +8,7 @@
<link rel="icon" href="data:;base64,="> <link rel="icon" href="data:;base64,=">
<!-- CSS --> <!-- CSS -->
<link rel="stylesheet" href="<?php echo Config::get('URL'); ?>css/style.css" /> <link rel="stylesheet" href="<?php echo Config::get('URL'); ?>css/style.css" />
<link rel="stylesheet" href="<?php echo Config::get('URL'); ?>css/gallery.css" />
</head> </head>
<body> <body>
<!-- wrapper, to center website --> <!-- wrapper, to center website -->
@@ -27,6 +28,9 @@
<li <?php if (View::checkForActiveController($filename, "directory")) { echo ' class="active" '; } ?> > <li <?php if (View::checkForActiveController($filename, "directory")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>directory/index">Benutzer</a> <a href="<?php echo Config::get('URL'); ?>directory/index">Benutzer</a>
</li> </li>
<li <?php if (View::checkForActiveController($filename, "gallery")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>gallery/index">Gallery</a>
</li>
<?php if (Session::userIsLoggedIn()) { ?> <?php if (Session::userIsLoggedIn()) { ?>
<li <?php if (View::checkForActiveController($filename, "dashboard")) { echo ' class="active" '; } ?> > <li <?php if (View::checkForActiveController($filename, "dashboard")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>dashboard/index">Dashboard</a> <a href="<?php echo Config::get('URL'); ?>dashboard/index">Dashboard</a>
@@ -80,6 +84,11 @@
</ul> </ul>
</li> </li>
<?php if (Session::get("user_account_type") == 7) : ?> <?php if (Session::get("user_account_type") == 7) : ?>
<li <?php if (View::checkForActiveControllers($filename, ['database', 'table', 'sql', 'dbuser'])) {
echo ' class="active" ';
} ?> >
<a href="<?php echo Config::get('URL'); ?>database/index">Database</a>
</li>
<li <?php if (View::checkForActiveController($filename, "admin")) { <li <?php if (View::checkForActiveController($filename, "admin")) {
echo ' class="active" '; echo ' class="active" ';
} ?> > } ?> >

View File

@@ -0,0 +1,86 @@
<div class="dbm-content-header">
<div class="dbm-breadcrumb">
<a href="<?php echo Config::get('URL'); ?>database/index">Databases</a>
</div>
<div class="dbm-title">
<h1>All Databases</h1>
<span class="badge"><?php echo count($this->databases); ?> total</span>
</div>
<div class="dbm-actions">
<button type="button" class="dbm-btn dbm-btn-success" onclick="document.getElementById('create-db-modal').style.display='flex'">
<i data-lucide="plus"></i>
Create Database
</button>
</div>
</div>
<div class="dbm-content-body">
<div class="dbm-stats">
<div class="dbm-stat">
<div class="dbm-stat-value"><?php echo count($this->databases); ?></div>
<div class="dbm-stat-label">Databases</div>
</div>
<div class="dbm-stat">
<div class="dbm-stat-value"><?php echo htmlspecialchars($this->current_db); ?></div>
<div class="dbm-stat-label">Current Database</div>
</div>
</div>
<div class="dbm-table-wrapper">
<table class="dbm-table">
<thead>
<tr>
<th>Database Name</th>
<th>Tables</th>
<th style="width: 200px;">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($this->databases as $db):
$tables = DatabaseModel::getTablesInDatabase($db);
?>
<tr>
<td>
<a href="<?php echo Config::get('URL'); ?>database/show/<?php echo urlencode($db); ?>" style="color: var(--accent-blue); text-decoration: none;">
<?php echo htmlspecialchars($db); ?>
</a>
<?php if ($db === $this->current_db): ?>
<span style="margin-left: 8px; font-size: 10px; padding: 2px 6px; background: var(--accent-green); color: #fff; border-radius: 3px;">ACTIVE</span>
<?php endif; ?>
</td>
<td><?php echo count($tables); ?></td>
<td>
<a href="<?php echo Config::get('URL'); ?>database/show/<?php echo urlencode($db); ?>" class="dbm-btn dbm-btn-sm dbm-btn-secondary">Browse</a>
<?php if ($db !== $this->current_db): ?>
<a href="<?php echo Config::get('URL'); ?>database/delete/<?php echo urlencode($db); ?>"
class="dbm-btn dbm-btn-sm dbm-btn-danger"
data-confirm="Delete database '<?php echo htmlspecialchars($db); ?>'? This cannot be undone!">Drop</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Create Database Modal -->
<div id="create-db-modal" style="display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.8); z-index:1000; align-items:center; justify-content:center;">
<div class="dbm-card" style="width: 400px; max-width: 90%;">
<div class="dbm-card-header">
<h3>Create New Database</h3>
</div>
<div class="dbm-card-body">
<form method="post" action="<?php echo Config::get('URL'); ?>database/create" data-ajax-form>
<div class="dbm-form-group">
<label class="dbm-form-label">Database Name</label>
<input type="text" name="database_name" class="dbm-form-input" required pattern="[a-zA-Z0-9_]+" placeholder="my_database" style="width:100%;max-width:100%;">
</div>
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
<button type="button" class="dbm-btn dbm-btn-secondary" onclick="document.getElementById('create-db-modal').style.display='none'">Cancel</button>
<button type="submit" class="dbm-btn dbm-btn-success">Create</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -0,0 +1,76 @@
<div class="dbm-content-header">
<div class="dbm-breadcrumb">
<a href="<?php echo Config::get('URL'); ?>database/index">Databases</a>
<span class="separator">/</span>
<span><?php echo htmlspecialchars($this->database_name); ?></span>
</div>
<div class="dbm-title">
<h1><?php echo htmlspecialchars($this->database_name); ?></h1>
<span class="badge"><?php echo count($this->tables); ?> tables</span>
</div>
<div class="dbm-actions">
<a href="<?php echo Config::get('URL'); ?>table/create/<?php echo urlencode($this->database_name); ?>" class="dbm-btn dbm-btn-success">
<i data-lucide="plus"></i>
New Table
</a>
<a href="<?php echo Config::get('URL'); ?>database/export/<?php echo urlencode($this->database_name); ?>" class="dbm-btn dbm-btn-secondary" target="_blank">
<i data-lucide="download"></i>
Export SQL
</a>
<a href="<?php echo Config::get('URL'); ?>sql/index/<?php echo urlencode($this->database_name); ?>" class="dbm-btn dbm-btn-secondary">
<i data-lucide="terminal"></i>
SQL Console
</a>
</div>
</div>
<div class="dbm-content-body">
<?php if (empty($this->tables)): ?>
<div class="dbm-empty">
<i data-lucide="table" class="dbm-empty-icon"></i>
<h3>No tables yet</h3>
<p>This database is empty. Create your first table to get started.</p>
<a href="<?php echo Config::get('URL'); ?>table/create/<?php echo urlencode($this->database_name); ?>" class="dbm-btn dbm-btn-success" style="margin-top: 16px;">Create Table</a>
</div>
<?php else: ?>
<div class="dbm-table-wrapper">
<table class="dbm-table">
<thead>
<tr>
<th>Table Name</th>
<th>Engine</th>
<th>Rows</th>
<th>Size</th>
<th>Collation</th>
<th style="width: 220px;">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($this->tables as $table):
$info = isset($this->table_info[$table]) ? $this->table_info[$table] : [];
?>
<tr>
<td>
<a href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($table); ?>" style="color: var(--accent-blue); text-decoration: none; font-weight: 500;">
<?php echo htmlspecialchars($table); ?>
</a>
</td>
<td><span class="type-column"><?php echo isset($info['engine']) ? htmlspecialchars($info['engine']) : '-'; ?></span></td>
<td><?php echo isset($info['rows']) ? number_format($info['rows']) : '-'; ?></td>
<td><?php echo isset($info['total_size']) ? $info['total_size'] : '-'; ?></td>
<td><span style="font-size: 11px;"><?php echo isset($info['collation']) ? htmlspecialchars($info['collation']) : '-'; ?></span></td>
<td>
<a href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($table); ?>" class="dbm-btn dbm-btn-sm dbm-btn-primary">Browse</a>
<a href="<?php echo Config::get('URL'); ?>table/structure/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($table); ?>" class="dbm-btn dbm-btn-sm dbm-btn-secondary">Structure</a>
<a href="<?php echo Config::get('URL'); ?>table/export/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($table); ?>" class="dbm-btn dbm-btn-sm dbm-btn-secondary" target="_blank">Export</a>
<a href="<?php echo Config::get('URL'); ?>table/delete/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($table); ?>"
class="dbm-btn dbm-btn-sm dbm-btn-danger"
data-confirm="Drop table '<?php echo htmlspecialchars($table); ?>'? This cannot be undone!">Drop</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>

View File

@@ -0,0 +1,68 @@
<div class="dbm-content-header">
<div class="dbm-breadcrumb">
<a href="<?php echo Config::get('URL'); ?>dbuser/index">Users</a>
<span class="separator">/</span>
<span>Create User</span>
</div>
<div class="dbm-title">
<h1>Create New User</h1>
</div>
</div>
<div class="dbm-content-body">
<div class="dbm-card">
<div class="dbm-card-body">
<form method="post" action="<?php echo Config::get('URL'); ?>dbuser/create">
<div class="dbm-form-group">
<label class="dbm-form-label">Username</label>
<input type="text" name="username" class="dbm-form-input" required pattern="[a-zA-Z0-9_]+" placeholder="username">
</div>
<div class="dbm-form-group">
<label class="dbm-form-label">Password</label>
<input type="password" name="password" class="dbm-form-input" required placeholder="password">
</div>
<div class="dbm-form-group">
<label class="dbm-form-label">Host</label>
<select name="host" class="dbm-form-select">
<option value="localhost">localhost</option>
<option value="%">% (any host)</option>
<option value="127.0.0.1">127.0.0.1</option>
</select>
</div>
<div class="dbm-form-group">
<label class="dbm-form-label">Privileges</label>
<div style="margin-top: 8px; padding: 12px; background: var(--dbm-bg-secondary); border-radius: var(--dbm-radius);">
<div style="margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid var(--dbm-border);">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 13px; font-weight: 500; color: var(--dbm-text);">
<input type="checkbox" name="privileges[]" value="ALL PRIVILEGES" id="all-privs-check"
onchange="document.querySelectorAll('.priv-checkbox').forEach(cb => { cb.checked = this.checked; cb.disabled = this.checked; })">
ALL PRIVILEGES (*)
</label>
<small style="color: var(--dbm-text-muted); font-size: 11px; margin-left: 22px;">Grant all privileges on all databases</small>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px;">
<?php
$all_privileges = ['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'];
foreach ($all_privileges as $priv):
?>
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 12px; color: var(--dbm-text-secondary);">
<input type="checkbox" name="privileges[]" value="<?php echo $priv; ?>" class="priv-checkbox">
<?php echo $priv; ?>
</label>
<?php endforeach; ?>
</div>
</div>
</div>
<div style="margin-top: 20px; display: flex; gap: 8px;">
<button type="submit" name="submit_create_user" class="dbm-btn dbm-btn-success">
<i data-lucide="plus"></i>
Create User
</button>
<a href="<?php echo Config::get('URL'); ?>dbuser/index" class="dbm-btn dbm-btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>

View File

@@ -0,0 +1,92 @@
<div class="dbm-content-header">
<div class="dbm-breadcrumb">
<a href="<?php echo Config::get('URL'); ?>dbuser/index">Users</a>
<span class="separator">/</span>
<span><?php echo htmlspecialchars($this->user->User); ?>@<?php echo htmlspecialchars($this->user->Host); ?></span>
</div>
<div class="dbm-title">
<h1>Edit User</h1>
</div>
</div>
<div class="dbm-content-body">
<form method="post" action="<?php echo Config::get('URL'); ?>dbuser/edit/<?php echo urlencode($this->user->User); ?>/<?php echo urlencode($this->user->Host); ?>">
<div class="dbm-card" style="margin-bottom: 20px;">
<div class="dbm-card-header">
<h3>User Details</h3>
</div>
<div class="dbm-card-body">
<div class="dbm-form-group">
<label class="dbm-form-label">Username</label>
<input type="text" class="dbm-form-input" value="<?php echo htmlspecialchars($this->user->User); ?>" disabled>
</div>
<div class="dbm-form-group">
<label class="dbm-form-label">Host</label>
<input type="text" class="dbm-form-input" value="<?php echo htmlspecialchars($this->user->Host); ?>" disabled>
</div>
<div class="dbm-form-group">
<label class="dbm-form-label">New Password</label>
<input type="password" name="password" class="dbm-form-input" placeholder="Leave empty to keep current password">
<small style="color: var(--dbm-text-muted); font-size: 11px; display: block; margin-top: 4px;">Only fill this if you want to change the password</small>
</div>
</div>
</div>
<div class="dbm-card" style="margin-bottom: 20px;">
<div class="dbm-card-header">
<h3>Global Privileges</h3>
</div>
<div class="dbm-card-body">
<?php
$all_privileges = ['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'];
$current_grants = implode(' ', $this->privileges);
$has_all = stripos($current_grants, 'ALL PRIVILEGES') !== false;
?>
<div style="margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid var(--dbm-border);">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 13px; font-weight: 500; color: var(--dbm-text);">
<input type="checkbox" name="privileges[]" value="ALL PRIVILEGES" id="all-privs-check"
<?php echo $has_all ? 'checked' : ''; ?>
onchange="document.querySelectorAll('.priv-checkbox').forEach(cb => { cb.checked = this.checked; cb.disabled = this.checked; })">
ALL PRIVILEGES (*)
</label>
<small style="color: var(--dbm-text-muted); font-size: 11px; margin-left: 22px;">Grant all privileges on all databases</small>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px;">
<?php foreach ($all_privileges as $priv): ?>
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 12px; color: var(--dbm-text-secondary);">
<input type="checkbox" name="privileges[]" value="<?php echo $priv; ?>" class="priv-checkbox"
<?php echo ($has_all || stripos($current_grants, $priv) !== false) ? 'checked' : ''; ?>
<?php echo $has_all ? 'disabled' : ''; ?>>
<?php echo $priv; ?>
</label>
<?php endforeach; ?>
</div>
</div>
</div>
<div class="dbm-card">
<div class="dbm-card-header">
<h3>Current Grants</h3>
</div>
<div class="dbm-card-body">
<?php if (!empty($this->privileges)): ?>
<?php foreach ($this->privileges as $grant): ?>
<div style="font-family: monospace; font-size: 11px; padding: 8px; background: var(--dbm-bg-secondary); border-radius: var(--dbm-radius); margin-bottom: 6px; word-break: break-all;">
<?php echo htmlspecialchars($grant); ?>
</div>
<?php endforeach; ?>
<?php else: ?>
<p style="color: var(--dbm-text-muted); font-size: 12px; margin: 0;">No grants found</p>
<?php endif; ?>
</div>
</div>
<div style="margin-top: 20px; display: flex; gap: 8px;">
<button type="submit" name="submit_edit_user" class="dbm-btn dbm-btn-success">
<i data-lucide="check"></i>
Save Changes
</button>
<a href="<?php echo Config::get('URL'); ?>dbuser/index" class="dbm-btn dbm-btn-secondary">Cancel</a>
</div>
</form>
</div>

View File

@@ -0,0 +1,101 @@
<div class="dbm-content-header">
<div class="dbm-breadcrumb">
<span>Users</span>
</div>
<div class="dbm-title">
<h1>MySQL Users</h1>
<span class="badge"><?php echo count($this->users); ?> users</span>
</div>
<div class="dbm-actions">
<button type="button" class="dbm-btn dbm-btn-success" onclick="document.getElementById('create-user-modal').style.display='flex'">
<i data-lucide="plus"></i>
Create User
</button>
</div>
</div>
<div class="dbm-content-body">
<div class="dbm-card" style="margin-bottom: 16px;">
<div class="dbm-card-body" style="padding: 12px 16px;">
<span style="color: var(--dbm-text-muted); font-size: 12px;">Connected as:</span>
<strong style="margin-left: 6px; color: var(--dbm-text);"><?php echo htmlspecialchars($this->current_user); ?></strong>
</div>
</div>
<div class="dbm-table-wrapper">
<table class="dbm-table">
<thead>
<tr>
<th>Username</th>
<th>Host</th>
<th style="width: 200px;">Actions</th>
</tr>
</thead>
<tbody>
<?php if (!empty($this->users)): ?>
<?php foreach ($this->users as $user): ?>
<tr>
<td style="font-weight: 500; color: var(--dbm-text);">
<i data-lucide="user" style="width: 12px; height: 12px; margin-right: 6px; color: var(--dbm-text-muted);"></i>
<?php echo htmlspecialchars($user->User); ?>
<?php if ($user->User === $this->current_user): ?>
<span class="badge" style="margin-left: 6px; font-size: 9px;">current</span>
<?php endif; ?>
</td>
<td><span class="type-column"><?php echo htmlspecialchars($user->Host); ?></span></td>
<td>
<a href="<?php echo Config::get('URL'); ?>dbuser/edit/<?php echo urlencode($user->User); ?>/<?php echo urlencode($user->Host); ?>" class="dbm-btn dbm-btn-sm dbm-btn-secondary">
<i data-lucide="pencil" style="width: 11px; height: 11px;"></i>
Edit
</a>
<?php if ($user->User !== $this->current_user): ?>
<a href="<?php echo Config::get('URL'); ?>dbuser/delete/<?php echo urlencode($user->User); ?>/<?php echo urlencode($user->Host); ?>"
class="dbm-btn dbm-btn-sm dbm-btn-danger"
data-confirm="Delete user '<?php echo htmlspecialchars($user->User); ?>'@'<?php echo htmlspecialchars($user->Host); ?>'? This cannot be undone!">
<i data-lucide="trash-2" style="width: 11px; height: 11px;"></i>
Delete
</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="3" style="text-align: center; padding: 40px; color: var(--dbm-text-muted);">
No users found
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<div id="create-user-modal" class="dbm-modal" style="display: none;">
<div class="dbm-modal-content">
<div class="dbm-modal-header">
<h3>Create New User</h3>
<button type="button" class="dbm-modal-close" onclick="this.closest('.dbm-modal').style.display='none'">&times;</button>
</div>
<form method="post" action="<?php echo Config::get('URL'); ?>dbuser/create">
<div class="dbm-modal-body">
<div class="dbm-form-group">
<label class="dbm-form-label">Username</label>
<input type="text" name="username" class="dbm-form-input" required pattern="[a-zA-Z0-9_]+" placeholder="username">
</div>
<div class="dbm-form-group">
<label class="dbm-form-label">Password</label>
<input type="password" name="password" class="dbm-form-input" required placeholder="password">
</div>
<div class="dbm-form-group">
<label class="dbm-form-label">Host</label>
<input type="text" name="host" class="dbm-form-input" required value="localhost" placeholder="localhost or %">
</div>
</div>
<div class="dbm-modal-footer">
<button type="button" class="dbm-btn dbm-btn-secondary" onclick="this.closest('.dbm-modal').style.display='none'">Cancel</button>
<button type="submit" name="submit_create_user" class="dbm-btn dbm-btn-success">Create User</button>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,37 @@
<div class="dbm-content-header">
<div class="dbm-breadcrumb">
<a href="<?php echo Config::get('URL'); ?>dbuser/index">Users</a>
<span class="separator">/</span>
<span><?php echo htmlspecialchars($this->user->User); ?>@<?php echo htmlspecialchars($this->user->Host); ?></span>
<span class="separator">/</span>
<span>Privileges</span>
</div>
<div class="dbm-title">
<h1>User Privileges</h1>
</div>
<div class="dbm-actions">
<a href="<?php echo Config::get('URL'); ?>dbuser/edit/<?php echo urlencode($this->user->User); ?>/<?php echo urlencode($this->user->Host); ?>" class="dbm-btn dbm-btn-secondary">
<i data-lucide="pencil"></i>
Edit User
</a>
</div>
</div>
<div class="dbm-content-body">
<div class="dbm-card">
<div class="dbm-card-header">
<h3>Grant Statements</h3>
</div>
<div class="dbm-card-body">
<?php if (!empty($this->privileges)): ?>
<?php foreach ($this->privileges as $grant): ?>
<div style="font-family: monospace; font-size: 11px; padding: 10px; background: var(--dbm-bg-secondary); border-radius: var(--dbm-radius); margin-bottom: 8px; word-break: break-all; border: 1px solid var(--dbm-border);">
<?php echo htmlspecialchars($grant); ?>
</div>
<?php endforeach; ?>
<?php else: ?>
<p style="color: var(--dbm-text-muted); font-size: 13px; margin: 0;">No privileges found for this user.</p>
<?php endif; ?>
</div>
</div>
</div>

View File

@@ -0,0 +1,51 @@
<div class="gallery-container">
<div class="gallery-header">
<a href="<?php echo Config::get('URL'); ?>gallery/view/<?php echo $this->image->id; ?>" class="gallery-back">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
Back to Image
</a>
</div>
<div class="gallery-form-container">
<h1>Edit Image</h1>
<div class="gallery-edit-preview">
<img src="<?php echo Config::get('URL'); ?>gallery/image/<?php echo $this->image->id; ?>/thumb"
alt="<?php echo htmlspecialchars($this->image->title); ?>">
</div>
<form method="post" action="<?php echo Config::get('URL'); ?>gallery/edit/<?php echo $this->image->id; ?>" class="gallery-form">
<div class="gallery-form-group">
<label class="gallery-form-label" for="title">Title</label>
<input type="text" name="title" id="title" class="gallery-form-input"
value="<?php echo htmlspecialchars($this->image->title); ?>" required>
</div>
<div class="gallery-form-group">
<label class="gallery-form-label" for="description">Description</label>
<textarea name="description" id="description" class="gallery-form-textarea" rows="4"><?php echo htmlspecialchars($this->image->description); ?></textarea>
</div>
<div class="gallery-form-group">
<label class="gallery-form-checkbox">
<input type="checkbox" name="is_public" value="1" <?php echo $this->image->is_public ? 'checked' : ''; ?>>
<span>Make this image public</span>
</label>
<small class="gallery-form-hint">Public images are visible to everyone in the gallery</small>
</div>
<div class="gallery-form-actions">
<button type="submit" name="submit_edit" class="gallery-btn gallery-btn-primary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
Save Changes
</button>
<a href="<?php echo Config::get('URL'); ?>gallery/view/<?php echo $this->image->id; ?>" class="gallery-btn gallery-btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,59 @@
<div class="gallery-container">
<div class="gallery-header">
<h1>Gallery</h1>
<div class="gallery-actions">
<?php if (Session::userIsLoggedIn()): ?>
<a href="<?php echo Config::get('URL'); ?>gallery/my" class="gallery-btn gallery-btn-secondary">My Images</a>
<a href="<?php echo Config::get('URL'); ?>gallery/upload" class="gallery-btn gallery-btn-primary">Upload Image</a>
<?php endif; ?>
</div>
</div>
<?php if (empty($this->images)): ?>
<div class="gallery-empty">
<div class="gallery-empty-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</div>
<h3>No images yet</h3>
<p>Be the first to share an image!</p>
<?php if (Session::userIsLoggedIn()): ?>
<a href="<?php echo Config::get('URL'); ?>gallery/upload" class="gallery-btn gallery-btn-primary" style="margin-top: 16px;">Upload Image</a>
<?php endif; ?>
</div>
<?php else: ?>
<div class="gallery-grid">
<?php foreach ($this->images as $image): ?>
<a href="<?php echo Config::get('URL'); ?>gallery/view/<?php echo $image->id; ?>" class="gallery-item">
<img src="<?php echo Config::get('URL'); ?>gallery/image/<?php echo $image->id; ?>/thumb"
alt="<?php echo htmlspecialchars($image->title); ?>"
loading="lazy">
<div class="gallery-item-overlay">
<span class="gallery-item-title"><?php echo htmlspecialchars($image->title); ?></span>
<span class="gallery-item-author">by <?php echo htmlspecialchars($image->user_name); ?></span>
</div>
</a>
<?php endforeach; ?>
</div>
<?php
$total_pages = ceil($this->total_images / $this->per_page);
if ($total_pages > 1):
?>
<div class="gallery-pagination">
<?php if ($this->current_page > 1): ?>
<a href="<?php echo Config::get('URL'); ?>gallery/index/<?php echo $this->current_page - 1; ?>" class="gallery-btn gallery-btn-secondary">Previous</a>
<?php endif; ?>
<span class="gallery-pagination-info">Page <?php echo $this->current_page; ?> of <?php echo $total_pages; ?></span>
<?php if ($this->current_page < $total_pages): ?>
<a href="<?php echo Config::get('URL'); ?>gallery/index/<?php echo $this->current_page + 1; ?>" class="gallery-btn gallery-btn-secondary">Next</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>

View File

@@ -0,0 +1,76 @@
<div class="gallery-container">
<div class="gallery-header">
<h1>My Images</h1>
<div class="gallery-actions">
<a href="<?php echo Config::get('URL'); ?>gallery/index" class="gallery-btn gallery-btn-secondary">Public Gallery</a>
<a href="<?php echo Config::get('URL'); ?>gallery/upload" class="gallery-btn gallery-btn-primary">Upload Image</a>
</div>
</div>
<?php if (empty($this->images)): ?>
<div class="gallery-empty">
<div class="gallery-empty-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</div>
<h3>No images uploaded</h3>
<p>Upload your first image to get started.</p>
<a href="<?php echo Config::get('URL'); ?>gallery/upload" class="gallery-btn gallery-btn-primary" style="margin-top: 16px;">Upload Image</a>
</div>
<?php else: ?>
<div class="gallery-grid">
<?php foreach ($this->images as $image): ?>
<div class="gallery-item gallery-item-owned">
<a href="<?php echo Config::get('URL'); ?>gallery/view/<?php echo $image->id; ?>">
<img src="<?php echo Config::get('URL'); ?>gallery/image/<?php echo $image->id; ?>/thumb"
alt="<?php echo htmlspecialchars($image->title); ?>"
loading="lazy">
</a>
<div class="gallery-item-overlay">
<span class="gallery-item-title"><?php echo htmlspecialchars($image->title); ?></span>
<span class="gallery-item-visibility <?php echo $image->is_public ? 'public' : 'private'; ?>">
<?php echo $image->is_public ? 'Public' : 'Private'; ?>
</span>
</div>
<div class="gallery-item-actions">
<a href="<?php echo Config::get('URL'); ?>gallery/edit/<?php echo $image->id; ?>" class="gallery-btn-icon" title="Edit">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</a>
<a href="<?php echo Config::get('URL'); ?>gallery/delete/<?php echo $image->id; ?>"
class="gallery-btn-icon gallery-btn-danger"
title="Delete"
onclick="return confirm('Delete this image?');">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</a>
</div>
</div>
<?php endforeach; ?>
</div>
<?php
$total_pages = ceil($this->total_images / $this->per_page);
if ($total_pages > 1):
?>
<div class="gallery-pagination">
<?php if ($this->current_page > 1): ?>
<a href="<?php echo Config::get('URL'); ?>gallery/my/<?php echo $this->current_page - 1; ?>" class="gallery-btn gallery-btn-secondary">Previous</a>
<?php endif; ?>
<span class="gallery-pagination-info">Page <?php echo $this->current_page; ?> of <?php echo $total_pages; ?></span>
<?php if ($this->current_page < $total_pages): ?>
<a href="<?php echo Config::get('URL'); ?>gallery/my/<?php echo $this->current_page + 1; ?>" class="gallery-btn gallery-btn-secondary">Next</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>

View File

@@ -0,0 +1,168 @@
<div class="gallery-success-overlay">
<div class="gallery-success-card">
<div class="gallery-success-icon">
<svg class="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
<circle class="checkmark-circle" cx="26" cy="26" r="25" fill="none"/>
<path class="checkmark-check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/>
</svg>
</div>
<h2>Upload Successful!</h2>
<p>Your image has been uploaded successfully.</p>
<div class="gallery-success-preview">
<img src="<?php echo Config::get('URL'); ?>gallery/image/<?php echo $this->image->id; ?>/thumb"
alt="<?php echo htmlspecialchars($this->image->title); ?>">
</div>
<p class="gallery-success-title"><?php echo htmlspecialchars($this->image->title); ?></p>
<p class="gallery-success-redirect">Redirecting to your image<span class="dots"></span></p>
</div>
</div>
<style>
.gallery-success-overlay {
position: fixed;
inset: 0;
background: rgba(255, 255, 255, 0.98);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.gallery-success-card {
text-align: center;
padding: 40px;
max-width: 400px;
animation: slideUp 0.4s ease 0.1s both;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.gallery-success-icon {
margin-bottom: 24px;
}
.checkmark {
width: 80px;
height: 80px;
border-radius: 50%;
display: block;
stroke-width: 2;
stroke: #4CAF50;
stroke-miterlimit: 10;
margin: 0 auto;
box-shadow: inset 0px 0px 0px #4CAF50;
animation: fill 0.4s ease-in-out 0.4s forwards, scale 0.3s ease-in-out 0.9s both;
}
.checkmark-circle {
stroke-dasharray: 166;
stroke-dashoffset: 166;
stroke-width: 2;
stroke-miterlimit: 10;
stroke: #4CAF50;
fill: none;
animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
}
.checkmark-check {
transform-origin: 50% 50%;
stroke-dasharray: 48;
stroke-dashoffset: 48;
animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards;
}
@keyframes stroke {
100% { stroke-dashoffset: 0; }
}
@keyframes scale {
0%, 100% { transform: none; }
50% { transform: scale3d(1.1, 1.1, 1); }
}
@keyframes fill {
100% { box-shadow: inset 0px 0px 0px 40px rgba(76, 175, 80, 0.1); }
}
.gallery-success-card h2 {
margin: 0 0 8px;
font-size: 28px;
font-weight: 600;
color: #333;
}
.gallery-success-card p {
margin: 0;
color: #666;
font-size: 16px;
}
.gallery-success-preview {
margin: 24px 0 16px;
animation: previewFade 0.5s ease 0.6s both;
}
@keyframes previewFade {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
.gallery-success-preview img {
max-width: 200px;
max-height: 200px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
object-fit: cover;
}
.gallery-success-title {
font-weight: 500;
color: #333;
margin-bottom: 24px !important;
}
.gallery-success-redirect {
font-size: 14px !important;
color: #999 !important;
animation: pulse 1.5s ease infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
.dots::after {
content: '';
animation: dots 1.5s steps(4, end) infinite;
}
@keyframes dots {
0% { content: ''; }
25% { content: '.'; }
50% { content: '..'; }
75% { content: '...'; }
100% { content: ''; }
}
</style>
<script>
setTimeout(function() {
window.location.href = '<?php echo Config::get('URL'); ?>gallery/view/<?php echo $this->image->id; ?>';
}, 2500);
</script>

View File

@@ -0,0 +1,437 @@
<div class="gallery-container">
<div class="gallery-header">
<a href="<?php echo Config::get('URL'); ?>gallery/my" class="gallery-back">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
Back to My Images
</a>
</div>
<div class="gallery-form-container">
<h1>Upload Image</h1>
<?php $this->renderFeedbackMessages(); ?>
<form id="upload-form" method="post" action="<?php echo Config::get('URL'); ?>gallery/upload" enctype="multipart/form-data" class="gallery-form">
<div class="gallery-form-group">
<label class="gallery-form-label">Image File</label>
<div class="gallery-upload-zone" id="upload-zone">
<input type="file" name="image" id="image-input" accept="image/jpeg,image/png,image/gif,image/webp" required>
<div class="gallery-upload-placeholder">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
<span>Click or drag image here</span>
<small>JPG, PNG, GIF, WebP - Max 10MB</small>
</div>
<div class="gallery-upload-preview" id="upload-preview" style="display: none;">
<img id="preview-image" src="" alt="Preview">
<button type="button" class="gallery-upload-remove" id="remove-preview">&times;</button>
</div>
</div>
</div>
<div class="gallery-form-group">
<label class="gallery-form-label" for="title">Title</label>
<input type="text" name="title" id="title" class="gallery-form-input" placeholder="Give your image a title">
</div>
<div class="gallery-form-group">
<label class="gallery-form-label" for="description">Description</label>
<textarea name="description" id="description" class="gallery-form-textarea" rows="4" placeholder="Add a description (optional)"></textarea>
</div>
<div class="gallery-form-group">
<label class="gallery-form-checkbox">
<input type="checkbox" name="is_public" value="1" checked>
<span>Make this image public</span>
</label>
<small class="gallery-form-hint">Public images are visible to everyone in the gallery</small>
</div>
<div class="gallery-form-actions">
<button type="submit" name="submit_upload" class="gallery-btn gallery-btn-primary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
Upload Image
</button>
<a href="<?php echo Config::get('URL'); ?>gallery/my" class="gallery-btn gallery-btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>
<!-- Upload Progress Overlay -->
<div id="upload-overlay" class="upload-overlay" style="display: none;">
<div class="upload-modal">
<!-- Progress State -->
<div id="upload-progress-state" class="upload-state">
<div class="upload-icon uploading">
<svg class="upload-spinner" viewBox="0 0 50 50">
<circle cx="25" cy="25" r="20" fill="none" stroke-width="4"></circle>
</svg>
</div>
<h2>Uploading...</h2>
<div class="upload-progress-bar">
<div class="upload-progress-fill" id="progress-fill"></div>
</div>
<p class="upload-progress-text"><span id="progress-percent">0</span>%</p>
</div>
<!-- Success State -->
<div id="upload-success-state" class="upload-state" style="display: none;">
<div class="upload-icon success">
<svg class="checkmark" viewBox="0 0 52 52">
<circle class="checkmark-circle" cx="26" cy="26" r="25" fill="none"/>
<path class="checkmark-check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/>
</svg>
</div>
<h2>Upload Complete!</h2>
<p>Your image has been uploaded successfully.</p>
<p class="upload-redirect">Redirecting<span class="dots"></span></p>
</div>
<!-- Error State -->
<div id="upload-error-state" class="upload-state" style="display: none;">
<div class="upload-icon error">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="15" y1="9" x2="9" y2="15"></line>
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>
</div>
<h2>Upload Failed</h2>
<p id="error-message">Something went wrong. Please try again.</p>
<button type="button" class="gallery-btn gallery-btn-primary" onclick="hideOverlay()">Try Again</button>
</div>
</div>
</div>
<style>
.upload-overlay {
position: fixed;
inset: 0;
background: rgba(255, 255, 255, 0.95);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.upload-modal {
text-align: center;
padding: 40px;
max-width: 400px;
}
.upload-state {
animation: slideUp 0.4s ease;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.upload-icon {
width: 80px;
height: 80px;
margin: 0 auto 24px;
}
.upload-icon svg {
width: 100%;
height: 100%;
}
.upload-icon.uploading svg {
stroke: #333;
}
.upload-icon.success svg {
stroke: #4CAF50;
}
.upload-icon.error svg {
stroke: #dc3545;
}
.upload-spinner {
animation: rotate 1.5s linear infinite;
}
.upload-spinner circle {
stroke: #333;
stroke-linecap: round;
stroke-dasharray: 90, 150;
stroke-dashoffset: 0;
animation: dash 1.5s ease-in-out infinite;
}
@keyframes rotate {
100% { transform: rotate(360deg); }
}
@keyframes dash {
0% { stroke-dasharray: 1, 150; stroke-dashoffset: 0; }
50% { stroke-dasharray: 90, 150; stroke-dashoffset: -35; }
100% { stroke-dasharray: 90, 150; stroke-dashoffset: -124; }
}
.upload-modal h2 {
margin: 0 0 16px;
font-size: 24px;
font-weight: 600;
color: #333;
}
.upload-modal p {
margin: 0;
color: #666;
font-size: 14px;
}
.upload-progress-bar {
width: 100%;
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
margin: 20px 0 12px;
}
.upload-progress-fill {
height: 100%;
background: linear-gradient(90deg, #333, #555);
border-radius: 4px;
width: 0%;
transition: width 0.3s ease;
}
.upload-progress-text {
font-family: monospace;
font-size: 16px !important;
font-weight: 600;
color: #333 !important;
}
/* Checkmark Animation */
.checkmark {
width: 80px;
height: 80px;
border-radius: 50%;
display: block;
stroke-width: 2;
stroke: #4CAF50;
stroke-miterlimit: 10;
box-shadow: inset 0px 0px 0px #4CAF50;
animation: fill 0.4s ease-in-out 0.4s forwards, scale 0.3s ease-in-out 0.9s both;
}
.checkmark-circle {
stroke-dasharray: 166;
stroke-dashoffset: 166;
stroke-width: 2;
stroke-miterlimit: 10;
stroke: #4CAF50;
fill: none;
animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
}
.checkmark-check {
transform-origin: 50% 50%;
stroke-dasharray: 48;
stroke-dashoffset: 48;
animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards;
}
@keyframes stroke {
100% { stroke-dashoffset: 0; }
}
@keyframes scale {
0%, 100% { transform: none; }
50% { transform: scale3d(1.1, 1.1, 1); }
}
@keyframes fill {
100% { box-shadow: inset 0px 0px 0px 40px rgba(76, 175, 80, 0.1); }
}
.upload-redirect {
margin-top: 20px !important;
font-size: 13px !important;
color: #999 !important;
animation: pulse 1.5s ease infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
.dots::after {
content: '';
animation: dots 1.5s steps(4, end) infinite;
}
@keyframes dots {
0% { content: ''; }
25% { content: '.'; }
50% { content: '..'; }
75% { content: '...'; }
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('upload-form');
const uploadZone = document.getElementById('upload-zone');
const imageInput = document.getElementById('image-input');
const uploadPreview = document.getElementById('upload-preview');
const previewImage = document.getElementById('preview-image');
const removePreview = document.getElementById('remove-preview');
const placeholder = uploadZone.querySelector('.gallery-upload-placeholder');
const overlay = document.getElementById('upload-overlay');
const progressState = document.getElementById('upload-progress-state');
const successState = document.getElementById('upload-success-state');
const errorState = document.getElementById('upload-error-state');
const progressFill = document.getElementById('progress-fill');
const progressPercent = document.getElementById('progress-percent');
const errorMessage = document.getElementById('error-message');
// Image preview
imageInput.addEventListener('change', function(e) {
if (this.files && this.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
previewImage.src = e.target.result;
placeholder.style.display = 'none';
uploadPreview.style.display = 'block';
};
reader.readAsDataURL(this.files[0]);
}
});
removePreview.addEventListener('click', function() {
imageInput.value = '';
previewImage.src = '';
placeholder.style.display = 'flex';
uploadPreview.style.display = 'none';
});
// Drag and drop
uploadZone.addEventListener('dragover', function(e) {
e.preventDefault();
this.classList.add('dragover');
});
uploadZone.addEventListener('dragleave', function(e) {
e.preventDefault();
this.classList.remove('dragover');
});
uploadZone.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('dragover');
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
imageInput.files = e.dataTransfer.files;
imageInput.dispatchEvent(new Event('change'));
}
});
// Form submission with AJAX
form.addEventListener('submit', function(e) {
e.preventDefault();
if (!imageInput.files || !imageInput.files[0]) {
alert('Please select an image to upload');
return;
}
const formData = new FormData(form);
formData.append('submit_upload', '1');
// Show overlay with progress state
showOverlay('progress');
const xhr = new XMLHttpRequest();
// Progress tracking
xhr.upload.addEventListener('progress', function(e) {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
progressFill.style.width = percent + '%';
progressPercent.textContent = percent;
}
});
xhr.addEventListener('load', function() {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.success) {
showOverlay('success');
// Redirect after animation
setTimeout(function() {
window.location.href = '<?php echo Config::get('URL'); ?>gallery/view/' + response.image_id;
}, 2000);
} else {
showOverlay('error', response.message || 'Upload failed');
}
} catch (e) {
showOverlay('error', 'Invalid server response');
}
} else {
showOverlay('error', 'Server error: ' + xhr.status);
}
});
xhr.addEventListener('error', function() {
showOverlay('error', 'Network error. Please check your connection.');
});
xhr.open('POST', form.action);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.send(formData);
});
function showOverlay(state, message) {
overlay.style.display = 'flex';
progressState.style.display = 'none';
successState.style.display = 'none';
errorState.style.display = 'none';
if (state === 'progress') {
progressState.style.display = 'block';
progressFill.style.width = '0%';
progressPercent.textContent = '0';
} else if (state === 'success') {
successState.style.display = 'block';
} else if (state === 'error') {
errorState.style.display = 'block';
if (message) {
errorMessage.textContent = message;
}
}
}
window.hideOverlay = function() {
overlay.style.display = 'none';
};
});
</script>

View File

@@ -0,0 +1,80 @@
<div class="gallery-container">
<div class="gallery-header">
<a href="<?php echo Config::get('URL'); ?>gallery/index" class="gallery-back">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
Back to Gallery
</a>
</div>
<div class="gallery-view">
<div class="gallery-view-image">
<img src="<?php echo Config::get('URL'); ?>gallery/image/<?php echo $this->image->id; ?>/full"
alt="<?php echo htmlspecialchars($this->image->title); ?>">
</div>
<div class="gallery-view-info">
<h1><?php echo htmlspecialchars($this->image->title); ?></h1>
<div class="gallery-view-meta">
<div class="gallery-view-author">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
<a href="<?php echo Config::get('URL'); ?>profile/showProfile/<?php echo $this->image->user_name; ?>">
<?php echo htmlspecialchars($this->image->user_name); ?>
</a>
</div>
<div class="gallery-view-date">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
<line x1="16" y1="2" x2="16" y2="6"></line>
<line x1="8" y1="2" x2="8" y2="6"></line>
<line x1="3" y1="10" x2="21" y2="10"></line>
</svg>
<?php echo date('M j, Y', strtotime($this->image->created_at)); ?>
</div>
<div class="gallery-view-size">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
<?php echo GalleryModel::formatFileSize($this->image->file_size); ?>
</div>
</div>
<?php if ($this->image->description): ?>
<div class="gallery-view-description">
<?php echo nl2br(htmlspecialchars($this->image->description)); ?>
</div>
<?php endif; ?>
<?php if ($this->image->user_id == Session::get('user_id')): ?>
<div class="gallery-view-actions">
<a href="<?php echo Config::get('URL'); ?>gallery/edit/<?php echo $this->image->id; ?>" class="gallery-btn gallery-btn-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
Edit
</a>
<a href="<?php echo Config::get('URL'); ?>gallery/delete/<?php echo $this->image->id; ?>"
class="gallery-btn gallery-btn-danger"
onclick="return confirm('Delete this image permanently?');">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
Delete
</a>
</div>
<?php endif; ?>
</div>
</div>
</div>

View File

@@ -0,0 +1,473 @@
<div class="dbm-content-header">
<div class="dbm-breadcrumb">
<a href="<?php echo Config::get('URL'); ?>database/index">Databases</a>
<span class="separator">/</span>
<a href="<?php echo Config::get('URL'); ?>database/show/<?php echo urlencode($this->database_name); ?>"><?php echo htmlspecialchars($this->database_name); ?></a>
<span class="separator">/</span>
<span>SQL Console</span>
</div>
<div class="dbm-title">
<h1>SQL Console</h1>
<span class="badge"><?php echo htmlspecialchars($this->database_name); ?></span>
</div>
</div>
<div class="dbm-content-body">
<div class="sql-console">
<form method="post" action="<?php echo Config::get('URL'); ?>sql/execute" id="sql-form">
<input type="hidden" name="database_name" value="<?php echo htmlspecialchars($this->database_name); ?>">
<div class="sql-editor-container">
<div class="sql-editor-wrapper">
<pre class="sql-highlight" id="sql-highlight" aria-hidden="true"></pre>
<textarea name="sql_query" id="sql_query" class="sql-textarea" spellcheck="false" placeholder="SELECT * FROM users LIMIT 10;"><?php echo isset($_POST['sql_query']) ? htmlspecialchars($_POST['sql_query']) : ''; ?></textarea>
</div>
<div class="sql-line-numbers" id="line-numbers">1</div>
</div>
<div class="sql-toolbar">
<div class="sql-toolbar-left">
<button type="submit" class="dbm-btn dbm-btn-success">
<i data-lucide="play"></i>
Execute
</button>
<button type="button" class="dbm-btn dbm-btn-secondary" onclick="formatSQL()">
<i data-lucide="align-left"></i>
Format
</button>
<button type="button" class="dbm-btn dbm-btn-secondary" onclick="clearSQL()">
<i data-lucide="trash-2"></i>
Clear
</button>
</div>
<div class="sql-toolbar-right">
<span class="sql-hint">Ctrl+Enter to execute</span>
</div>
</div>
</form>
<div id="sql-result" class="sql-result">
<?php
$result = Session::get('sql_result');
if ($result) {
Session::set('sql_result', null);
if ($result['success']) {
echo '<div class="sql-result-success">';
echo '<div class="sql-result-header">';
echo '<span class="sql-result-status"><i data-lucide="check-circle"></i> ' . htmlspecialchars($result['message']) . '</span>';
echo '<span class="sql-result-time">' . $result['execution_time'] . ' ms</span>';
echo '</div>';
if (!empty($result['result'])) {
echo '<div class="sql-result-table-wrapper"><table class="dbm-table"><thead><tr>';
foreach (array_keys($result['result'][0]) as $col) {
echo '<th>' . htmlspecialchars($col) . '</th>';
}
echo '</tr></thead><tbody>';
foreach ($result['result'] as $row) {
echo '<tr>';
foreach ($row as $value) {
echo '<td>' . ($value === null ? '<span class="null-value">NULL</span>' : htmlspecialchars(substr($value, 0, 200))) . '</td>';
}
echo '</tr>';
}
echo '</tbody></table></div>';
}
echo '</div>';
} else {
echo '<div class="sql-result-error">';
echo '<div class="sql-result-header">';
echo '<span class="sql-result-status"><i data-lucide="x-circle"></i> ' . htmlspecialchars($result['message']) . '</span>';
echo '</div>';
if (!empty($result['error'])) {
echo '<pre class="sql-error-details">' . htmlspecialchars($result['error']) . '</pre>';
}
echo '</div>';
}
}
?>
</div>
<?php if (!empty($this->history)): ?>
<div class="sql-history">
<h3>Recent Queries</h3>
<div class="sql-history-list">
<?php foreach (array_slice($this->history, 0, 10) as $item): ?>
<div class="sql-history-item" onclick="loadQuery(this)" data-query="<?php echo htmlspecialchars($item['query_text']); ?>">
<code><?php echo htmlspecialchars(substr($item['query_text'], 0, 80)); ?><?php echo strlen($item['query_text']) > 80 ? '...' : ''; ?></code>
<span class="sql-history-time"><?php echo date('M j, H:i', strtotime($item['query_timestamp'])); ?></span>
</div>
<?php endforeach; ?>
</div>
<a href="<?php echo Config::get('URL'); ?>sql/clearHistory" class="dbm-btn dbm-btn-sm dbm-btn-secondary" style="margin-top: 12px;">Clear History</a>
</div>
<?php endif; ?>
</div>
</div>
<style>
.sql-console {
display: flex;
flex-direction: column;
gap: 20px;
}
.sql-editor-container {
display: flex;
border: 1px solid var(--dbm-border);
border-radius: var(--dbm-radius);
overflow: hidden;
background: #1e1e1e;
}
.sql-line-numbers {
padding: 12px 8px;
background: #252526;
color: #858585;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
text-align: right;
user-select: none;
min-width: 40px;
border-right: 1px solid #333;
}
.sql-editor-wrapper {
flex: 1;
position: relative;
min-height: 180px;
}
.sql-highlight, .sql-textarea {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
padding: 12px;
margin: 0;
border: none;
width: 100%;
min-height: 180px;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
}
.sql-highlight {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
background: transparent;
color: #d4d4d4;
overflow: hidden;
}
.sql-textarea {
position: relative;
background: transparent;
color: transparent;
caret-color: #fff;
resize: none;
outline: none;
z-index: 1;
box-sizing: border-box;
}
.sql-textarea::placeholder {
color: #666;
}
/* Syntax highlighting colors - VS Code dark theme */
.sql-highlight .sql-keyword { color: #569cd6 !important; font-weight: bold !important; }
.sql-highlight .sql-function { color: #dcdcaa !important; }
.sql-highlight .sql-string { color: #ce9178 !important; }
.sql-highlight .sql-number { color: #b5cea8 !important; }
.sql-highlight .sql-operator { color: #d4d4d4 !important; }
.sql-highlight .sql-comment { color: #6a9955 !important; font-style: italic !important; }
.sql-highlight .sql-table { color: #4ec9b0 !important; }
.sql-highlight .sql-column { color: #9cdcfe !important; }
.sql-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.sql-toolbar-left {
display: flex;
gap: 8px;
}
.sql-hint {
font-size: 12px;
color: var(--dbm-text-muted);
}
.sql-result {
margin-top: 8px;
}
.sql-result-success {
background: #f0f9f0;
border: 1px solid #c3e6c3;
border-radius: var(--dbm-radius);
overflow: hidden;
}
.sql-result-error {
background: #fdf0f0;
border: 1px solid #f5c6c6;
border-radius: var(--dbm-radius);
overflow: hidden;
}
.sql-result-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: rgba(0,0,0,0.03);
border-bottom: 1px solid rgba(0,0,0,0.05);
}
.sql-result-status {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 500;
}
.sql-result-success .sql-result-status { color: #2e7d32; }
.sql-result-error .sql-result-status { color: #c62828; }
.sql-result-time {
font-size: 12px;
color: #666;
font-family: monospace;
}
.sql-result-table-wrapper {
overflow-x: auto;
max-height: 400px;
overflow-y: auto;
}
.sql-result-table-wrapper .dbm-table {
margin: 0;
font-size: 12px;
}
.sql-result-table-wrapper .dbm-table th {
position: sticky;
top: 0;
background: #f8f9fa;
z-index: 1;
}
.null-value {
color: #999;
font-style: italic;
}
.sql-error-details {
margin: 0;
padding: 12px 16px;
font-family: monospace;
font-size: 12px;
color: #c62828;
white-space: pre-wrap;
word-break: break-all;
}
.sql-history {
background: var(--dbm-bg-secondary);
border-radius: var(--dbm-radius);
padding: 16px;
}
.sql-history h3 {
margin: 0 0 12px;
font-size: 14px;
font-weight: 600;
color: var(--dbm-text);
}
.sql-history-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.sql-history-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: var(--dbm-bg);
border-radius: var(--dbm-radius);
cursor: pointer;
transition: background 0.15s;
}
.sql-history-item:hover {
background: var(--dbm-bg-tertiary);
}
.sql-history-item code {
font-size: 12px;
color: var(--dbm-text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
margin-right: 12px;
}
.sql-history-time {
font-size: 11px;
color: var(--dbm-text-muted);
white-space: nowrap;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const textarea = document.getElementById('sql_query');
const highlight = document.getElementById('sql-highlight');
const lineNumbers = document.getElementById('line-numbers');
function updateHighlight() {
const code = textarea.value;
highlight.innerHTML = highlightSQL(code) + '\n';
updateLineNumbers();
}
function updateLineNumbers() {
const lines = textarea.value.split('\n').length;
let nums = '';
for (let i = 1; i <= lines; i++) {
nums += i + '\n';
}
lineNumbers.textContent = nums;
}
function highlightSQL(code) {
if (!code) return '';
// Escape HTML
code = code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
// Use inline styles for guaranteed coloring
const styles = {
keyword: 'color:#569cd6;font-weight:bold',
function: 'color:#dcdcaa',
string: 'color:#ce9178',
number: 'color:#b5cea8',
comment: 'color:#6a9955;font-style:italic'
};
// Comments (must be first to avoid highlighting keywords inside comments)
code = code.replace(/(--[^\n]*)/g, '<span style="' + styles.comment + '">$1</span>');
code = code.replace(/(\/\*[\s\S]*?\*\/)/g, '<span style="' + styles.comment + '">$1</span>');
// Strings
code = code.replace(/('[^']*')/g, '<span style="' + styles.string + '">$1</span>');
code = code.replace(/("[^"]*")/g, '<span style="' + styles.string + '">$1</span>');
// Numbers (but not inside already-styled spans)
code = code.replace(/\b(\d+\.?\d*)\b(?![^<]*>)/g, '<span style="' + styles.number + '">$1</span>');
// Keywords
const keywords = [
'SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'NOT', 'IN', 'BETWEEN', 'LIKE', 'IS', 'NULL',
'ORDER', 'BY', 'ASC', 'DESC', 'GROUP', 'HAVING', 'LIMIT', 'OFFSET',
'INSERT', 'INTO', 'VALUES', 'UPDATE', 'SET', 'DELETE',
'CREATE', 'TABLE', 'DATABASE', 'INDEX', 'VIEW', 'TRIGGER', 'PROCEDURE', 'FUNCTION',
'ALTER', 'DROP', 'TRUNCATE', 'ADD', 'MODIFY', 'CHANGE', 'RENAME',
'JOIN', 'INNER', 'LEFT', 'RIGHT', 'OUTER', 'CROSS', 'ON', 'USING',
'UNION', 'ALL', 'DISTINCT', 'AS', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END',
'PRIMARY', 'KEY', 'FOREIGN', 'REFERENCES', 'CONSTRAINT', 'UNIQUE', 'DEFAULT',
'AUTO_INCREMENT', 'ENGINE', 'CHARSET', 'COLLATE',
'IF', 'EXISTS', 'SHOW', 'DESCRIBE', 'EXPLAIN', 'USE', 'GRANT', 'REVOKE',
'BEGIN', 'COMMIT', 'ROLLBACK', 'TRANSACTION'
];
const keywordRegex = new RegExp('\\b(' + keywords.join('|') + ')\\b(?![^<]*>)', 'gi');
code = code.replace(keywordRegex, '<span style="' + styles.keyword + '">$1</span>');
// Functions
const functions = [
'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'CONCAT', 'SUBSTRING', 'LENGTH', 'UPPER', 'LOWER',
'TRIM', 'LTRIM', 'RTRIM', 'REPLACE', 'COALESCE', 'IFNULL', 'NULLIF', 'CAST', 'CONVERT',
'DATE', 'NOW', 'CURDATE', 'CURTIME', 'YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE', 'SECOND',
'DATE_FORMAT', 'DATEDIFF', 'DATE_ADD', 'DATE_SUB', 'ROUND', 'FLOOR', 'CEIL', 'ABS', 'MOD',
'RAND', 'UUID', 'MD5', 'SHA1', 'SHA2', 'GROUP_CONCAT', 'JSON_OBJECT', 'JSON_ARRAY'
];
const funcRegex = new RegExp('\\b(' + functions.join('|') + ')\\s*\\(', 'gi');
code = code.replace(funcRegex, '<span style="' + styles.function + '">$1</span>(');
return code;
}
textarea.addEventListener('input', updateHighlight);
textarea.addEventListener('scroll', function() {
highlight.scrollTop = textarea.scrollTop;
highlight.scrollLeft = textarea.scrollLeft;
});
// Ctrl+Enter to execute
textarea.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.key === 'Enter') {
e.preventDefault();
document.getElementById('sql-form').submit();
}
// Tab to insert spaces
if (e.key === 'Tab') {
e.preventDefault();
const start = this.selectionStart;
const end = this.selectionEnd;
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
this.selectionStart = this.selectionEnd = start + 4;
updateHighlight();
}
});
updateHighlight();
lucide.createIcons();
});
function loadQuery(element) {
document.getElementById('sql_query').value = element.dataset.query;
document.getElementById('sql_query').dispatchEvent(new Event('input'));
document.getElementById('sql_query').focus();
}
function clearSQL() {
document.getElementById('sql_query').value = '';
document.getElementById('sql_query').dispatchEvent(new Event('input'));
}
function formatSQL() {
const textarea = document.getElementById('sql_query');
let sql = textarea.value;
// Basic formatting
const keywords = ['SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'ORDER BY', 'GROUP BY', 'HAVING', 'LIMIT', 'JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'INNER JOIN', 'ON', 'SET', 'VALUES', 'INSERT INTO', 'UPDATE', 'DELETE FROM'];
keywords.forEach(kw => {
const regex = new RegExp('\\b' + kw.replace(' ', '\\s+') + '\\b', 'gi');
sql = sql.replace(regex, '\n' + kw);
});
sql = sql.trim();
textarea.value = sql;
textarea.dispatchEvent(new Event('input'));
}
</script>

View File

@@ -0,0 +1,68 @@
<div class="dbm-content-header">
<div class="dbm-breadcrumb">
<a href="<?php echo Config::get('URL'); ?>database/index">Databases</a>
<span class="separator">/</span>
<a href="<?php echo Config::get('URL'); ?>database/show/<?php echo urlencode($this->database_name); ?>"><?php echo htmlspecialchars($this->database_name); ?></a>
<span class="separator">/</span>
<a href="<?php echo Config::get('URL'); ?>table/structure/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>"><?php echo htmlspecialchars($this->table_name); ?></a>
<span class="separator">/</span>
<span>Add Column</span>
</div>
<div class="dbm-title">
<h1>Add Column</h1>
</div>
</div>
<div class="dbm-content-body">
<div class="dbm-card" style="max-width: 500px;">
<div class="dbm-card-body">
<form method="post" action="<?php echo Config::get('URL'); ?>table/addColumn/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>">
<div class="dbm-form-group">
<label class="dbm-form-label">Column Name</label>
<input type="text" name="column_name" class="dbm-form-input" required pattern="[a-zA-Z0-9_]+" placeholder="column_name" style="width: 100%; max-width: 100%;">
</div>
<div class="dbm-form-group">
<label class="dbm-form-label">Type</label>
<select name="column_type" class="dbm-form-select" style="width: 100%; max-width: 100%;">
<option value="INT">INT</option>
<option value="VARCHAR(255)">VARCHAR(255)</option>
<option value="TEXT">TEXT</option>
<option value="DATETIME">DATETIME</option>
<option value="TIMESTAMP">TIMESTAMP</option>
<option value="DECIMAL(10,2)">DECIMAL(10,2)</option>
<option value="BOOLEAN">BOOLEAN</option>
</select>
</div>
<div class="dbm-form-group">
<label class="dbm-form-label">Null</label>
<select name="column_null" class="dbm-form-select" style="width: 100%; max-width: 100%;">
<option value="YES">NULL</option>
<option value="NO">NOT NULL</option>
</select>
</div>
<div class="dbm-form-group">
<label class="dbm-form-label">Default Value</label>
<input type="text" name="column_default" class="dbm-form-input" placeholder="Leave empty for none" style="width: 100%; max-width: 100%;">
</div>
<div class="dbm-form-group">
<label class="dbm-form-label">Extra</label>
<select name="column_extra" class="dbm-form-select" style="width: 100%; max-width: 100%;">
<option value="">None</option>
<option value="auto_increment">AUTO_INCREMENT</option>
</select>
</div>
<input type="hidden" name="column_key" value="">
<div style="margin-top: 24px; display: flex; gap: 10px;">
<button type="submit" name="submit_add_column" class="dbm-btn dbm-btn-success">Add Column</button>
<a href="<?php echo Config::get('URL'); ?>table/structure/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>" class="dbm-btn dbm-btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>

View File

@@ -0,0 +1,122 @@
<div class="dbm-content-header">
<div class="dbm-breadcrumb">
<a href="<?php echo Config::get('URL'); ?>database/index">Databases</a>
<span class="separator">/</span>
<a href="<?php echo Config::get('URL'); ?>database/show/<?php echo urlencode($this->database_name); ?>"><?php echo htmlspecialchars($this->database_name); ?></a>
<span class="separator">/</span>
<span>New Table</span>
</div>
<div class="dbm-title">
<h1>Create New Table</h1>
</div>
</div>
<div class="dbm-content-body">
<div class="dbm-card">
<div class="dbm-card-body">
<form method="post" action="<?php echo Config::get('URL'); ?>table/create/<?php echo urlencode($this->database_name); ?>" id="create-table-form">
<div class="dbm-form-group">
<label class="dbm-form-label">Table Name</label>
<input type="text" name="table_name" class="dbm-form-input" required pattern="[a-zA-Z0-9_]+" placeholder="my_table">
</div>
<h3 style="color: var(--text-primary); margin: 24px 0 16px;">Columns</h3>
<div id="columns-container">
<div class="column-row" style="display: flex; gap: 10px; margin-bottom: 12px; padding: 16px; background: var(--bg-input); border-radius: var(--radius); flex-wrap: wrap; align-items: center;">
<input type="text" name="columns[0][name]" class="dbm-form-input" placeholder="Column name" required style="width: 150px; max-width: 150px;">
<select name="columns[0][type]" class="dbm-form-select" style="width: 140px; max-width: 140px;">
<option value="INT">INT</option>
<option value="VARCHAR(255)">VARCHAR(255)</option>
<option value="TEXT">TEXT</option>
<option value="DATETIME">DATETIME</option>
<option value="TIMESTAMP">TIMESTAMP</option>
<option value="DECIMAL(10,2)">DECIMAL(10,2)</option>
<option value="BOOLEAN">BOOLEAN</option>
</select>
<select name="columns[0][null]" class="dbm-form-select" style="width: 100px; max-width: 100px;">
<option value="YES">NULL</option>
<option value="NO">NOT NULL</option>
</select>
<select name="columns[0][key]" class="dbm-form-select" style="width: 130px; max-width: 130px;">
<option value="">No Key</option>
<option value="PRI">PRIMARY KEY</option>
</select>
<input type="text" name="columns[0][default]" class="dbm-form-input" placeholder="Default" style="width: 100px; max-width: 100px;">
<select name="columns[0][extra]" class="dbm-form-select" style="width: 140px; max-width: 140px;">
<option value="">None</option>
<option value="auto_increment">AUTO_INCREMENT</option>
</select>
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-danger remove-column" style="display: none;">Remove</button>
</div>
</div>
<button type="button" id="add-column" class="dbm-btn dbm-btn-secondary" style="margin-top: 8px;">
<i data-lucide="plus"></i>
Add Column
</button>
<div style="margin-top: 32px; padding-top: 24px; border-top: 1px solid var(--border-color);">
<button type="submit" name="submit_create_table" class="dbm-btn dbm-btn-success">
<i data-lucide="check-circle"></i>
Create Table
</button>
<a href="<?php echo Config::get('URL'); ?>database/show/<?php echo urlencode($this->database_name); ?>" class="dbm-btn dbm-btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
let columnIndex = 1;
document.getElementById('add-column').addEventListener('click', function() {
const container = document.getElementById('columns-container');
const newRow = document.createElement('div');
newRow.className = 'column-row';
newRow.style.cssText = 'display: flex; gap: 10px; margin-bottom: 12px; padding: 16px; background: var(--bg-input); border-radius: var(--radius); flex-wrap: wrap; align-items: center;';
newRow.innerHTML = `
<input type="text" name="columns[${columnIndex}][name]" class="dbm-form-input" placeholder="Column name" required style="width: 150px; max-width: 150px;">
<select name="columns[${columnIndex}][type]" class="dbm-form-select" style="width: 140px; max-width: 140px;">
<option value="INT">INT</option>
<option value="VARCHAR(255)">VARCHAR(255)</option>
<option value="TEXT">TEXT</option>
<option value="DATETIME">DATETIME</option>
<option value="TIMESTAMP">TIMESTAMP</option>
<option value="DECIMAL(10,2)">DECIMAL(10,2)</option>
<option value="BOOLEAN">BOOLEAN</option>
</select>
<select name="columns[${columnIndex}][null]" class="dbm-form-select" style="width: 100px; max-width: 100px;">
<option value="YES">NULL</option>
<option value="NO">NOT NULL</option>
</select>
<select name="columns[${columnIndex}][key]" class="dbm-form-select" style="width: 130px; max-width: 130px;">
<option value="">No Key</option>
<option value="PRI">PRIMARY KEY</option>
</select>
<input type="text" name="columns[${columnIndex}][default]" class="dbm-form-input" placeholder="Default" style="width: 100px; max-width: 100px;">
<select name="columns[${columnIndex}][extra]" class="dbm-form-select" style="width: 140px; max-width: 140px;">
<option value="">None</option>
<option value="auto_increment">AUTO_INCREMENT</option>
</select>
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-danger remove-column">Remove</button>
`;
container.appendChild(newRow);
columnIndex++;
document.querySelectorAll('.remove-column').forEach(btn => btn.style.display = 'inline-flex');
});
document.getElementById('columns-container').addEventListener('click', function(e) {
if (e.target.classList.contains('remove-column')) {
e.target.closest('.column-row').remove();
const rows = document.querySelectorAll('.column-row');
if (rows.length === 1) {
rows[0].querySelector('.remove-column').style.display = 'none';
}
}
});
});
</script>

View File

@@ -0,0 +1,435 @@
<?php
$total_pages = ceil($this->total_rows / $this->per_page);
$pk_column = null;
foreach ($this->columns as $col) {
if ($col['Key'] === 'PRI') {
$pk_column = $col['Field'];
break;
}
}
?>
<div class="dbm-content-header">
<div class="dbm-breadcrumb">
<a href="<?php echo Config::get('URL'); ?>database/index">Databases</a>
<span class="separator">/</span>
<a href="<?php echo Config::get('URL'); ?>database/show/<?php echo urlencode($this->database_name); ?>"><?php echo htmlspecialchars($this->database_name); ?></a>
<span class="separator">/</span>
<span><?php echo htmlspecialchars($this->table_name); ?></span>
</div>
<div class="dbm-title">
<h1><?php echo htmlspecialchars($this->table_name); ?></h1>
<span class="badge"><?php echo number_format($this->total_rows); ?> rows</span>
</div>
<div class="dbm-actions">
<button type="button" id="btn-add-row" class="dbm-btn dbm-btn-success">
<i data-lucide="plus"></i>
Add Row
</button>
<a href="<?php echo Config::get('URL'); ?>table/structure/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>" class="dbm-btn dbm-btn-secondary">
<i data-lucide="settings"></i>
Structure
</a>
<a href="<?php echo Config::get('URL'); ?>table/export/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>" class="dbm-btn dbm-btn-secondary" target="_blank">
<i data-lucide="download"></i>
Export
</a>
<a href="<?php echo Config::get('URL'); ?>table/delete/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>"
class="dbm-btn dbm-btn-danger"
data-confirm="Drop table '<?php echo htmlspecialchars($this->table_name); ?>'? This cannot be undone!">
<i data-lucide="trash-2"></i>
Drop Table
</a>
</div>
</div>
<div class="dbm-content-body">
<?php if (!empty($this->table_info)): ?>
<div class="dbm-stats">
<div class="dbm-stat">
<div class="dbm-stat-value"><?php echo number_format($this->total_rows); ?></div>
<div class="dbm-stat-label">Total Rows</div>
</div>
<div class="dbm-stat">
<div class="dbm-stat-value"><?php echo count($this->columns); ?></div>
<div class="dbm-stat-label">Columns</div>
</div>
<div class="dbm-stat">
<div class="dbm-stat-value"><?php echo $this->table_info['total_size'] ?? '-'; ?></div>
<div class="dbm-stat-label">Size</div>
</div>
<div class="dbm-stat">
<div class="dbm-stat-value"><?php echo $this->table_info['engine'] ?? '-'; ?></div>
<div class="dbm-stat-label">Engine</div>
</div>
</div>
<?php endif; ?>
<div class="dbm-table-wrapper" id="data-table-wrapper"
data-database="<?php echo htmlspecialchars($this->database_name); ?>"
data-table="<?php echo htmlspecialchars($this->table_name); ?>"
data-pk-column="<?php echo htmlspecialchars($pk_column ?? ''); ?>"
style="max-height: 500px; overflow: auto;">
<table class="dbm-table" id="data-table">
<thead>
<tr>
<?php foreach ($this->columns as $col): ?>
<th data-column="<?php echo htmlspecialchars($col['Field']); ?>" data-type="<?php echo htmlspecialchars($col['Type']); ?>">
<?php if ($col['Key'] === 'PRI'): ?>
<span style="color: var(--accent-green);" title="Primary Key">
<i data-lucide="key-round" style="width: 12px; height: 12px; vertical-align: middle;"></i>
</span>
<?php endif; ?>
<?php echo htmlspecialchars($col['Field']); ?>
</th>
<?php endforeach; ?>
<th style="width: 120px; text-align: center;">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($this->rows)): ?>
<tr class="empty-row">
<td colspan="<?php echo count($this->columns) + 1; ?>" style="text-align: center; padding: 40px; color: var(--text-muted);">
No data in this table. Click "Add Row" to insert data.
</td>
</tr>
<?php else: ?>
<?php foreach ($this->rows as $row): ?>
<tr class="data-row" data-pk="<?php echo htmlspecialchars($pk_column ? ($row[$pk_column] ?? '') : ''); ?>">
<?php foreach ($this->columns as $col): ?>
<td class="editable-cell" data-column="<?php echo htmlspecialchars($col['Field']); ?>" data-original="<?php echo htmlspecialchars($row[$col['Field']] ?? ''); ?>">
<span class="cell-display"><?php
$value = $row[$col['Field']] ?? null;
if ($value === null) {
echo '<span class="null-value">NULL</span>';
} else {
$display = htmlspecialchars(substr($value, 0, 100));
if (strlen($value) > 100) $display .= '...';
echo $display;
}
?></span>
<input type="text" class="cell-input dbm-form-input" style="display: none; width: 100%; padding: 4px 8px; font-size: 13px;" value="<?php echo htmlspecialchars($row[$col['Field']] ?? ''); ?>">
</td>
<?php endforeach; ?>
<td class="actions-cell" style="text-align: center; white-space: nowrap;">
<div class="view-actions">
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-secondary btn-edit-row" title="Edit">
<i data-lucide="pencil" style="width: 12px; height: 12px;"></i>
</button>
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-danger btn-delete-row" title="Delete">
<i data-lucide="trash-2" style="width: 12px; height: 12px;"></i>
</button>
</div>
<div class="edit-actions" style="display: none;">
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-success btn-save-row" title="Save">
<i data-lucide="check" style="width: 12px; height: 12px;"></i>
</button>
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-secondary btn-cancel-edit" title="Cancel">
<i data-lucide="x" style="width: 12px; height: 12px;"></i>
</button>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if ($total_pages > 1): ?>
<div class="dbm-pagination">
<div class="dbm-pagination-info">
Showing <?php echo (($this->current_page - 1) * $this->per_page) + 1; ?> - <?php echo min($this->current_page * $this->per_page, $this->total_rows); ?> of <?php echo number_format($this->total_rows); ?>
</div>
<div class="dbm-pagination-controls">
<?php if ($this->current_page > 1): ?>
<a href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>/1" class="dbm-pagination-btn">First</a>
<a href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>/<?php echo $this->current_page - 1; ?>" class="dbm-pagination-btn">Prev</a>
<?php endif; ?>
<?php
$start = max(1, $this->current_page - 2);
$end = min($total_pages, $this->current_page + 2);
for ($i = $start; $i <= $end; $i++):
?>
<a href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>/<?php echo $i; ?>"
class="dbm-pagination-btn <?php echo $i === $this->current_page ? 'active' : ''; ?>"><?php echo $i; ?></a>
<?php endfor; ?>
<?php if ($this->current_page < $total_pages): ?>
<a href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>/<?php echo $this->current_page + 1; ?>" class="dbm-pagination-btn">Next</a>
<a href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>/<?php echo $total_pages; ?>" class="dbm-pagination-btn">Last</a>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const wrapper = document.getElementById('data-table-wrapper');
const table = document.getElementById('data-table');
const database = wrapper.dataset.database;
const tableName = wrapper.dataset.table;
const pkColumn = wrapper.dataset.pkColumn;
const baseUrl = '<?php echo Config::get('URL'); ?>';
// Get column info from table headers
function getColumns() {
const headers = table.querySelectorAll('thead th[data-column]');
return Array.from(headers).map(th => ({
name: th.dataset.column,
type: th.dataset.type
}));
}
// Edit row
table.addEventListener('click', function(e) {
const editBtn = e.target.closest('.btn-edit-row');
if (editBtn) {
const row = editBtn.closest('tr');
enterEditMode(row);
}
});
// Save row
table.addEventListener('click', function(e) {
const saveBtn = e.target.closest('.btn-save-row');
if (saveBtn) {
const row = saveBtn.closest('tr');
saveRow(row);
}
});
// Cancel edit
table.addEventListener('click', function(e) {
const cancelBtn = e.target.closest('.btn-cancel-edit');
if (cancelBtn) {
const row = cancelBtn.closest('tr');
cancelEdit(row);
}
});
// Delete row
table.addEventListener('click', function(e) {
const deleteBtn = e.target.closest('.btn-delete-row');
if (deleteBtn) {
const row = deleteBtn.closest('tr');
if (confirm('Delete this row? This cannot be undone.')) {
deleteRow(row);
}
}
});
// Add new row
document.getElementById('btn-add-row').addEventListener('click', function() {
addNewRow();
});
function enterEditMode(row) {
row.classList.add('editing');
row.querySelectorAll('.cell-display').forEach(el => el.style.display = 'none');
row.querySelectorAll('.cell-input').forEach(el => el.style.display = 'block');
row.querySelector('.view-actions').style.display = 'none';
row.querySelector('.edit-actions').style.display = 'inline-flex';
}
function exitEditMode(row) {
row.classList.remove('editing');
row.querySelectorAll('.cell-display').forEach(el => el.style.display = 'inline');
row.querySelectorAll('.cell-input').forEach(el => el.style.display = 'none');
row.querySelector('.view-actions').style.display = 'inline-flex';
row.querySelector('.edit-actions').style.display = 'none';
}
function cancelEdit(row) {
if (row.classList.contains('new-row')) {
row.remove();
// Show empty message if no rows left
const remainingRows = table.querySelectorAll('tbody tr.data-row');
if (remainingRows.length === 0) {
const cols = getColumns();
const emptyRow = document.createElement('tr');
emptyRow.className = 'empty-row';
emptyRow.innerHTML = '<td colspan="' + (cols.length + 1) + '" style="text-align: center; padding: 40px; color: var(--text-muted);">No data in this table. Click "Add Row" to insert data.</td>';
table.querySelector('tbody').appendChild(emptyRow);
}
return;
}
// Restore original values
row.querySelectorAll('.editable-cell').forEach(cell => {
const original = cell.dataset.original;
cell.querySelector('.cell-input').value = original;
});
exitEditMode(row);
}
function saveRow(row) {
const isNew = row.classList.contains('new-row');
const pkValue = row.dataset.pk;
const data = {};
row.querySelectorAll('.editable-cell').forEach(cell => {
const column = cell.dataset.column;
const input = cell.querySelector('.cell-input');
data[column] = input.value;
});
const url = isNew
? baseUrl + 'table/insertRow/' + encodeURIComponent(database) + '/' + encodeURIComponent(tableName)
: baseUrl + 'table/updateRow/' + encodeURIComponent(database) + '/' + encodeURIComponent(tableName);
const formData = new FormData();
if (!isNew) {
formData.append('pk_value', pkValue);
}
for (const key in data) {
formData.append('data[' + key + ']', data[key]);
}
fetch(url, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(result => {
if (result.success) {
// Update display values
row.querySelectorAll('.editable-cell').forEach(cell => {
const input = cell.querySelector('.cell-input');
const display = cell.querySelector('.cell-display');
const value = input.value;
cell.dataset.original = value;
if (value === '' || value === null) {
display.innerHTML = '<span class="null-value">NULL</span>';
} else {
display.textContent = value.length > 100 ? value.substring(0, 100) + '...' : value;
}
});
if (isNew && result.insert_id && pkColumn) {
row.dataset.pk = result.insert_id;
// Update the PK cell display
const pkCell = row.querySelector('.editable-cell[data-column="' + pkColumn + '"]');
if (pkCell) {
pkCell.querySelector('.cell-input').value = result.insert_id;
pkCell.querySelector('.cell-display').textContent = result.insert_id;
pkCell.dataset.original = result.insert_id;
}
}
row.classList.remove('new-row');
exitEditMode(row);
} else {
alert('Error: ' + result.message);
}
})
.catch(error => {
alert('Error saving row: ' + error.message);
});
}
function deleteRow(row) {
const pkValue = row.dataset.pk;
if (!pkValue) {
alert('Cannot delete: no primary key');
return;
}
const formData = new FormData();
formData.append('pk_value', pkValue);
fetch(baseUrl + 'table/deleteRow/' + encodeURIComponent(database) + '/' + encodeURIComponent(tableName), {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(result => {
if (result.success) {
row.remove();
// Show empty message if no rows left
const remainingRows = table.querySelectorAll('tbody tr.data-row');
if (remainingRows.length === 0) {
const cols = getColumns();
const emptyRow = document.createElement('tr');
emptyRow.className = 'empty-row';
emptyRow.innerHTML = '<td colspan="' + (cols.length + 1) + '" style="text-align: center; padding: 40px; color: var(--text-muted);">No data in this table. Click "Add Row" to insert data.</td>';
table.querySelector('tbody').appendChild(emptyRow);
}
} else {
alert('Error: ' + result.message);
}
})
.catch(error => {
alert('Error deleting row: ' + error.message);
});
}
function addNewRow() {
// Remove empty message if present
const emptyRow = table.querySelector('tbody tr.empty-row');
if (emptyRow) {
emptyRow.remove();
}
const columns = getColumns();
const tr = document.createElement('tr');
tr.className = 'data-row new-row editing';
tr.dataset.pk = '';
columns.forEach(col => {
const td = document.createElement('td');
td.className = 'editable-cell';
td.dataset.column = col.name;
td.dataset.original = '';
td.innerHTML = `
<span class="cell-display" style="display: none;"><span class="null-value">NULL</span></span>
<input type="text" class="cell-input dbm-form-input" style="display: block; width: 100%; padding: 4px 8px; font-size: 13px;" value="">
`;
tr.appendChild(td);
});
// Actions cell
const actionsTd = document.createElement('td');
actionsTd.className = 'actions-cell';
actionsTd.style.textAlign = 'center';
actionsTd.style.whiteSpace = 'nowrap';
actionsTd.innerHTML = `
<div class="view-actions" style="display: none;">
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-secondary btn-edit-row" title="Edit">
<i data-lucide="pencil" style="width: 12px; height: 12px;"></i>
</button>
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-danger btn-delete-row" title="Delete">
<i data-lucide="trash-2" style="width: 12px; height: 12px;"></i>
</button>
</div>
<div class="edit-actions" style="display: inline-flex;">
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-success btn-save-row" title="Save">
<i data-lucide="check" style="width: 12px; height: 12px;"></i>
</button>
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-secondary btn-cancel-edit" title="Cancel">
<i data-lucide="x" style="width: 12px; height: 12px;"></i>
</button>
</div>
`;
lucide.createIcons();
tr.appendChild(actionsTd);
// Insert at the top of tbody
const tbody = table.querySelector('tbody');
tbody.insertBefore(tr, tbody.firstChild);
// Focus first input
const firstInput = tr.querySelector('.cell-input');
if (firstInput) {
firstInput.focus();
}
}
});
</script>

View File

@@ -0,0 +1,112 @@
<div class="dbm-content-header">
<div class="dbm-breadcrumb">
<a href="<?php echo Config::get('URL'); ?>database/index">Databases</a>
<span class="separator">/</span>
<a href="<?php echo Config::get('URL'); ?>database/show/<?php echo urlencode($this->database_name); ?>"><?php echo htmlspecialchars($this->database_name); ?></a>
<span class="separator">/</span>
<a href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>"><?php echo htmlspecialchars($this->table_name); ?></a>
<span class="separator">/</span>
<span>Structure</span>
</div>
<div class="dbm-title">
<h1>Structure: <?php echo htmlspecialchars($this->table_name); ?></h1>
<span class="badge"><?php echo count($this->columns); ?> columns</span>
</div>
<div class="dbm-actions">
<a href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>" class="dbm-btn dbm-btn-primary">
<i data-lucide="list"></i>
Browse Data
</a>
<a href="<?php echo Config::get('URL'); ?>table/addColumn/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>" class="dbm-btn dbm-btn-success">
<i data-lucide="plus"></i>
Add Column
</a>
</div>
</div>
<div class="dbm-content-body">
<div class="dbm-card" style="margin-bottom: 24px;">
<div class="dbm-card-header">
<h3>Columns</h3>
</div>
<div class="dbm-table-wrapper" style="border: none; border-radius: 0;">
<table class="dbm-table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Null</th>
<th>Key</th>
<th>Default</th>
<th>Extra</th>
<th style="width: 80px;">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($this->columns as $col): ?>
<tr>
<td style="font-weight: 500;">
<?php if ($col['Key'] === 'PRI'): ?>
<span class="key-column">
<i data-lucide="key-round" style="width: 12px; height: 12px; vertical-align: middle; margin-right: 4px;"></i>
</span>
<?php endif; ?>
<?php echo htmlspecialchars($col['Field']); ?>
</td>
<td><span class="type-column"><?php echo htmlspecialchars($col['Type']); ?></span></td>
<td><?php echo $col['Null'] === 'YES' ? '<span style="color: var(--accent-orange);">YES</span>' : 'NO'; ?></td>
<td>
<?php if ($col['Key'] === 'PRI'): ?>
<span style="color: var(--accent-green);">PRIMARY</span>
<?php elseif ($col['Key'] === 'UNI'): ?>
<span style="color: var(--accent-blue);">UNIQUE</span>
<?php elseif ($col['Key'] === 'MUL'): ?>
<span style="color: var(--accent-purple);">INDEX</span>
<?php else: ?>
-
<?php endif; ?>
</td>
<td><?php echo $col['Default'] !== null ? htmlspecialchars($col['Default']) : '<span class="null-value">NULL</span>'; ?></td>
<td><span class="type-column"><?php echo htmlspecialchars($col['Extra']); ?></span></td>
<td>
<a href="<?php echo Config::get('URL'); ?>table/dropColumn/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>/<?php echo urlencode($col['Field']); ?>"
class="dbm-btn dbm-btn-sm dbm-btn-danger"
data-confirm="Drop column '<?php echo htmlspecialchars($col['Field']); ?>'? This cannot be undone!">Drop</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php if (!empty($this->indexes)): ?>
<div class="dbm-card">
<div class="dbm-card-header">
<h3>Indexes</h3>
</div>
<div class="dbm-table-wrapper" style="border: none; border-radius: 0;">
<table class="dbm-table">
<thead>
<tr>
<th>Key Name</th>
<th>Column</th>
<th>Unique</th>
<th>Type</th>
</tr>
</thead>
<tbody>
<?php foreach ($this->indexes as $idx): ?>
<tr>
<td style="font-weight: 500;"><?php echo htmlspecialchars($idx['Key_name']); ?></td>
<td><?php echo htmlspecialchars($idx['Column_name']); ?></td>
<td><?php echo $idx['Non_unique'] ? '<span style="color: var(--text-muted);">No</span>' : '<span style="color: var(--accent-green);">Yes</span>'; ?></td>
<td><span class="type-column"><?php echo htmlspecialchars($idx['Index_type']); ?></span></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
</div>

BIN
assets/screenshots/img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
assets/screenshots/img2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
assets/screenshots/img3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

953
public/css/dbmanager.css Normal file
View File

@@ -0,0 +1,953 @@
: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:hover .tree-toggle {
background: rgba(0, 0, 0, 0.06);
}
.tree-header.active {
background: var(--dbm-bg-tertiary);
border-left-color: var(--dbm-accent);
}
.tree-toggle {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 2px;
margin-left: -4px;
color: var(--dbm-text-muted);
transition: var(--dbm-transition);
flex-shrink: 0;
border-radius: 3px;
cursor: pointer;
}
.tree-toggle:hover {
background: var(--dbm-bg-tertiary);
color: var(--dbm-text);
}
.tree-toggle svg,
.tree-toggle [data-lucide] {
width: 12px;
height: 12px;
transition: transform 0.15s ease;
pointer-events: none;
}
.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 highlighting colors moved to sql/index.php view */
.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);
}

474
public/css/gallery.css Normal file
View File

@@ -0,0 +1,474 @@
.gallery-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.gallery-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
flex-wrap: wrap;
gap: 16px;
}
.gallery-header h1 {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
.gallery-actions {
display: flex;
gap: 8px;
}
.gallery-back {
display: inline-flex;
align-items: center;
gap: 8px;
color: #666;
text-decoration: none;
font-size: 14px;
transition: color 0.2s;
}
.gallery-back:hover {
color: #333;
}
.gallery-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
text-decoration: none;
transition: all 0.2s;
}
.gallery-btn-primary {
background: #333;
color: #fff;
}
.gallery-btn-primary:hover {
background: #555;
}
.gallery-btn-secondary {
background: #f0f0f0;
color: #333;
}
.gallery-btn-secondary:hover {
background: #e0e0e0;
}
.gallery-btn-danger {
background: #dc3545;
color: #fff;
}
.gallery-btn-danger:hover {
background: #c82333;
}
.gallery-btn-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.9);
color: #666;
text-decoration: none;
transition: all 0.2s;
}
.gallery-btn-icon:hover {
background: #fff;
color: #333;
}
.gallery-btn-icon.gallery-btn-danger:hover {
background: #dc3545;
color: #fff;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 16px;
}
.gallery-item {
position: relative;
aspect-ratio: 1;
border-radius: 8px;
overflow: hidden;
background: #f0f0f0;
text-decoration: none;
}
.gallery-item img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s;
}
.gallery-item:hover img {
transform: scale(1.05);
}
.gallery-item-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 16px;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
color: #fff;
opacity: 0;
transition: opacity 0.3s;
}
.gallery-item:hover .gallery-item-overlay {
opacity: 1;
}
.gallery-item-title {
display: block;
font-weight: 500;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.gallery-item-author {
display: block;
font-size: 12px;
opacity: 0.8;
margin-top: 4px;
}
.gallery-item-visibility {
display: inline-block;
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
margin-top: 4px;
}
.gallery-item-visibility.public {
background: rgba(40, 167, 69, 0.8);
}
.gallery-item-visibility.private {
background: rgba(255, 193, 7, 0.8);
color: #333;
}
.gallery-item-owned {
position: relative;
}
.gallery-item-owned .gallery-item-actions {
position: absolute;
top: 8px;
right: 8px;
display: flex;
gap: 4px;
opacity: 0;
transition: opacity 0.2s;
}
.gallery-item-owned:hover .gallery-item-actions {
opacity: 1;
}
.gallery-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
}
.gallery-empty-icon {
color: #ccc;
margin-bottom: 16px;
}
.gallery-empty h3 {
margin: 0 0 8px;
font-size: 18px;
color: #333;
}
.gallery-empty p {
margin: 0;
color: #666;
}
.gallery-pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
margin-top: 32px;
padding-top: 24px;
border-top: 1px solid #e0e0e0;
}
.gallery-pagination-info {
font-size: 14px;
color: #666;
}
.gallery-view {
display: grid;
grid-template-columns: 1fr 350px;
gap: 32px;
margin-top: 24px;
}
@media (max-width: 900px) {
.gallery-view {
grid-template-columns: 1fr;
}
}
.gallery-view-image {
background: #f0f0f0;
border-radius: 8px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.gallery-view-image img {
max-width: 100%;
max-height: 70vh;
object-fit: contain;
}
.gallery-view-info h1 {
margin: 0 0 16px;
font-size: 24px;
font-weight: 600;
color: #333;
}
.gallery-view-meta {
display: flex;
flex-direction: column;
gap: 12px;
padding-bottom: 16px;
border-bottom: 1px solid #e0e0e0;
margin-bottom: 16px;
}
.gallery-view-author,
.gallery-view-date,
.gallery-view-size {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #666;
}
.gallery-view-author a {
color: #333;
text-decoration: none;
}
.gallery-view-author a:hover {
text-decoration: underline;
}
.gallery-view-description {
font-size: 14px;
line-height: 1.6;
color: #555;
margin-bottom: 24px;
}
.gallery-view-actions {
display: flex;
gap: 8px;
}
.gallery-form-container {
max-width: 600px;
margin: 0 auto;
}
.gallery-form-container h1 {
margin: 0 0 24px;
font-size: 24px;
font-weight: 600;
color: #333;
}
.gallery-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.gallery-form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.gallery-form-label {
font-size: 14px;
font-weight: 500;
color: #333;
}
.gallery-form-input,
.gallery-form-textarea {
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
font-family: inherit;
transition: border-color 0.2s;
}
.gallery-form-input:focus,
.gallery-form-textarea:focus {
outline: none;
border-color: #333;
}
.gallery-form-textarea {
resize: vertical;
min-height: 100px;
}
.gallery-form-checkbox {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 14px;
color: #333;
}
.gallery-form-checkbox input {
width: 18px;
height: 18px;
cursor: pointer;
}
.gallery-form-hint {
font-size: 12px;
color: #888;
}
.gallery-form-actions {
display: flex;
gap: 8px;
margin-top: 8px;
}
.gallery-upload-zone {
position: relative;
border: 2px dashed #ddd;
border-radius: 8px;
transition: all 0.2s;
min-height: 200px;
}
.gallery-upload-zone:hover,
.gallery-upload-zone.dragover {
border-color: #333;
background: #fafafa;
}
.gallery-upload-zone input[type="file"] {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
}
.gallery-upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: #888;
text-align: center;
gap: 8px;
}
.gallery-upload-placeholder small {
font-size: 12px;
color: #aaa;
}
.gallery-upload-preview {
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
}
.gallery-upload-preview img {
max-width: 100%;
max-height: 300px;
border-radius: 4px;
}
.gallery-upload-remove {
position: absolute;
top: 8px;
right: 8px;
width: 28px;
height: 28px;
border: none;
border-radius: 50%;
background: #333;
color: #fff;
font-size: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.gallery-upload-remove:hover {
background: #dc3545;
}
.gallery-edit-preview {
text-align: center;
margin-bottom: 24px;
}
.gallery-edit-preview img {
max-width: 200px;
max-height: 200px;
border-radius: 8px;
object-fit: cover;
}

View File

@@ -0,0 +1,12 @@
# Deny access to all files except images
<FilesMatch "\.(?!(jpg|jpeg|png|gif|webp)$)[^.]+$">
Deny from all
</FilesMatch>
# Disable directory listing
Options -Indexes
# Disable script execution
<FilesMatch "\.(php|php5|phtml|cgi|pl|py)$">
Deny from all
</FilesMatch>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

433
public/js/dbmanager.js Normal file
View File

@@ -0,0 +1,433 @@
const DBManager = {
baseUrl: '',
init(baseUrl) {
this.baseUrl = baseUrl;
this.initTree();
this.initConsole();
this.initSqlHighlighting();
this.initAjaxForms();
},
initTree() {
document.querySelectorAll('.tree-header').forEach(header => {
// Skip if already initialized
if (header.dataset.treeInit) return;
header.dataset.treeInit = 'true';
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 (don't navigate)
if (toggle) {
e.preventDefault();
e.stopPropagation();
// Lazy-load tables for databases if needed
if (item.dataset.db && children && !children.dataset.loaded) {
this.loadTables(item.dataset.db, children);
}
// Lazy-load columns for tables if needed
else if (item.dataset.table && children && !children.dataset.loaded) {
// Find the parent database name
const dbItem = item.closest('.tree-item[data-db]');
if (dbItem) {
this.loadColumns(dbItem.dataset.db, item.dataset.table, children);
}
}
this.toggleTreeItem(item);
return;
}
// If it's a database or table item with href, navigate
if (href) {
// For databases, also expand and load tables
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 = '<div class="tree-loading">Loading...</div>';
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 || '<div class="tree-empty">No tables</div>';
this.initTree();
this.refreshIcons();
}
} catch (error) {
container.innerHTML = '<div class="tree-error">Failed to load</div>';
}
},
async loadColumns(dbName, tableName, container) {
container.innerHTML = '<div class="tree-loading">Loading...</div>';
container.dataset.loaded = 'true';
try {
const response = await fetch(`${this.baseUrl}database/getColumns/${encodeURIComponent(dbName)}/${encodeURIComponent(tableName)}`);
const data = await response.json();
if (data.success && data.columns) {
let html = '';
data.columns.forEach(col => {
html += `
<div class="tree-item">
<div class="tree-header">
<span class="tree-icon ${col.Key === 'PRI' ? 'key' : 'column'}">
${col.Key === 'PRI' ? this.icons.key : this.icons.column}
</span>
<span class="tree-label">${this.escapeHtml(col.Field)}</span>
<span class="tree-badge">${this.escapeHtml(col.Type)}</span>
</div>
</div>
`;
});
container.innerHTML = html || '<div class="tree-empty">No columns</div>';
this.refreshIcons();
}
} catch (error) {
container.innerHTML = '<div class="tree-error">Failed to load</div>';
}
},
renderTableTreeItem(dbName, tableName, columns) {
const columnsHtml = columns.map(col => `
<div class="tree-item">
<div class="tree-header">
<span class="tree-icon column">
${col.Key === 'PRI' ? this.icons.key : this.icons.column}
</span>
<span class="tree-label">${this.escapeHtml(col.Field)}</span>
<span class="tree-badge">${this.escapeHtml(col.Type)}</span>
</div>
</div>
`).join('');
return `
<div class="tree-item" data-table="${this.escapeHtml(tableName)}">
<div class="tree-header" data-href="${this.baseUrl}table/show/${encodeURIComponent(dbName)}/${encodeURIComponent(tableName)}">
<span class="tree-toggle">${this.icons.chevron}</span>
<span class="tree-icon table">${this.icons.table}</span>
<span class="tree-label">${this.escapeHtml(tableName)}</span>
</div>
<div class="tree-children">
${columnsHtml}
</div>
</div>
`;
},
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 = '<div class="dbm-loading"><div class="dbm-spinner"></div></div>';
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 = `
<div class="dbm-sql-result error">
<div class="dbm-sql-result-header">
${this.icons.error} Error: Failed to execute query
</div>
</div>
`;
}
},
renderSqlResult(data, container) {
if (data.success) {
let tableHtml = '';
if (data.result && data.result.length > 0) {
const columns = Object.keys(data.result[0]);
tableHtml = `
<div class="dbm-table-wrapper">
<table class="dbm-table">
<thead>
<tr>${columns.map(col => `<th>${this.escapeHtml(col)}</th>`).join('')}</tr>
</thead>
<tbody>
${data.result.map(row => `
<tr>${columns.map(col => `
<td>${row[col] === null ? '<span class="null-value">NULL</span>' : this.escapeHtml(String(row[col]).substring(0, 100))}</td>
`).join('')}</tr>
`).join('')}
</tbody>
</table>
</div>
`;
}
container.innerHTML = `
<div class="dbm-sql-result success">
<div class="dbm-sql-result-header">
${this.icons.success} ${this.escapeHtml(data.message)}
<span style="margin-left: auto; color: var(--text-muted); font-size: 12px;">
${data.execution_time}ms
</span>
</div>
${tableHtml ? `<div class="dbm-sql-result-body">${tableHtml}</div>` : ''}
</div>
`;
} else {
container.innerHTML = `
<div class="dbm-sql-result error">
<div class="dbm-sql-result-header">
${this.icons.error} ${this.escapeHtml(data.message)}
</div>
${data.error ? `<div class="dbm-sql-result-body" style="padding: 16px; font-family: monospace; font-size: 13px; color: var(--accent-red);">${this.escapeHtml(data.error)}</div>` : ''}
</div>
`;
}
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, '<span class="sql-string">$&</span>');
code = code.replace(/"([^"\\]|\\.)*"/g, '<span class="sql-string">$&</span>');
code = code.replace(/\b(\d+\.?\d*)\b/g, '<span class="sql-number">$1</span>');
functions.forEach(func => {
const regex = new RegExp(`\\b(${func})\\s*\\(`, 'gi');
code = code.replace(regex, '<span class="sql-function">$1</span>(');
});
keywords.forEach(keyword => {
const regex = new RegExp(`\\b(${keyword.replace(' ', '\\s+')})\\b`, 'gi');
code = code.replace(regex, '<span class="sql-keyword">$1</span>');
});
code = code.replace(/(--[^\n]*)/g, '<span class="sql-comment">$1</span>');
code = code.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="sql-comment">$1</span>');
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 = '<span class="dbm-spinner" style="width:16px;height:16px;border-width:2px;"></span>';
}
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}
<span>${this.escapeHtml(message)}</span>
`;
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: '<i data-lucide="chevron-right"></i>',
database: '<i data-lucide="database"></i>',
table: '<i data-lucide="table"></i>',
column: '<i data-lucide="columns-2"></i>',
key: '<i data-lucide="key-round"></i>',
success: '<i data-lucide="check-circle"></i>',
error: '<i data-lucide="x-circle"></i>',
terminal: '<i data-lucide="terminal"></i>'
},
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);