Compare commits
6 Commits
edcc1b5403
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 33243fbe4a | |||
| d9b4c73baa | |||
| 2f7d11b7d2 | |||
| 01861f08c6 | |||
| a4d386f2c5 | |||
| 674fabb715 |
102
CHANGELOG.md
102
CHANGELOG.md
@@ -1,102 +0,0 @@
|
|||||||
# CHANGE LOG
|
|
||||||
|
|
||||||
For the newest (und unstable) version always check the develop branch, for beta check
|
|
||||||
master branch, for really stable stuff check the releases (the ones that have a real version number :))
|
|
||||||
|
|
||||||
## master branch
|
|
||||||
|
|
||||||
- [slaveek/panique] [PR](https://github.com/panique/huge/pull/773) [#770] fix for sending user back to last visited page after login
|
|
||||||
- [slaveek] [PR](https://github.com/panique/huge/pull/815) lots of code styling fixes
|
|
||||||
- [panique] [#729] Fix, mail sending now returns true or false success status (https://github.com/panique/huge/issues/729)
|
|
||||||
- [OmarElGabry] [PR](https://github.com/panique/huge/pull/693) session id regeneration in certain situations
|
|
||||||
- [OmarElGabry] [PR](https://github.com/panique/huge/pull/693) encrypted cookies
|
|
||||||
- [OmarElGabry] [PR](https://github.com/panique/huge/pull/693) new encryption class
|
|
||||||
- [OmarElGabry] [PR](https://github.com/panique/huge/pull/693) anti-CSRF feature (used in login and username change forms)
|
|
||||||
- [josh-bridge] [PR](https://github.com/panique/huge/pull/689) logged-in user can now change password
|
|
||||||
- [justincdotme] [PR](https://github.com/panique/huge/pull/684) better code for brute-force blocking when logging in
|
|
||||||
- [panique] soft autoinstaller improvements
|
|
||||||
- [panique] updated dependencies to current versions
|
|
||||||
- [Kent55/panique] XSS protection filter
|
|
||||||
- [FAlbanni] XSS protection with better session/cookie, now only allowed on used domain
|
|
||||||
- [panique] there's now a simple Favicon and a fallback to avoid browsers hammering the application requesting favicons
|
|
||||||
- [panique] application has now a page title
|
|
||||||
- [panique] avatar upload feature can now handle jpg, png, gif
|
|
||||||
- [panique/tankerkiller125] avatars folder now does not run any PHP code (security improvement)
|
|
||||||
- [tysonlist] [#657] send user back to last-visited page after successful login (when not being logged in first)
|
|
||||||
- [sandropons] anti-brute-force feature for login process
|
|
||||||
- [panique] removed old Facebook texts (as Login-via-Facebook feature was removed since 3.0)
|
|
||||||
- [oisian/ldmusic] [#608] Deletion / suspension of users, Admin menu
|
|
||||||
- [panique] [#654](https://github.com/panique/huge/issues/654) little frontend navi bug fixed
|
|
||||||
- [Dominic28] [PR](https://github.com/panique/huge/pull/645) added checkboxes to request class
|
|
||||||
- [Dominic28] [PR](https://github.com/panique/huge/pull/644) code style fixes
|
|
||||||
- [M0ritzWeide] [PR](https://github.com/panique/huge/pull/635) added browser caching
|
|
||||||
- [modInfo/panique] [PR](https://github.com/panique/huge/pull/647) added missing view table column
|
|
||||||
|
|
||||||
## 3.1
|
|
||||||
|
|
||||||
Code Quality at Scrutinizer 9.7/10, at Code Climate 3.9/4
|
|
||||||
|
|
||||||
**February 2015**
|
|
||||||
|
|
||||||
- [panique] several code quality improvements (and line reductions :) ) all over the project
|
|
||||||
- [PR](https://github.com/panique/huge/pull/620) [owenr88] view rending now possible with multiple view files
|
|
||||||
- [panique] lots of code refactorings and simplifications all over the project
|
|
||||||
- [PR](https://github.com/panique/huge/pull/615) [Dominic28] Avatar can now be deleted by the user
|
|
||||||
- [panique] First Unit tests :)
|
|
||||||
- [panique] several code quality improvements all over the project
|
|
||||||
- [panique] avatarModel code improvements
|
|
||||||
- [panique] renamed AccountType stuff to UserRole, minor changes
|
|
||||||
|
|
||||||
## 3.0
|
|
||||||
|
|
||||||
Code Quality at Scrutinizer 9.3/10, at Code Climate 3.9/4
|
|
||||||
|
|
||||||
**February 2015**
|
|
||||||
|
|
||||||
- [panique] removed duplicate code in AccountTypeModel
|
|
||||||
- [PR](https://github.com/panique/huge/pull/587) [upperwood] Facebook stuff completely removed from SQL
|
|
||||||
- [panique] tiny text changes
|
|
||||||
|
|
||||||
**January 2015**
|
|
||||||
|
|
||||||
- [panique] added static Text class (gets the messages etc)
|
|
||||||
- [panique] added static Environment class (get the environment)
|
|
||||||
- [panique] added static Config class (gets config easily and according to environment)
|
|
||||||
- [panique] new styling of the entire project: login/index has new look now
|
|
||||||
- [panique] massive refactoring of all model classes: lots of methods have been organized into other model classes
|
|
||||||
- [panique] massive refactoring of all model classes: all methods are static now
|
|
||||||
- [panique] EXPERIMENTAL: added static database call / DatabaseFactory, rebuild NoteModel with static methods
|
|
||||||
- [panique] massive refactoring of mail sending, (chose between PHPMailer, SwiftMailer, native / SMTP or no SMTP)
|
|
||||||
|
|
||||||
**December 2014**
|
|
||||||
|
|
||||||
- [panique] lots of refactorings
|
|
||||||
- [panique] refactored LoginModel'S login() method / LoginController's login() method
|
|
||||||
- [panique] removed COOKIE_DOMAIN (cookie is now valid on the domain/IP it has been created on)
|
|
||||||
- [panique] Abstracting super-globals like $_POST['x'] into Request::post('x')
|
|
||||||
- [panique] entirely removed all the Facebook stuff [will be replaced by new proper Oauth2 solution soon]
|
|
||||||
- [panique] lots of code refactorings and cleaning, deletions of duplicate code
|
|
||||||
- [panique] moving nearly all hardcoded values to config
|
|
||||||
- [panique] new View handling: you'll have to pass vars to the view renderer now
|
|
||||||
- [panique] completely removed Facebook login process from controller (incomplete) [will be replaced by new solution]
|
|
||||||
- [panique] less config, URL/IP is auto-detected now
|
|
||||||
- [panique] added loadConfig() to load a specific config according to environment setting (fallback: development)
|
|
||||||
- [panique] added getEnvironment() to fetch (potential) environment setting
|
|
||||||
- [panique] replaced native super-globals access by wrapper access (Session:get instead of $_SESSION)
|
|
||||||
- [panique] complete frontend rebuilding (incomplete yet)
|
|
||||||
- [panique] massive cleaning of all controllers
|
|
||||||
- [panique] added Session::add() to allow stacking of elements (useful for collecting feedback, errors etc)
|
|
||||||
- [panique] complete rebuild of model handling
|
|
||||||
- [panique] View can now render(), renderWithoutHeaderFooter() and renderJSON
|
|
||||||
- [panique] using Composer's PSR-4 autoloader (in a very basic way currently)
|
|
||||||
- [panique] DB construction needs now port by default
|
|
||||||
- [panique] removed (semi-optional) hashing cost factor (as it's redundant usually)
|
|
||||||
- [panique] email max limit increased to 254/255 (official number)
|
|
||||||
- [panique] simpler and improved core
|
|
||||||
- [panique] improved architecture, controllers are now named like "IndexController"
|
|
||||||
- [panique] moved index.php to /public folder, new .htaccess, new installation guideline
|
|
||||||
- [panique] MVC naming fixes
|
|
||||||
- [nerdalertdk] betters paths, automatic paths
|
|
||||||
- [panique] removed legacy PHP stuff: 5.5.x is now the minimum
|
|
||||||
- [PR](https://github.com/panique/php-login/pull/503) [Malkleth] allow users to request password reset by inputting email as well as user names
|
|
||||||
- [PR](https://github.com/panique/php-login/pull/516) [pein0119] cookie runtime calculation fix
|
|
||||||
386
README.md
386
README.md
@@ -1,220 +1,278 @@
|
|||||||
# HUGE – Installation und Setup
|
# HUGE - Elias Fähnrich
|
||||||
|
|
||||||
## Schnellstart
|
---
|
||||||
|
|
||||||
- Voraussetzungen prüfen (siehe unten)
|
## Anpassung der Useranmeldung
|
||||||
- Abhängigkeiten installieren
|
|
||||||
- Datenbank anlegen
|
|
||||||
- Webserver auf `public/` zeigen lassen
|
|
||||||
- Starten und testen
|
|
||||||
|
|
||||||
Befehle (Beispiele):
|
#### Registrierung mit Google reCAPTCHA v3
|
||||||
|
|
||||||
```bash
|
**Beschreibung:**
|
||||||
# Composer installieren (macOS – über Homebrew)
|
Die Registrierung wurde mit Google reCAPTCHA v3 abgesichert. reCAPTCHA v3 arbeitet unsichtbar im Hintergrund und analysiert das Benutzerverhalten, um Bots zu erkennen. E-Mail-Verifizierung ist deaktiviert - Benutzer werden nach der Registrierung automatisch aktiviert.
|
||||||
brew install composer
|
|
||||||
|
|
||||||
# Abhängigkeiten holen (im Projektordner)
|
**Technische Umsetzung:**
|
||||||
composer install
|
- Integration von Google reCAPTCHA v3 (unsichtbar, keine Checkbox)
|
||||||
|
- Score-basierte Validierung (0.0 = Bot, 1.0 = Mensch)
|
||||||
|
- Blockierung bei Score unter 0.5
|
||||||
|
- Konfiguration über `RECAPTCHA_SITE_KEY` und `RECAPTCHA_SECRET_KEY`
|
||||||
|
|
||||||
# Datenbank & Tabellen anlegen
|
**Code-Beispiel (View):**
|
||||||
mysql -u root -p < application/_installation/01-create-database.sql
|
```html
|
||||||
mysql -u root -p < application/_installation/02-create-table-users.sql
|
<script src="https://www.google.com/recaptcha/api.js?render=SITE_KEY"></script>
|
||||||
mysql -u root -p < application/_installation/03-create-table-notes.sql
|
<script>
|
||||||
|
grecaptcha.ready(function() {
|
||||||
|
grecaptcha.execute('SITE_KEY', {action: 'register'}).then(function(token) {
|
||||||
|
document.getElementById('recaptcha-response').value = token;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
# Rechte für Avatare (je nach OS anpassen)
|
**Code-Beispiel (Server-Validierung):**
|
||||||
# Ubuntu/Debian:
|
```php
|
||||||
sudo chown -R www-data:www-data public/avatars
|
$response = file_get_contents('https://www.google.com/recaptcha/api/siteverify?secret=' . $secret_key . '&response=' . $recaptcha_response);
|
||||||
sudo chmod -R 775 public/avatars
|
$response_data = json_decode($response);
|
||||||
|
if (!$response_data->success || $response_data->score < 0.5) {
|
||||||
|
// Bot erkannt oder Validierung fehlgeschlagen
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
# macOS (Apache Standardnutzer _www):
|
**Vorteile von v3:**
|
||||||
sudo chown -R _www:_www public/avatars
|
- Keine Benutzerinteraktion erforderlich
|
||||||
sudo chmod -R 775 public/avatars
|
- Bessere User Experience
|
||||||
|
- Intelligente Bot-Erkennung durch Verhaltensanalyse
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
---
|
||||||
|
|
||||||
|
## Erweiterung der Admin-Funktionen
|
||||||
|
|
||||||
|
#### 👥 Benutzergruppen-Verwaltung
|
||||||
|
|
||||||
|
**Beschreibung:**
|
||||||
|
Implementierung einer erweiterten Benutzerverwaltung mit Gruppen-System. Anstelle des einfachen Typen-Feldes wurde eine vollwertige Gruppenverwaltung eingeführt.
|
||||||
|
|
||||||
|
**Gruppenstruktur:**
|
||||||
|
- **Admin (Typ 7)**: Voll administrative Rechte
|
||||||
|
- **Gast (Typ 1)**: Leserechte für öffentliche Inhalte
|
||||||
|
- **Normaler Benutzer (Typ 2)**: Standard-Benutzerrechte
|
||||||
|
- **Typen 3-6**: Reserviert für zukünftige Gruppenerweiterungen
|
||||||
|
|
||||||
|
**Technische Umsetzung:**
|
||||||
|
- Erstellung einer neuen Tabelle `user_groups` zur Gruppendefinition
|
||||||
|
- Zuweisung von Gruppennamen zu den Typen
|
||||||
|
- Integration in die Benutzerverwaltung des Admins
|
||||||
|
|
||||||
|
#### Öffentliches Benutzerverzeichnis mit DataTables
|
||||||
|
|
||||||
|
**Beschreibung:**
|
||||||
|
Implementierung einer öffentlichen Benutzerliste, die alle Benutzer und ihre Gruppen anzeigt. Diese Liste ist für alle zugänglich, jedoch schreibgeschützt.
|
||||||
|
|
||||||
|
**Technische Features:**
|
||||||
|
- Integration von DataTables/jQuery für interaktive Tabellen
|
||||||
|
- Sortier- und Filterfunktionen
|
||||||
|
- Paginierung der Benutzerliste
|
||||||
|
- Responsive Darstellung auf verschiedenen Geräten
|
||||||
|
|
||||||
|
**Zugriffsrechte:**
|
||||||
|
- **Öffentlicher Zugriff**: Nur Anzeige der Benutzerliste
|
||||||
|
- **Admin-Zugriff**: Vollständige Verwaltungsfunktionen
|
||||||
|
|
||||||
|
```
|
||||||
|
<!-- Screenshot Platzhalter -->
|
||||||
|
[📸 Screenshot: Admin-Gruppenzuweisung]
|
||||||
|
[📸 Screenshot: Öffentliches Benutzerverzeichnis mit DataTables]
|
||||||
|
[📸 Screenshot: jQuery DataTables in Aktion]
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Voraussetzungen
|
## Notizen-App
|
||||||
|
|
||||||
- PHP >= 5.5 (laut composer.json)
|
#### Persönliche Notizen mit Markdown-Unterstützung
|
||||||
- Composer
|
|
||||||
- Apache 2.4 mit `mod_rewrite`
|
|
||||||
- MySQL/MariaDB
|
|
||||||
- PHP-Erweiterungen: PDO + pdo_mysql, OpenSSL, mbstring
|
|
||||||
- Schreibrechte für `public/avatars/`
|
|
||||||
|
|
||||||
---
|
**Beschreibung:**
|
||||||
|
Implementierung einer vollständigen Notizen-Anwendung mit CRUD-Funktionalität (Create, Read, Update, Delete). Benutzer können persönliche Notizen erstellen, bearbeiten und löschen.
|
||||||
|
|
||||||
## Projekt beziehen
|
**Features:**
|
||||||
|
- **Markdown-Unterstützung**: Notizen werden mit SimpleMarkdown gerendert
|
||||||
|
- **AJAX-Integration**: Alle Operationen ohne Seiten-Neuladung
|
||||||
|
- **Benutzergebunden**: Jeder Benutzer sieht nur seine eigenen Notizen
|
||||||
|
- **Echtzeit-Vorschau**: Markdown wird direkt in HTML umgewandelt
|
||||||
|
|
||||||
- Repository klonen oder entpacken
|
**Technische Umsetzung:**
|
||||||
- In den Projektordner wechseln
|
- NoteController mit vollständiger CRUD-Implementierung
|
||||||
|
- NoteModel für Datenbankoperationen
|
||||||
|
- SimpleMarkdown-Library für Markdown-Parsing
|
||||||
|
- AJAX-Endpoints für dynamische Interaktion
|
||||||
|
|
||||||
---
|
**Zugriffsrechte:**
|
||||||
|
- Nur für angemeldete Benutzer verfügbar
|
||||||
|
- Jeder Benutzer hat nur Zugriff auf seine eigenen Notizen
|
||||||
|
|
||||||
## Abhängigkeiten installieren (Composer)
|
```
|
||||||
|
<!-- Screenshot Platzhalter -->
|
||||||
- Im Projektordner ausführen:
|
[📸 Screenshot: Notizen-Übersicht]
|
||||||
|
[📸 Screenshot: Notiz erstellen/bearbeiten]
|
||||||
```bash
|
[📸 Screenshot: Markdown-Vorschau]
|
||||||
composer install
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Webserver konfigurieren (Apache)
|
## Bildergalerie mit Verschlüsselung
|
||||||
|
|
||||||
- DocumentRoot auf `.../huge/public` setzen
|
#### Sichere Bildergalerie mit AES-256 Verschlüsselung
|
||||||
- `AllowOverride All` aktivieren (damit `.htaccess` greift)
|
|
||||||
- `mod_rewrite` aktivieren
|
|
||||||
- Apache neu starten
|
|
||||||
|
|
||||||
Minimalbeispiel VirtualHost:
|
**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.
|
||||||
|
|
||||||
```apacheconf
|
**Features:**
|
||||||
<VirtualHost *:80>
|
- **AES-256-CBC Verschlüsselung**: Jedes Bild wird mit einem einzigartigen Schlüssel verschlüsselt
|
||||||
ServerName huge.local
|
- **Automatische Thumbnail-Generierung**: Optimierte Vorschaubilder für schnelles Laden
|
||||||
DocumentRoot "/pfad/zu/huge/public"
|
- **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
|
||||||
|
|
||||||
<Directory "/pfad/zu/huge/public">
|
**Technische Umsetzung:**
|
||||||
AllowOverride All
|
|
||||||
Require all granted
|
|
||||||
</Directory>
|
|
||||||
|
|
||||||
SetEnv APPLICATION_ENV development
|
*Verschlüsselung (Upload):*
|
||||||
ErrorLog "${APACHE_LOG_DIR}/huge_error.log"
|
```php
|
||||||
CustomLog "${APACHE_LOG_DIR}/huge_access.log" combined
|
$encryption_key = bin2hex(random_bytes(32));
|
||||||
</VirtualHost>
|
$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);
|
||||||
```
|
```
|
||||||
|
|
||||||
Hinweis: `.htaccess` leitet auf `public/index.php` um.
|
*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`)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
## Datenbank anlegen
|
**Unterstützte Formate:**
|
||||||
|
- JPEG / JPG
|
||||||
|
- PNG (mit Transparenz-Erhaltung)
|
||||||
|
- GIF
|
||||||
|
- WebP
|
||||||
|
|
||||||
- SQL-Skripte in dieser Reihenfolge ausführen (`application/_installation/`):
|
**Zugriffsrechte:**
|
||||||
- `01-create-database.sql`
|
- **Öffentliche Galerie**: Alle öffentlichen Bilder für jeden sichtbar
|
||||||
- `02-create-table-users.sql`
|
- **Meine Bilder**: Nur eigene Bilder (öffentlich und privat)
|
||||||
- `03-create-table-notes.sql`
|
- **Upload/Bearbeiten/Löschen**: Nur für angemeldete Benutzer
|
||||||
|
|
||||||
Beispiel in der Shell:
|
```
|
||||||
|
<!-- Screenshot Platzhalter -->
|
||||||
```bash
|
[📸 Screenshot: Galerie-Übersicht mit Bildraster]
|
||||||
mysql -u root -p < application/_installation/01-create-database.sql
|
[📸 Screenshot: Upload-Formular mit Drag & Drop Zone]
|
||||||
mysql -u root -p < application/_installation/02-create-table-users.sql
|
[📸 Screenshot: Upload-Fortschrittsanzeige]
|
||||||
mysql -u root -p < application/_installation/03-create-table-notes.sql
|
[📸 Screenshot: Einzelbildansicht]
|
||||||
|
[📸 Screenshot: Meine Bilder - Verwaltung]
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Framework-Konfiguration (Entwicklung)
|
## jQuery - Einführung und Grundlagen
|
||||||
|
|
||||||
- Datei: `application/config/config.development.php`
|
#### JavaScript Basics
|
||||||
- Wichtige Schlüssel:
|
jQuery ist eine JavaScript-Bibliothek, die grundlegende JavaScript-Kenntnisse voraussetzt. Wichtige Grundlagen umfassen:
|
||||||
- URL
|
|
||||||
- `URL` (Basis-URL, endet mit `/`; auto-detect möglich)
|
|
||||||
- Pfade
|
|
||||||
- `PATH_CONTROLLER`, `PATH_VIEW` (meist unverändert)
|
|
||||||
- Routing
|
|
||||||
- `DEFAULT_CONTROLLER`, `DEFAULT_ACTION`
|
|
||||||
- Datenbank
|
|
||||||
- `DB_TYPE`, `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASS`, `DB_CHARSET`
|
|
||||||
- Captcha
|
|
||||||
- `CAPTCHA_WIDTH`, `CAPTCHA_HEIGHT`
|
|
||||||
- Cookies & Session
|
|
||||||
- `COOKIE_RUNTIME`, `COOKIE_PATH`, `COOKIE_DOMAIN`, `COOKIE_SECURE`, `COOKIE_HTTP`
|
|
||||||
- `SESSION_RUNTIME`
|
|
||||||
- Avatare/Gravatar
|
|
||||||
- `USE_GRAVATAR`, `GRAVATAR_DEFAULT_IMAGESET`, `GRAVATAR_RATING`
|
|
||||||
- `AVATAR_SIZE`, `AVATAR_JPEG_QUALITY`, `AVATAR_DEFAULT_IMAGE`
|
|
||||||
- Ordner `public/avatars/` beschreibbar machen
|
|
||||||
- Sicherheit
|
|
||||||
- `ENCRYPTION_KEY`, `HMAC_SALT` (für eigene Installation ändern)
|
|
||||||
- E-Mail
|
|
||||||
- `EMAIL_USED_MAILER`, `EMAIL_USE_SMTP`
|
|
||||||
- `EMAIL_SMTP_HOST`, `EMAIL_SMTP_AUTH`, `EMAIL_SMTP_USERNAME`, `EMAIL_SMTP_PASSWORD`, `EMAIL_SMTP_PORT`, `EMAIL_SMTP_ENCRYPTION`
|
|
||||||
- `EMAIL_PASSWORD_RESET_*`, `EMAIL_VERIFICATION_*`
|
|
||||||
|
|
||||||
---
|
- DOM-Manipulation
|
||||||
|
- Event-Handling
|
||||||
|
- Asynchrone Programmierung
|
||||||
|
- Objektorientierte Programmierung
|
||||||
|
|
||||||
## Umgebungen (Environment)
|
#### jQuery Grundlagen
|
||||||
|
Die jQuery-Bibliothek ist zentral für die Interaktivität der Benutzeroberfläche:
|
||||||
|
|
||||||
- Klasse: `application/core/Environment.php`
|
**Einbindung:**
|
||||||
- Ermittelt `APPLICATION_ENV` (Fallback: `development`)
|
```html
|
||||||
- Weitere Datei möglich: `config.production.php`
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
||||||
- Apache-Variable setzen:
|
|
||||||
|
|
||||||
```apacheconf
|
|
||||||
SetEnv APPLICATION_ENV production
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
**Zentrale Elemente:**
|
||||||
|
- **Dollarzeichen ($)**: Hauptselektor für DOM-Elemente
|
||||||
|
- **Basis-Syntax**: `$("element")`
|
||||||
|
- **Document Ready**: `$(document).ready(function(){ ... })`
|
||||||
|
|
||||||
## Verzeichnisrechte
|
**Wichtige Selektoren:**
|
||||||
|
```javascript
|
||||||
|
// ID-Selektor (schnellste Methode)
|
||||||
|
$("#elementId")
|
||||||
|
|
||||||
- `public/avatars/` beschreibbar
|
// Klassen-Selektor
|
||||||
- Optional Logs/Uploads je nach Bedarf
|
$(".klassenname")
|
||||||
|
|
||||||
---
|
// Multi-Selektor
|
||||||
|
$("div, p, a, span")
|
||||||
|
|
||||||
## Starten
|
// Komplexe Selektoren
|
||||||
|
$("#container .item:first-child")
|
||||||
- Browser: `http://<host>/`
|
|
||||||
- Registrierung/Login testen
|
|
||||||
- Mailversand nur mit korrekt konfiguriertem SMTP
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tests ausführen
|
|
||||||
|
|
||||||
- PHPUnit unter `vendor/bin/phpunit`
|
|
||||||
- Konfiguration: `tests/phpunit.xml`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
vendor/bin/phpunit -c tests/phpunit.xml
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
**DOM-Manipulation:**
|
||||||
|
```javascript
|
||||||
|
// Einzelne Eigenschaft ändern
|
||||||
|
$("#test").css("color", "#FFFFFF");
|
||||||
|
|
||||||
## Häufige Probleme
|
// Mehrere Eigenschaften ändern
|
||||||
|
$("#test").css({
|
||||||
|
"color": "#FFFFFF",
|
||||||
|
"height": "25px"
|
||||||
|
});
|
||||||
|
|
||||||
- 404 bei allen Routen
|
// Methodenverkettung (Chaining)
|
||||||
- `mod_rewrite` aktivieren
|
$("#test")
|
||||||
- `AllowOverride All` setzen
|
.css({ "color": "#FFFFFF", "height": "25px" })
|
||||||
- DocumentRoot auf `public/`
|
.html("Neuer Text")
|
||||||
- Falsche Links/Assets
|
.show();
|
||||||
- `URL` in der Config prüfen
|
|
||||||
- Datenbankfehler
|
|
||||||
- `DB_*`-Werte und Rechte prüfen
|
|
||||||
- E-Mails kommen nicht an
|
|
||||||
- `EMAIL_USE_SMTP=true` und SMTP-Daten prüfen
|
|
||||||
- Avatare fehlen
|
|
||||||
- Schreibrechte für `public/avatars/`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Option: Vagrant „One-Click-Installation“
|
|
||||||
|
|
||||||
- Ordner: `_one-click-installation/`
|
|
||||||
- Voraussetzungen: Vagrant + VirtualBox
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd _one-click-installation
|
|
||||||
vagrant up
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- Projekt über die VM nutzen (Host/Ports laut Vagrant-Ausgabe)
|
#### DataTables Integration
|
||||||
|
DataTables erweitert HTML-Tabellen um leistungsstarke Funktionen:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#benutzerTabelle').DataTable({
|
||||||
|
"language": {
|
||||||
|
"url": "//cdn.datatables.net/plug-ins/1.10.25/i18n/German.json"
|
||||||
|
},
|
||||||
|
"pageLength": 25,
|
||||||
|
"responsive": true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Automatische Sortierung aller Spalten
|
||||||
|
- Suchfunktion in Echtzeit
|
||||||
|
- Paginierung mit anpassbarer Seitenlänge
|
||||||
|
- Responsive Darstellung auf mobilen Geräten
|
||||||
|
- Mehrsprachige Unterstützung (Deutsch implementiert)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Struktur (Kurz)
|
<div align="center">
|
||||||
|
<img src="./assets/footer/gray0_ctp_on_line.svg" alt="Gadze"/>
|
||||||
- `public/` (Webroot, `.htaccess`, `index.php`)
|
</div>
|
||||||
- `application/controller/` (Controller)
|
|
||||||
- `application/model/` (Modelle)
|
|
||||||
- `application/view/` (Views)
|
|
||||||
- `application/config/` (Konfiguration je Environment)
|
|
||||||
- `application/_installation/` (SQL-Skripte)
|
|
||||||
- `vendor/` (Composer-Abhängigkeiten)
|
|
||||||
- `tests/` (PHPUnit)
|
|
||||||
25
_installation/gallery_table.sql
Normal file
25
_installation/gallery_table.sql
Normal 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;
|
||||||
@@ -2,5 +2,10 @@ CREATE TABLE IF NOT EXISTS `huge`.`notes` (
|
|||||||
`note_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
`note_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
`note_text` text NOT NULL,
|
`note_text` text NOT NULL,
|
||||||
`user_id` int(11) unsigned NOT NULL,
|
`user_id` int(11) unsigned NOT NULL,
|
||||||
|
`note_timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (`note_id`)
|
PRIMARY KEY (`note_id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='user notes';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='user notes';
|
||||||
|
|
||||||
|
-- Foreign key constraint
|
||||||
|
ALTER TABLE `notes`
|
||||||
|
ADD CONSTRAINT `notes_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ CREATE TABLE IF NOT EXISTS `messages` (
|
|||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
`sender_id` int(11) NOT NULL,
|
`sender_id` int(11) NOT NULL,
|
||||||
`receiver_id` int(11) DEFAULT NULL,
|
`receiver_id` int(11) DEFAULT NULL,
|
||||||
`group_type` enum('admins','moderators','all_users') DEFAULT NULL,
|
`group_type` enum('admins','moderators','all_users','global') DEFAULT NULL,
|
||||||
`subject` varchar(255) NOT NULL,
|
`subject` varchar(255) NOT NULL,
|
||||||
`message` text NOT NULL,
|
`message` text NOT NULL,
|
||||||
`is_read` tinyint(1) NOT NULL DEFAULT 0,
|
`is_read` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
@@ -11,7 +11,8 @@ CREATE TABLE IF NOT EXISTS `messages` (
|
|||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `sender_id` (`sender_id`),
|
KEY `sender_id` (`sender_id`),
|
||||||
KEY `receiver_id` (`receiver_id`),
|
KEY `receiver_id` (`receiver_id`),
|
||||||
KEY `is_read` (`is_read`)
|
KEY `is_read` (`is_read`),
|
||||||
|
KEY `group_type` (`group_type`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
|
||||||
|
|
||||||
-- Foreign key constraints
|
-- Foreign key constraints
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
@@ -70,12 +72,10 @@ return array(
|
|||||||
'DB_PORT' => '3306',
|
'DB_PORT' => '3306',
|
||||||
'DB_CHARSET' => 'utf8',
|
'DB_CHARSET' => 'utf8',
|
||||||
/**
|
/**
|
||||||
* Configuration for: Captcha size
|
* Configuration for: Google reCAPTCHA v2
|
||||||
* The currently used Captcha generator (https://github.com/Gregwar/Captcha) also runs without giving a size,
|
|
||||||
* so feel free to use ->build(); inside CaptchaModel.
|
|
||||||
*/
|
*/
|
||||||
'CAPTCHA_WIDTH' => 359,
|
'RECAPTCHA_SITE_KEY' => '6Lfl-EcsAAAAAG9svnagihb5y6HCNK2cd5W9jQm-',
|
||||||
'CAPTCHA_HEIGHT' => 100,
|
'RECAPTCHA_SECRET_KEY' => '6Lfl-EcsAAAAADusuMYTprgTZ42BVIWPsF_jVtk6',
|
||||||
/**
|
/**
|
||||||
* Configuration for: Cookies
|
* Configuration for: Cookies
|
||||||
* 1209600 seconds = 2 weeks
|
* 1209600 seconds = 2 weeks
|
||||||
|
|||||||
173
application/controller/DatabaseController.php
Normal file
173
application/controller/DatabaseController.php
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class DatabaseController
|
||||||
|
*
|
||||||
|
* Controller for managing databases and showing their structure
|
||||||
|
*/
|
||||||
|
class DatabaseController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Construct this object by extending the basic Controller class
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
// Only admin users can access the database manager
|
||||||
|
Auth::checkAuthentication();
|
||||||
|
Auth::checkAdminAuthentication();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main database management interface
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$this->View->renderDbManager('database/index', array(
|
||||||
|
'databases' => DatabaseModel::getAllDatabases(),
|
||||||
|
'current_db' => Config::get('DB_NAME')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show details of a specific database
|
||||||
|
* @param string $database_name
|
||||||
|
*/
|
||||||
|
public function show($database_name = null)
|
||||||
|
{
|
||||||
|
if (!$database_name) {
|
||||||
|
$database_name = Config::get('DB_NAME');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->View->renderDbManager('database/show', array(
|
||||||
|
'tables' => DatabaseModel::getTablesInDatabase($database_name),
|
||||||
|
'database_name' => $database_name,
|
||||||
|
'table_info' => DatabaseModel::getTableDetails($database_name)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new database
|
||||||
|
*/
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
$database_name = Request::post('database_name');
|
||||||
|
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if (DatabaseModel::createDatabase($database_name)) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Database created successfully'
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Failed to create database'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Redirect::to('database');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a database
|
||||||
|
* @param string $database_name
|
||||||
|
*/
|
||||||
|
public function delete($database_name)
|
||||||
|
{
|
||||||
|
// Prevent deletion of the current database
|
||||||
|
if ($database_name === Config::get('DB_NAME')) {
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Cannot delete the currently connected database'
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Redirect::to('database');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = DatabaseModel::deleteDatabase($database_name);
|
||||||
|
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
if ($success) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Database deleted successfully'
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Failed to delete database'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Redirect::to('database');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get database structure as JSON (AJAX endpoint)
|
||||||
|
* @param string $database_name
|
||||||
|
*/
|
||||||
|
public function getStructure($database_name = null)
|
||||||
|
{
|
||||||
|
if (!$database_name) {
|
||||||
|
$database_name = Config::get('DB_NAME');
|
||||||
|
}
|
||||||
|
|
||||||
|
$structure = DatabaseModel::getDatabaseStructure($database_name);
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'structure' => $structure
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
private function isAjaxRequest()
|
||||||
|
{
|
||||||
|
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||||
|
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
|
||||||
|
}
|
||||||
|
}
|
||||||
207
application/controller/DbUserController.php
Normal file
207
application/controller/DbUserController.php
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class UserController for Database Manager
|
||||||
|
*
|
||||||
|
* Controller for managing MySQL users and privileges
|
||||||
|
*/
|
||||||
|
class DbUserController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Construct this object by extending the basic Controller class
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
// Only admin users can access database user management
|
||||||
|
Auth::checkAuthentication();
|
||||||
|
Auth::checkAdminAuthentication();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all database users
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$this->View->renderDbManager('dbuser/index', array(
|
||||||
|
'users' => DbUserModel::getAllUsers(),
|
||||||
|
'current_user' => Config::get('DB_USER')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new database user
|
||||||
|
*/
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
if (Request::post('submit_create_user')) {
|
||||||
|
$username = Request::post('username');
|
||||||
|
$password = Request::post('password');
|
||||||
|
$host = Request::post('host');
|
||||||
|
$privileges = Request::post('privileges');
|
||||||
|
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if (DbUserModel::createUser($username, $password, $host)) {
|
||||||
|
if (!empty($privileges)) {
|
||||||
|
DbUserModel::updateUserPrivileges($username, $host, $privileges);
|
||||||
|
}
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'User created successfully',
|
||||||
|
'reload' => true
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Failed to create user'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DbUserModel::createUser($username, $password, $host)) {
|
||||||
|
if (!empty($privileges)) {
|
||||||
|
DbUserModel::updateUserPrivileges($username, $host, $privileges);
|
||||||
|
}
|
||||||
|
Redirect::to('dbuser');
|
||||||
|
} else {
|
||||||
|
Redirect::to('dbuser');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->View->renderDbManager('dbuser/create');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit user details and privileges
|
||||||
|
* @param string $username
|
||||||
|
* @param string $host
|
||||||
|
*/
|
||||||
|
public function edit($username, $host)
|
||||||
|
{
|
||||||
|
if (Request::post('submit_edit_user')) {
|
||||||
|
$new_password = Request::post('password');
|
||||||
|
$privileges = Request::post('privileges');
|
||||||
|
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$success = true;
|
||||||
|
$message = 'User updated successfully';
|
||||||
|
|
||||||
|
if (!empty($new_password)) {
|
||||||
|
if (!DbUserModel::updateUserPassword($username, $host, $new_password)) {
|
||||||
|
$success = false;
|
||||||
|
$message = 'Failed to update user password';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($success && !DbUserModel::updateUserPrivileges($username, $host, $privileges)) {
|
||||||
|
$success = false;
|
||||||
|
$message = 'Failed to update user privileges';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($success) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => $message
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $message
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = true;
|
||||||
|
if (!empty($new_password)) {
|
||||||
|
$success = DbUserModel::updateUserPassword($username, $host, $new_password);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($success && !DbUserModel::updateUserPrivileges($username, $host, $privileges)) {
|
||||||
|
$success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Redirect::to('dbuser');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show edit user form
|
||||||
|
$this->View->renderDbManager('dbuser/edit', array(
|
||||||
|
'user' => DbUserModel::getUserDetails($username, $host),
|
||||||
|
'privileges' => DbUserModel::getUserPrivileges($username, $host),
|
||||||
|
'databases' => DatabaseModel::getAllDatabases()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a user
|
||||||
|
* @param string $username
|
||||||
|
* @param string $host
|
||||||
|
*/
|
||||||
|
public function delete($username, $host)
|
||||||
|
{
|
||||||
|
// Prevent deletion of current user
|
||||||
|
if ($username === Config::get('DB_USER')) {
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Cannot delete the currently connected user'
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Redirect::to('dbuser');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = DbUserModel::deleteUser($username, $host);
|
||||||
|
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($success) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'User deleted successfully'
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Failed to delete user'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Redirect::to('dbuser');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show user privileges
|
||||||
|
* @param string $username
|
||||||
|
* @param string $host
|
||||||
|
*/
|
||||||
|
public function privileges($username, $host)
|
||||||
|
{
|
||||||
|
$this->View->renderDbManager('dbuser/privileges', array(
|
||||||
|
'user' => DbUserModel::getUserDetails($username, $host),
|
||||||
|
'privileges' => DbUserModel::getUserPrivileges($username, $host)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the request is an AJAX request
|
||||||
|
*/
|
||||||
|
private function isAjaxRequest()
|
||||||
|
{
|
||||||
|
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||||
|
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
|
||||||
|
}
|
||||||
|
}
|
||||||
226
application/controller/GalleryController.php
Normal file
226
application/controller/GalleryController.php
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,40 +5,53 @@ class MessageController extends Controller
|
|||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
|
||||||
// Require login for all message features
|
|
||||||
Auth::checkAuthentication();
|
Auth::checkAuthentication();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function isAjaxRequest()
|
||||||
* Send a message to a specific user via URL parameters
|
{
|
||||||
* URL format: message/send/{receiver_id}/{subject}/{message}
|
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||||
*/
|
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
|
||||||
|
}
|
||||||
|
|
||||||
public function send()
|
public function send()
|
||||||
{
|
{
|
||||||
// Handle POST request
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$receiver_id = isset($_POST['receiver_id']) ? $_POST['receiver_id'] : null;
|
$receiver_id = isset($_POST['receiver_id']) ? $_POST['receiver_id'] : null;
|
||||||
$subject = isset($_POST['subject']) ? $_POST['subject'] : 'No Subject';
|
$subject = isset($_POST['subject']) ? $_POST['subject'] : 'No Subject';
|
||||||
$message = isset($_POST['message']) ? $_POST['message'] : null;
|
$message = isset($_POST['message']) ? $_POST['message'] : null;
|
||||||
|
|
||||||
if (!$receiver_id || !$message) {
|
if (!$receiver_id || !$message) {
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Receiver and message are required']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Session::add('feedback_negative', 'Receiver and message are required');
|
Session::add('feedback_negative', 'Receiver and message are required');
|
||||||
Redirect::to('message');
|
Redirect::to('message');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the message
|
|
||||||
$sender_id = Session::get('user_id');
|
$sender_id = Session::get('user_id');
|
||||||
$success = MessageModel::sendToUser($sender_id, $receiver_id, $subject, $message);
|
$success = MessageModel::sendToUser($sender_id, $receiver_id, $subject, $message);
|
||||||
|
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
if ($success) {
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Message sent successfully']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Failed to send message']);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ($success) {
|
if ($success) {
|
||||||
Session::add('feedback_positive', 'Message sent successfully');
|
Session::add('feedback_positive', 'Message sent successfully');
|
||||||
} else {
|
} else {
|
||||||
Session::add('feedback_negative', 'Failed to send message');
|
Session::add('feedback_negative', 'Failed to send message');
|
||||||
}
|
}
|
||||||
|
|
||||||
// If coming from conversation view, return there
|
|
||||||
if (isset($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], 'conversation') !== false) {
|
if (isset($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], 'conversation') !== false) {
|
||||||
Redirect::to('message/conversation/' . $receiver_id);
|
Redirect::to('message/conversation/' . $receiver_id);
|
||||||
} else {
|
} else {
|
||||||
@@ -47,7 +60,7 @@ class MessageController extends Controller
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle GET request
|
// GET request: message/send/{receiver_id}/{subject}/{message}
|
||||||
$url_parts = explode('/', trim($_SERVER['REQUEST_URI'], '/'));
|
$url_parts = explode('/', trim($_SERVER['REQUEST_URI'], '/'));
|
||||||
$receiver_id = isset($url_parts[2]) ? $url_parts[2] : null;
|
$receiver_id = isset($url_parts[2]) ? $url_parts[2] : null;
|
||||||
$subject = isset($url_parts[3]) ? urldecode($url_parts[3]) : null;
|
$subject = isset($url_parts[3]) ? urldecode($url_parts[3]) : null;
|
||||||
@@ -59,7 +72,6 @@ class MessageController extends Controller
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify receiver exists
|
|
||||||
$receiver = UserModel::getPublicProfileOfUser($receiver_id);
|
$receiver = UserModel::getPublicProfileOfUser($receiver_id);
|
||||||
if (!$receiver) {
|
if (!$receiver) {
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
@@ -67,7 +79,6 @@ class MessageController extends Controller
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the message
|
|
||||||
$sender_id = Session::get('user_id');
|
$sender_id = Session::get('user_id');
|
||||||
$success = MessageModel::sendToUser($sender_id, $receiver_id, $subject, $message);
|
$success = MessageModel::sendToUser($sender_id, $receiver_id, $subject, $message);
|
||||||
|
|
||||||
@@ -79,14 +90,8 @@ class MessageController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a message to a group via URL parameters
|
|
||||||
* URL format: message/sendgroup/{group_type}/{subject}/{message}
|
|
||||||
* group_type can be: admins, moderators, all_users
|
|
||||||
*/
|
|
||||||
public function sendgroup()
|
public function sendgroup()
|
||||||
{
|
{
|
||||||
// Handle POST request
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$group_type = isset($_POST['group_type']) ? $_POST['group_type'] : null;
|
$group_type = isset($_POST['group_type']) ? $_POST['group_type'] : null;
|
||||||
$subject = isset($_POST['subject']) ? $_POST['subject'] : 'No Subject';
|
$subject = isset($_POST['subject']) ? $_POST['subject'] : 'No Subject';
|
||||||
@@ -98,14 +103,12 @@ class MessageController extends Controller
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate group type
|
|
||||||
if (!in_array($group_type, ['admins', 'moderators', 'all_users'])) {
|
if (!in_array($group_type, ['admins', 'moderators', 'all_users'])) {
|
||||||
Session::add('feedback_negative', 'Invalid group type');
|
Session::add('feedback_negative', 'Invalid group type');
|
||||||
Redirect::to('message');
|
Redirect::to('message');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the message
|
|
||||||
$sender_id = Session::get('user_id');
|
$sender_id = Session::get('user_id');
|
||||||
$success = MessageModel::sendToGroup($sender_id, $group_type, $subject, $message);
|
$success = MessageModel::sendToGroup($sender_id, $group_type, $subject, $message);
|
||||||
|
|
||||||
@@ -119,7 +122,7 @@ class MessageController extends Controller
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle GET request
|
// GET request: message/sendgroup/{group_type}/{subject}/{message}
|
||||||
$url_parts = explode('/', trim($_SERVER['REQUEST_URI'], '/'));
|
$url_parts = explode('/', trim($_SERVER['REQUEST_URI'], '/'));
|
||||||
$group_type = isset($url_parts[2]) ? $url_parts[2] : null;
|
$group_type = isset($url_parts[2]) ? $url_parts[2] : null;
|
||||||
$subject = isset($url_parts[3]) ? urldecode($url_parts[3]) : null;
|
$subject = isset($url_parts[3]) ? urldecode($url_parts[3]) : null;
|
||||||
@@ -131,14 +134,12 @@ class MessageController extends Controller
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate group type
|
|
||||||
if (!in_array($group_type, ['admins', 'moderators', 'all_users'])) {
|
if (!in_array($group_type, ['admins', 'moderators', 'all_users'])) {
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
echo json_encode(['success' => false, 'message' => 'Invalid group type. Must be: admins, moderators, or all_users']);
|
echo json_encode(['success' => false, 'message' => 'Invalid group type. Must be: admins, moderators, or all_users']);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the message
|
|
||||||
$sender_id = Session::get('user_id');
|
$sender_id = Session::get('user_id');
|
||||||
$success = MessageModel::sendToGroup($sender_id, $group_type, $subject, $message);
|
$success = MessageModel::sendToGroup($sender_id, $group_type, $subject, $message);
|
||||||
|
|
||||||
@@ -150,14 +151,50 @@ class MessageController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function reply()
|
||||||
* Show the messenger interface
|
{
|
||||||
*/
|
while (ob_get_level()) ob_end_clean();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Invalid request method']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$receiver_id = isset($_POST['receiver_id']) ? $_POST['receiver_id'] : null;
|
||||||
|
$message = isset($_POST['message']) ? $_POST['message'] : null;
|
||||||
|
|
||||||
|
if (!$receiver_id || !$message) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Receiver and message are required']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$sender_id = Session::get('user_id');
|
||||||
|
if (!$sender_id) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Not logged in']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = MessageModel::sendToUser($sender_id, $receiver_id, 'Direct Message', $message);
|
||||||
|
|
||||||
|
if ($success) {
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Reply sent successfully']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Failed to send reply']);
|
||||||
|
}
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function global()
|
||||||
|
{
|
||||||
|
Redirect::to('message#load-global');
|
||||||
|
}
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$user_id = Session::get('user_id');
|
$user_id = Session::get('user_id');
|
||||||
|
|
||||||
// Get conversations and unread count
|
|
||||||
$conversations = MessageModel::getConversations($user_id);
|
$conversations = MessageModel::getConversations($user_id);
|
||||||
$unread_count = MessageModel::getUnreadCount($user_id);
|
$unread_count = MessageModel::getUnreadCount($user_id);
|
||||||
|
|
||||||
@@ -168,9 +205,6 @@ class MessageController extends Controller
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Show conversation with a specific user
|
|
||||||
*/
|
|
||||||
public function conversation()
|
public function conversation()
|
||||||
{
|
{
|
||||||
$user_id = Session::get('user_id');
|
$user_id = Session::get('user_id');
|
||||||
@@ -182,31 +216,90 @@ class MessageController extends Controller
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user info for the other person
|
|
||||||
$other_user = UserModel::getPublicProfileOfUser($other_user_id);
|
$other_user = UserModel::getPublicProfileOfUser($other_user_id);
|
||||||
if (!$other_user) {
|
if (!$other_user) {
|
||||||
Redirect::to('message');
|
Redirect::to('message');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get messages
|
Redirect::to('message#load-conversation-' . $other_user_id);
|
||||||
$messages = MessageModel::getMessagesWithUser($user_id, $other_user_id);
|
}
|
||||||
|
|
||||||
$this->View->render('message/conversation', array(
|
public function getConversationMessages()
|
||||||
'messages' => $messages,
|
{
|
||||||
'other_user' => $other_user
|
while (ob_get_level()) ob_end_clean();
|
||||||
));
|
|
||||||
|
$user_id = Session::get('user_id');
|
||||||
|
$url_parts = explode('/', trim($_SERVER['REQUEST_URI'], '/'));
|
||||||
|
$other_user_id = isset($url_parts[2]) ? $url_parts[2] : null;
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if (!$other_user_id) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Missing user ID']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$messages = MessageModel::getMessagesWithUser($user_id, $other_user_id);
|
||||||
|
MessageModel::markAsRead($user_id, $other_user_id);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'messages' => $messages]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGlobalMessages()
|
||||||
|
{
|
||||||
|
while (ob_get_level()) ob_end_clean();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
$messages = MessageModel::getGlobalMessages();
|
||||||
|
echo json_encode(['success' => true, 'messages' => $messages]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendToGlobal()
|
||||||
|
{
|
||||||
|
while (ob_get_level()) ob_end_clean();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Invalid request method']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = isset($_POST['message']) ? $_POST['message'] : null;
|
||||||
|
$sender_id = Session::get('user_id');
|
||||||
|
|
||||||
|
if (!$message) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Message is required']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$sender_id) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Not logged in']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = MessageModel::sendToGlobal($sender_id, $message);
|
||||||
|
|
||||||
|
if ($success) {
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Message sent to global chat']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Failed to send message']);
|
||||||
|
}
|
||||||
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get unread count as JSON
|
|
||||||
*/
|
|
||||||
public function unreadcount()
|
public function unreadcount()
|
||||||
{
|
{
|
||||||
|
while (ob_get_level()) ob_end_clean();
|
||||||
|
|
||||||
$user_id = Session::get('user_id');
|
$user_id = Session::get('user_id');
|
||||||
$unread_count = MessageModel::getUnreadCount($user_id);
|
$unread_count = MessageModel::getUnreadCount($user_id);
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
echo json_encode(['unread_count' => $unread_count]);
|
echo json_encode(['unread_count' => $unread_count]);
|
||||||
|
exit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
/**
|
/**
|
||||||
* The note controller: Just an example of simple create, read, update and delete (CRUD) actions.
|
* The note controller: Just an example of simple create, read, update and delete (CRUD) actions.
|
||||||
*/
|
*/
|
||||||
|
require_once __DIR__ . '/../libs/SimpleMarkdown.php';
|
||||||
|
|
||||||
class NoteController extends Controller
|
class NoteController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
@@ -29,6 +31,23 @@ class NoteController extends Controller
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get note as JSON (AJAX endpoint)
|
||||||
|
*/
|
||||||
|
public function getNote($note_id)
|
||||||
|
{
|
||||||
|
$note = NoteModel::getNote($note_id);
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
if ($note) {
|
||||||
|
// Add markdown version
|
||||||
|
$note->note_html = SimpleMarkdown::parse($note->note_text);
|
||||||
|
echo json_encode(['success' => true, 'note' => $note]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Note not found']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method controls what happens when you move to /dashboard/create in your app.
|
* This method controls what happens when you move to /dashboard/create in your app.
|
||||||
* Creates a new note. This is usually the target of form submit actions.
|
* Creates a new note. This is usually the target of form submit actions.
|
||||||
@@ -36,7 +55,18 @@ class NoteController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
NoteModel::createNote(Request::post('note_text'));
|
$success = NoteModel::createNote(Request::post('note_text'));
|
||||||
|
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
if ($success) {
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Note created successfully']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Failed to create note']);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Redirect::to('note');
|
Redirect::to('note');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +89,18 @@ class NoteController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function editSave()
|
public function editSave()
|
||||||
{
|
{
|
||||||
NoteModel::updateNote(Request::post('note_id'), Request::post('note_text'));
|
$success = NoteModel::updateNote(Request::post('note_id'), Request::post('note_text'));
|
||||||
|
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
if ($success) {
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Note updated successfully']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Failed to update note']);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Redirect::to('note');
|
Redirect::to('note');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +112,27 @@ class NoteController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function delete($note_id)
|
public function delete($note_id)
|
||||||
{
|
{
|
||||||
NoteModel::deleteNote($note_id);
|
$success = NoteModel::deleteNote($note_id);
|
||||||
|
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
if ($success) {
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Note deleted successfully']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Failed to delete note']);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Redirect::to('note');
|
Redirect::to('note');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the request is an AJAX request
|
||||||
|
*/
|
||||||
|
private function isAjaxRequest()
|
||||||
|
{
|
||||||
|
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||||
|
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,23 +22,25 @@ class RegisterController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
// only admins can access registration; reuse existing admin auth check
|
if (Session::userIsLoggedIn()) {
|
||||||
Auth::checkAdminAuthentication();
|
Redirect::to('index');
|
||||||
|
return;
|
||||||
|
}
|
||||||
$this->View->render('register/index');
|
$this->View->render('register/index');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Register page action
|
|
||||||
* POST-request after form submit
|
|
||||||
*/
|
|
||||||
public function register_action()
|
public function register_action()
|
||||||
{
|
{
|
||||||
// enforce admin-only for registration
|
if (Session::userIsLoggedIn()) {
|
||||||
Auth::checkAdminAuthentication();
|
Redirect::to('index');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
RegistrationModel::registerNewUser();
|
if (RegistrationModel::registerNewUser()) {
|
||||||
|
Redirect::to('login');
|
||||||
Redirect::to('admin/index');
|
} else {
|
||||||
|
Redirect::to('register');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
182
application/controller/SqlController.php
Normal file
182
application/controller/SqlController.php
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class SqlController
|
||||||
|
*
|
||||||
|
* Controller for executing raw SQL queries
|
||||||
|
*/
|
||||||
|
class SqlController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Construct this object by extending the basic Controller class
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
// Only admin users can access the SQL console
|
||||||
|
Auth::checkAuthentication();
|
||||||
|
Auth::checkAdminAuthentication();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show SQL console interface
|
||||||
|
* @param string $database_name
|
||||||
|
*/
|
||||||
|
public function index($database_name = null)
|
||||||
|
{
|
||||||
|
if (!$database_name) {
|
||||||
|
$database_name = Config::get('DB_NAME');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->View->renderDbManager('sql/index', array(
|
||||||
|
'database_name' => $database_name,
|
||||||
|
'databases' => DatabaseModel::getAllDatabases(),
|
||||||
|
'history' => SqlModel::getQueryHistory(Session::get('user_id'))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute SQL query
|
||||||
|
*/
|
||||||
|
public function execute()
|
||||||
|
{
|
||||||
|
$database_name = Request::post('database_name') ?: Config::get('DB_NAME');
|
||||||
|
$sql_query = Request::post('sql_query');
|
||||||
|
|
||||||
|
if (empty($sql_query)) {
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'SQL query cannot be empty'
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Redirect::to('sql');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = SqlModel::executeQuery($database_name, $sql_query, Session::get('user_id'));
|
||||||
|
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => $result['message'],
|
||||||
|
'result' => $result['result'],
|
||||||
|
'affected_rows' => $result['affected_rows'],
|
||||||
|
'execution_time' => $result['execution_time'],
|
||||||
|
'query_type' => $result['query_type']
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $result['message'],
|
||||||
|
'error' => $result['error']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-AJAX: redirect with results in session
|
||||||
|
Session::set('sql_result', $result);
|
||||||
|
Redirect::to('sql/index/' . urlencode($database_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get query history as JSON (AJAX endpoint)
|
||||||
|
*/
|
||||||
|
public function getHistory()
|
||||||
|
{
|
||||||
|
$history = SqlModel::getQueryHistory(Session::get('user_id'));
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'history' => $history
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear query history
|
||||||
|
*/
|
||||||
|
public function clearHistory()
|
||||||
|
{
|
||||||
|
$success = SqlModel::clearQueryHistory(Session::get('user_id'));
|
||||||
|
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($success) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Query history cleared successfully'
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Failed to clear query history'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Redirect::to('sql');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get database schema for autocomplete
|
||||||
|
* @param string $database_name
|
||||||
|
*/
|
||||||
|
public function getSchema($database_name = null)
|
||||||
|
{
|
||||||
|
if (!$database_name) {
|
||||||
|
$database_name = Config::get('DB_NAME');
|
||||||
|
}
|
||||||
|
|
||||||
|
$schema = SqlModel::getDatabaseSchema($database_name);
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'schema' => $schema
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format SQL query (AJAX endpoint)
|
||||||
|
*/
|
||||||
|
public function formatQuery()
|
||||||
|
{
|
||||||
|
$query = Request::post('query');
|
||||||
|
|
||||||
|
if (empty($query)) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Query cannot be empty'
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$formatted = SqlModel::formatQuery($query);
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'formatted' => $formatted
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the request is an AJAX request
|
||||||
|
*/
|
||||||
|
private function isAjaxRequest()
|
||||||
|
{
|
||||||
|
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||||
|
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
|
||||||
|
}
|
||||||
|
}
|
||||||
397
application/controller/TableController.php
Normal file
397
application/controller/TableController.php
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class TableController
|
||||||
|
*
|
||||||
|
* Controller for managing database tables
|
||||||
|
*/
|
||||||
|
class TableController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Construct this object by extending the basic Controller class
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
// Only admin users can access the table manager
|
||||||
|
Auth::checkAuthentication();
|
||||||
|
Auth::checkAdminAuthentication();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show table content with pagination
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @param int $page
|
||||||
|
*/
|
||||||
|
public function show($database_name = null, $table_name = null, $page = 1)
|
||||||
|
{
|
||||||
|
if (!$database_name) {
|
||||||
|
$database_name = Config::get('DB_NAME');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table_name) {
|
||||||
|
Redirect::to('database/show/' . urlencode($database_name));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$page = (int)$page;
|
||||||
|
$per_page = 20;
|
||||||
|
|
||||||
|
$this->View->renderDbManager('table/show', array(
|
||||||
|
'database_name' => $database_name,
|
||||||
|
'table_name' => $table_name,
|
||||||
|
'columns' => TableModel::getTableColumns($database_name, $table_name),
|
||||||
|
'rows' => TableModel::getTableRows($database_name, $table_name, $page, $per_page),
|
||||||
|
'total_rows' => TableModel::getTableRowCount($database_name, $table_name),
|
||||||
|
'current_page' => $page,
|
||||||
|
'per_page' => $per_page,
|
||||||
|
'table_info' => TableModel::getTableInfo($database_name, $table_name)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new table
|
||||||
|
* @param string $database_name
|
||||||
|
*/
|
||||||
|
public function create($database_name = null)
|
||||||
|
{
|
||||||
|
if (!$database_name) {
|
||||||
|
$database_name = Config::get('DB_NAME');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Request::post('submit_create_table')) {
|
||||||
|
$table_name = Request::post('table_name');
|
||||||
|
$columns = Request::post('columns');
|
||||||
|
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if (TableModel::createTable($database_name, $table_name, $columns)) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Table created successfully',
|
||||||
|
'redirect' => Config::get('URL') . 'table/show/' . urlencode($database_name) . '/' . urlencode($table_name)
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Failed to create table'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TableModel::createTable($database_name, $table_name, $columns)) {
|
||||||
|
Redirect::to('table/show/' . urlencode($database_name) . '/' . urlencode($table_name));
|
||||||
|
} else {
|
||||||
|
Redirect::to('database/show/' . urlencode($database_name));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show create table form
|
||||||
|
$this->View->renderDbManager('table/create', array(
|
||||||
|
'database_name' => $database_name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show table structure
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
*/
|
||||||
|
public function structure($database_name = null, $table_name = null)
|
||||||
|
{
|
||||||
|
if (!$database_name) {
|
||||||
|
$database_name = Config::get('DB_NAME');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table_name) {
|
||||||
|
Redirect::to('database/show/' . urlencode($database_name));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->View->renderDbManager('table/structure', array(
|
||||||
|
'database_name' => $database_name,
|
||||||
|
'table_name' => $table_name,
|
||||||
|
'columns' => TableModel::getTableColumns($database_name, $table_name),
|
||||||
|
'indexes' => TableModel::getTableIndexes($database_name, $table_name),
|
||||||
|
'table_info' => TableModel::getTableInfo($database_name, $table_name)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a column to a table
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
*/
|
||||||
|
public function addColumn($database_name = null, $table_name = null)
|
||||||
|
{
|
||||||
|
if (!$database_name) {
|
||||||
|
$database_name = Config::get('DB_NAME');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table_name) {
|
||||||
|
Redirect::to('database/show/' . urlencode($database_name));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Request::post('submit_add_column')) {
|
||||||
|
$column_name = Request::post('column_name');
|
||||||
|
$column_type = Request::post('column_type');
|
||||||
|
$column_null = Request::post('column_null');
|
||||||
|
$column_key = Request::post('column_key');
|
||||||
|
$column_default = Request::post('column_default');
|
||||||
|
$column_extra = Request::post('column_extra');
|
||||||
|
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if (TableModel::addColumn($database_name, $table_name, $column_name, $column_type, $column_null, $column_key, $column_default, $column_extra)) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Column added successfully',
|
||||||
|
'reload' => true
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Failed to add column'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TableModel::addColumn($database_name, $table_name, $column_name, $column_type, $column_null, $column_key, $column_default, $column_extra)) {
|
||||||
|
Redirect::to('table/structure/' . urlencode($database_name) . '/' . urlencode($table_name));
|
||||||
|
} else {
|
||||||
|
Redirect::to('table/structure/' . urlencode($database_name) . '/' . urlencode($table_name));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show add column form
|
||||||
|
$this->View->renderDbManager('table/add_column', array(
|
||||||
|
'database_name' => $database_name,
|
||||||
|
'table_name' => $table_name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop a column from a table
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @param string $column_name
|
||||||
|
*/
|
||||||
|
public function dropColumn($database_name, $table_name, $column_name)
|
||||||
|
{
|
||||||
|
$success = TableModel::dropColumn($database_name, $table_name, $column_name);
|
||||||
|
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
if ($success) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Column dropped successfully',
|
||||||
|
'reload' => true
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Failed to drop column'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Redirect::to('table/structure/' . urlencode($database_name) . '/' . urlencode($table_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a table
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
*/
|
||||||
|
public function delete($database_name, $table_name)
|
||||||
|
{
|
||||||
|
$success = TableModel::deleteTable($database_name, $table_name);
|
||||||
|
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
if ($success) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Table deleted successfully',
|
||||||
|
'redirect' => Config::get('URL') . 'database/show/' . urlencode($database_name)
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Failed to delete table'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Redirect::to('database/show/' . urlencode($database_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a row in the table (AJAX)
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
*/
|
||||||
|
public function updateRow($database_name = null, $table_name = null)
|
||||||
|
{
|
||||||
|
if (!$database_name || !$table_name) {
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Invalid parameters']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Redirect::to('database/index');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pk_value = Request::post('pk_value');
|
||||||
|
$data = Request::post('data');
|
||||||
|
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if (!$pk_value || !$data) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Missing required data']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TableModel::updateRow($database_name, $table_name, $pk_value, $data)) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Row updated successfully'
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Failed to update row'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Redirect::to('table/show/' . urlencode($database_name) . '/' . urlencode($table_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a row from the table (AJAX)
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
*/
|
||||||
|
public function deleteRow($database_name = null, $table_name = null)
|
||||||
|
{
|
||||||
|
if (!$database_name || !$table_name) {
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Invalid parameters']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Redirect::to('database/index');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pk_value = Request::post('pk_value');
|
||||||
|
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if (!$pk_value) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Missing primary key value']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TableModel::deleteRow($database_name, $table_name, $pk_value)) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Row deleted successfully'
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Failed to delete row'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Redirect::to('table/show/' . urlencode($database_name) . '/' . urlencode($table_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a new row into the table (AJAX)
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
*/
|
||||||
|
public function insertRow($database_name = null, $table_name = null)
|
||||||
|
{
|
||||||
|
if (!$database_name || !$table_name) {
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Invalid parameters']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Redirect::to('database/index');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = Request::post('data');
|
||||||
|
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if (!$data) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Missing row data']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$insertId = TableModel::insertRow($database_name, $table_name, $data);
|
||||||
|
if ($insertId !== false) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Row inserted successfully',
|
||||||
|
'insert_id' => $insertId
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Failed to insert row'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Redirect::to('table/show/' . urlencode($database_name) . '/' . urlencode($table_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the request is an AJAX request
|
||||||
|
*/
|
||||||
|
private function isAjaxRequest()
|
||||||
|
{
|
||||||
|
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,41 @@ class View
|
|||||||
public $avatar_file_path;
|
public $avatar_file_path;
|
||||||
public $user_account_type;
|
public $user_account_type;
|
||||||
|
|
||||||
|
/* Messenger properties */
|
||||||
|
public $conversations;
|
||||||
|
public $unread_count;
|
||||||
|
public $all_users;
|
||||||
|
|
||||||
|
/* Note properties */
|
||||||
|
public $messages;
|
||||||
|
public $other_user;
|
||||||
|
|
||||||
|
/* Group properties */
|
||||||
|
public $groups;
|
||||||
|
|
||||||
|
/* Database Manager properties */
|
||||||
|
public $databases;
|
||||||
|
public $database_name;
|
||||||
|
public $current_db;
|
||||||
|
public $tables;
|
||||||
|
public $table_name;
|
||||||
|
public $table_info;
|
||||||
|
public $columns;
|
||||||
|
public $rows;
|
||||||
|
public $indexes;
|
||||||
|
public $total_rows;
|
||||||
|
public $current_page;
|
||||||
|
public $per_page;
|
||||||
|
public $history;
|
||||||
|
public $user;
|
||||||
|
public $privileges;
|
||||||
|
public $current_user;
|
||||||
|
|
||||||
|
/* 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
|
||||||
*/
|
*/
|
||||||
@@ -107,6 +142,24 @@ class View
|
|||||||
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
|
||||||
*/
|
*/
|
||||||
@@ -197,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.
|
||||||
|
|||||||
102
application/libs/SimpleMarkdown.php
Normal file
102
application/libs/SimpleMarkdown.php
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple Markdown Parser
|
||||||
|
* Supports basic markdown syntax:
|
||||||
|
* # Headers
|
||||||
|
* **bold** *italic* `code`
|
||||||
|
* - lists
|
||||||
|
* [links](url)
|
||||||
|
* > quotes
|
||||||
|
*/
|
||||||
|
|
||||||
|
class SimpleMarkdown
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Parse markdown text to HTML
|
||||||
|
*/
|
||||||
|
public static function parse($text)
|
||||||
|
{
|
||||||
|
// Convert special characters to HTML entities first
|
||||||
|
$text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
|
||||||
|
|
||||||
|
// Headers (h1-h6)
|
||||||
|
$text = preg_replace('/^#{1,6}\s+(.+)$/m', '<h3>$1</h3>', $text);
|
||||||
|
|
||||||
|
// Bold text
|
||||||
|
$text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text);
|
||||||
|
|
||||||
|
// Italic text
|
||||||
|
$text = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $text);
|
||||||
|
|
||||||
|
// Inline code
|
||||||
|
$text = preg_replace('/`(.+?)`/', '<code>$1</code>', $text);
|
||||||
|
|
||||||
|
// Code blocks
|
||||||
|
$text = preg_replace('/```(.+?)```/s', '<pre><code>$1</code></pre>', $text);
|
||||||
|
|
||||||
|
// Links
|
||||||
|
$text = preg_replace('/\[(.+?)\]\((.+?)\)/', '<a href="$2" target="_blank">$1</a>', $text);
|
||||||
|
|
||||||
|
// Blockquotes
|
||||||
|
$text = preg_replace('/^>\s+(.+)$/m', '<blockquote>$1</blockquote>', $text);
|
||||||
|
|
||||||
|
// Unordered lists
|
||||||
|
$text = preg_replace('/^\-\s+(.+)$/m', '<li>$1</li>', $text);
|
||||||
|
$text = preg_replace('/(<li>.*<\/li>)/s', '<ul>$1</ul>', $text);
|
||||||
|
|
||||||
|
// Line breaks
|
||||||
|
$text = nl2br($text);
|
||||||
|
|
||||||
|
// Clean up multiple consecutive line breaks
|
||||||
|
$text = preg_replace('/(<br\s*\/?>\s*){3,}/', '<br><br>', $text);
|
||||||
|
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert HTML back to markdown (simplified)
|
||||||
|
* This is a basic implementation - may not handle all cases perfectly
|
||||||
|
*/
|
||||||
|
public static function toMarkdown($html)
|
||||||
|
{
|
||||||
|
// Basic HTML to markdown conversion
|
||||||
|
$markdown = $html;
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
$markdown = preg_replace('/<h[1-6]>(.+?)<\/h[1-6]>/i', '# $1', $markdown);
|
||||||
|
|
||||||
|
// Bold
|
||||||
|
$markdown = preg_replace('/<strong>(.+?)<\/strong>/i', '**$1**', $markdown);
|
||||||
|
|
||||||
|
// Italic
|
||||||
|
$markdown = preg_replace('/<em>(.+?)<\/em>/i', '*$1*', $markdown);
|
||||||
|
|
||||||
|
// Code
|
||||||
|
$markdown = preg_replace('/<code>(.+?)<\/code>/i', '`$1`', $markdown);
|
||||||
|
$markdown = preg_replace('/<pre><code>(.+?)<\/code><\/pre>/is', "```$1```", $markdown);
|
||||||
|
|
||||||
|
// Links
|
||||||
|
$markdown = preg_replace('/<a href="(.+?)"(?:\s+target="_blank")?>(.+?)<\/a>/i', '[$2]($1)', $markdown);
|
||||||
|
|
||||||
|
// Blockquotes
|
||||||
|
$markdown = preg_replace('/<blockquote>(.+?)<\/blockquote>/i', '> $1', $markdown);
|
||||||
|
|
||||||
|
// Lists (simplified)
|
||||||
|
$markdown = preg_replace('/<ul>(.+?)<\/ul>/is', '$1', $markdown);
|
||||||
|
$markdown = preg_replace('/<li>(.+?)<\/li>/i', '- $1', $markdown);
|
||||||
|
|
||||||
|
// Line breaks
|
||||||
|
$markdown = preg_replace('/<br\s*\/?>/i', "\n", $markdown);
|
||||||
|
|
||||||
|
// Decode HTML entities
|
||||||
|
$markdown = htmlspecialchars_decode($markdown, ENT_QUOTES);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
$markdown = preg_replace('/\n{3,}/', "\n\n", $markdown);
|
||||||
|
$markdown = trim($markdown);
|
||||||
|
|
||||||
|
return $markdown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
275
application/model/DatabaseModel.php
Normal file
275
application/model/DatabaseModel.php
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class DatabaseModel
|
||||||
|
*
|
||||||
|
* Model for database operations using PDO
|
||||||
|
*/
|
||||||
|
class DatabaseModel
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get all databases on the server
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getAllDatabases()
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
$sql = "SHOW DATABASES";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
$databases = $query->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
$system_dbs = ['information_schema', 'performance_schema', 'mysql', 'sys'];
|
||||||
|
return array_diff($databases, $system_dbs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all tables in a specific database
|
||||||
|
* @param string $database_name
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getTablesInDatabase($database_name)
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
$sql = "SHOW TABLES FROM `" . $database_name . "`";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
return $query->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get detailed information about tables in a database
|
||||||
|
* @param string $database_name
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getTableDetails($database_name)
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
$tables = self::getTablesInDatabase($database_name);
|
||||||
|
$table_details = array();
|
||||||
|
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
$sql = "SHOW TABLE STATUS FROM `" . $database_name . "` LIKE :table_name";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute(array(':table_name' => $table));
|
||||||
|
|
||||||
|
$details = $query->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if ($details) {
|
||||||
|
$table_details[$table] = array(
|
||||||
|
'engine' => $details['Engine'],
|
||||||
|
'rows' => $details['Rows'],
|
||||||
|
'data_size' => self::formatBytes($details['Data_length']),
|
||||||
|
'index_size' => self::formatBytes($details['Index_length']),
|
||||||
|
'total_size' => self::formatBytes($details['Data_length'] + $details['Index_length']),
|
||||||
|
'collation' => $details['Collation'],
|
||||||
|
'comment' => $details['Comment']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $table_details;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get complete database structure (tables and columns)
|
||||||
|
* @param string $database_name
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getDatabaseStructure($database_name)
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
$structure = array();
|
||||||
|
$tables = self::getTablesInDatabase($database_name);
|
||||||
|
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
$sql = "DESCRIBE `" . $database_name . "`.`" . $table . "`";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
$columns = $query->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
$structure[$table] = $columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $structure;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new database
|
||||||
|
* @param string $database_name
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function createDatabase($database_name)
|
||||||
|
{
|
||||||
|
if (!$database_name || !preg_match('/^[a-zA-Z0-9_]+$/', $database_name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "CREATE DATABASE `" . $database_name . "` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
return $query->execute();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a database
|
||||||
|
* @param string $database_name
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function deleteDatabase($database_name)
|
||||||
|
{
|
||||||
|
if (!$database_name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "DROP DATABASE `" . $database_name . "`";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
return $query->execute();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get table columns with details
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getTableColumns($database_name, $table_name)
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
$sql = "SHOW COLUMNS FROM `" . $database_name . "`.`" . $table_name . "`";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
return $query->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export database as SQL dump
|
||||||
|
* @param string $database_name
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function exportDatabase($database_name)
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
$output = "-- Database Export: " . $database_name . "\n";
|
||||||
|
$output .= "-- Generated: " . date('Y-m-d H:i:s') . "\n\n";
|
||||||
|
$output .= "SET FOREIGN_KEY_CHECKS=0;\n";
|
||||||
|
$output .= "SET SQL_MODE = \"NO_AUTO_VALUE_ON_ZERO\";\n\n";
|
||||||
|
|
||||||
|
$tables = self::getTablesInDatabase($database_name);
|
||||||
|
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
$sql = "SHOW CREATE TABLE `" . $database_name . "`.`" . $table . "`";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute();
|
||||||
|
$row = $query->fetch(PDO::FETCH_NUM);
|
||||||
|
|
||||||
|
$output .= "DROP TABLE IF EXISTS `" . $table . "`;\n";
|
||||||
|
$output .= $row[1] . ";\n\n";
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM `" . $database_name . "`.`" . $table . "`";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute();
|
||||||
|
$rows = $query->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!empty($rows)) {
|
||||||
|
foreach ($rows as $dataRow) {
|
||||||
|
$columns = array_keys($dataRow);
|
||||||
|
$values = array_map(function($val) use ($database) {
|
||||||
|
if ($val === null) {
|
||||||
|
return 'NULL';
|
||||||
|
}
|
||||||
|
return $database->quote($val);
|
||||||
|
}, array_values($dataRow));
|
||||||
|
|
||||||
|
$output .= "INSERT INTO `" . $table . "` (`" . implode("`, `", $columns) . "`) VALUES (" . implode(", ", $values) . ");\n";
|
||||||
|
}
|
||||||
|
$output .= "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$output .= "SET FOREIGN_KEY_CHECKS=1;\n";
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export single table as SQL dump
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function exportTable($database_name, $table_name)
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
$output = "-- Table Export: " . $table_name . " from " . $database_name . "\n";
|
||||||
|
$output .= "-- Generated: " . date('Y-m-d H:i:s') . "\n\n";
|
||||||
|
$output .= "SET FOREIGN_KEY_CHECKS=0;\n\n";
|
||||||
|
|
||||||
|
$sql = "SHOW CREATE TABLE `" . $database_name . "`.`" . $table_name . "`";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute();
|
||||||
|
$row = $query->fetch(PDO::FETCH_NUM);
|
||||||
|
|
||||||
|
$output .= "DROP TABLE IF EXISTS `" . $table_name . "`;\n";
|
||||||
|
$output .= $row[1] . ";\n\n";
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM `" . $database_name . "`.`" . $table_name . "`";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute();
|
||||||
|
$rows = $query->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!empty($rows)) {
|
||||||
|
foreach ($rows as $dataRow) {
|
||||||
|
$columns = array_keys($dataRow);
|
||||||
|
$values = array_map(function($val) use ($database) {
|
||||||
|
if ($val === null) {
|
||||||
|
return 'NULL';
|
||||||
|
}
|
||||||
|
return $database->quote($val);
|
||||||
|
}, array_values($dataRow));
|
||||||
|
|
||||||
|
$output .= "INSERT INTO `" . $table_name . "` (`" . implode("`, `", $columns) . "`) VALUES (" . implode(", ", $values) . ");\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$output .= "\nSET FOREIGN_KEY_CHECKS=1;\n";
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format bytes to human readable format
|
||||||
|
* @param int $bytes
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private static function formatBytes($bytes)
|
||||||
|
{
|
||||||
|
if ($bytes >= 1073741824) {
|
||||||
|
return number_format($bytes / 1073741824, 2) . ' GB';
|
||||||
|
} elseif ($bytes >= 1048576) {
|
||||||
|
return number_format($bytes / 1048576, 2) . ' MB';
|
||||||
|
} elseif ($bytes >= 1024) {
|
||||||
|
return number_format($bytes / 1024, 2) . ' KB';
|
||||||
|
} elseif ($bytes > 1) {
|
||||||
|
return $bytes . ' bytes';
|
||||||
|
} elseif ($bytes == 1) {
|
||||||
|
return '1 byte';
|
||||||
|
} else {
|
||||||
|
return '0 bytes';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
202
application/model/DbUserModel.php
Normal file
202
application/model/DbUserModel.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
323
application/model/GalleryModel.php
Normal file
323
application/model/GalleryModel.php
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,8 @@ class MessageModel
|
|||||||
{
|
{
|
||||||
$database = DatabaseFactory::getFactory()->getConnection();
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
$sql = "INSERT INTO messages (sender_id, receiver_id, group_type, subject, message)
|
$sql = "INSERT INTO messages (sender_id, receiver_id, group_type, subject, message, is_read)
|
||||||
VALUES (:sender_id, :receiver_id, :group_type, :subject, :message)";
|
VALUES (:sender_id, :receiver_id, :group_type, :subject, :message, 0)";
|
||||||
$query = $database->prepare($sql);
|
$query = $database->prepare($sql);
|
||||||
return $query->execute(array(
|
return $query->execute(array(
|
||||||
':sender_id' => $sender_id,
|
':sender_id' => $sender_id,
|
||||||
@@ -170,6 +170,36 @@ class MessageModel
|
|||||||
return $query->fetchAll();
|
return $query->fetchAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getGlobalMessages()
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
$sql = "SELECT m.*, u.user_name as sender_name,
|
||||||
|
'received' as message_type
|
||||||
|
FROM messages m
|
||||||
|
JOIN users u ON m.sender_id = u.user_id
|
||||||
|
WHERE m.group_type = 'global'
|
||||||
|
AND m.receiver_id IS NULL
|
||||||
|
ORDER BY m.created_at ASC";
|
||||||
|
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute();
|
||||||
|
return $query->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function sendToGlobal($sender_id, $message)
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
$sql = "INSERT INTO messages (sender_id, group_type, subject, message, is_read)
|
||||||
|
VALUES (:sender_id, 'global', 'Global Chat', :message, 1)";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
return $query->execute(array(
|
||||||
|
':sender_id' => $sender_id,
|
||||||
|
':message' => $message
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
public static function getAllUsers($current_user_id)
|
public static function getAllUsers($current_user_id)
|
||||||
{
|
{
|
||||||
$database = DatabaseFactory::getFactory()->getConnection();
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|||||||
@@ -14,12 +14,20 @@ class NoteModel
|
|||||||
{
|
{
|
||||||
$database = DatabaseFactory::getFactory()->getConnection();
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
$sql = "SELECT user_id, note_id, note_text FROM notes WHERE user_id = :user_id";
|
$sql = "SELECT note_id, note_text, note_timestamp FROM notes WHERE user_id = :user_id ORDER BY note_timestamp DESC";
|
||||||
$query = $database->prepare($sql);
|
$query = $database->prepare($sql);
|
||||||
$query->execute(array(':user_id' => Session::get('user_id')));
|
$query->execute(array(':user_id' => Session::get('user_id')));
|
||||||
|
|
||||||
// fetchAll() is the PDO method that gets all result rows
|
// fetchAll() is the PDO method that gets all result rows
|
||||||
return $query->fetchAll();
|
$notes = $query->fetchAll();
|
||||||
|
|
||||||
|
// Add markdown HTML to each note
|
||||||
|
require_once __DIR__ . '/../libs/SimpleMarkdown.php';
|
||||||
|
foreach ($notes as &$note) {
|
||||||
|
$note->note_html = SimpleMarkdown::parse($note->note_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $notes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,13 +37,21 @@ class NoteModel
|
|||||||
*/
|
*/
|
||||||
public static function getNote($note_id)
|
public static function getNote($note_id)
|
||||||
{
|
{
|
||||||
$database = DatabaseFactory::getFactory()->getConnectionWithMySQLI();
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
$sql = "SELECT user_id, note_id, note_text FROM notes WHERE user_id = :user_id AND note_id = :note_id LIMIT 1";
|
$sql = "SELECT note_id, note_text, note_timestamp FROM notes WHERE user_id = :user_id AND note_id = :note_id LIMIT 1";
|
||||||
$query = $database->prepare($sql);
|
$query = $database->prepare($sql);
|
||||||
$query->execute(array(':user_id' => Session::get('user_id'), ':note_id' => $note_id));
|
$query->execute(array(':user_id' => Session::get('user_id'), ':note_id' => $note_id));
|
||||||
|
|
||||||
return $query;
|
$result = $query->fetch();
|
||||||
|
|
||||||
|
// Add markdown HTML
|
||||||
|
if ($result) {
|
||||||
|
require_once __DIR__ . '/../libs/SimpleMarkdown.php';
|
||||||
|
$result->note_html = SimpleMarkdown::parse($result->note_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result ? $result : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -15,15 +15,13 @@ class RegistrationModel
|
|||||||
*/
|
*/
|
||||||
public static function registerNewUser($isAdmin = false)
|
public static function registerNewUser($isAdmin = false)
|
||||||
{
|
{
|
||||||
// clean the input
|
|
||||||
$user_name = strip_tags(Request::post('user_name'));
|
$user_name = strip_tags(Request::post('user_name'));
|
||||||
$user_email = strip_tags(Request::post('user_email'));
|
$user_email = strip_tags(Request::post('user_email'));
|
||||||
// Use 'user_password' if provided (admin registration), otherwise 'user_password_new'
|
$user_password_new = Request::post('user_password_new');
|
||||||
$user_password_new = $isAdmin ? Request::post('user_password_new') : Request::post('user_password_new');
|
$user_password_repeat = $user_password_new;
|
||||||
$user_password_repeat = $user_password_new; // no repeat field
|
|
||||||
|
|
||||||
// validate using existing validators and messages
|
|
||||||
$valid = true;
|
$valid = true;
|
||||||
|
if (!self::validateRecaptcha()) { $valid = false; }
|
||||||
if (!self::validateUserName($user_name)) { $valid = false; }
|
if (!self::validateUserName($user_name)) { $valid = false; }
|
||||||
if (!self::validateUserEmail($user_email, $user_email)) { $valid = false; }
|
if (!self::validateUserEmail($user_email, $user_email)) { $valid = false; }
|
||||||
if (!self::validateUserPassword($user_password_new, $user_password_repeat)) { $valid = false; }
|
if (!self::validateUserPassword($user_password_new, $user_password_repeat)) { $valid = false; }
|
||||||
@@ -77,12 +75,35 @@ class RegistrationModel
|
|||||||
return $return;
|
return $return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static function validateRecaptcha()
|
||||||
* Validates the username
|
{
|
||||||
*
|
$recaptcha_response = Request::post('g-recaptcha-response');
|
||||||
* @param $user_name
|
|
||||||
* @return bool
|
if (empty($recaptcha_response)) {
|
||||||
*/
|
Session::add('feedback_negative', 'reCAPTCHA verification failed. Please try again.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$secret_key = Config::get('RECAPTCHA_SECRET_KEY');
|
||||||
|
$verify_url = 'https://www.google.com/recaptcha/api/siteverify';
|
||||||
|
|
||||||
|
$response = file_get_contents($verify_url . '?secret=' . $secret_key . '&response=' . $recaptcha_response);
|
||||||
|
$response_data = json_decode($response);
|
||||||
|
|
||||||
|
if (!$response_data->success) {
|
||||||
|
Session::add('feedback_negative', 'reCAPTCHA verification failed. Please try again.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// v3 returns a score from 0.0 to 1.0 (1.0 = likely human, 0.0 = likely bot)
|
||||||
|
if (isset($response_data->score) && $response_data->score < 0.5) {
|
||||||
|
Session::add('feedback_negative', 'Registration blocked due to suspicious activity.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public static function validateUserName($user_name)
|
public static function validateUserName($user_name)
|
||||||
{
|
{
|
||||||
if (empty($user_name)) {
|
if (empty($user_name)) {
|
||||||
@@ -90,7 +111,6 @@ class RegistrationModel
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if username is too short (2), too long (64) or does not fit the pattern (aZ09)
|
|
||||||
if (!preg_match('/^[a-zA-Z0-9]{2,64}$/', $user_name)) {
|
if (!preg_match('/^[a-zA-Z0-9]{2,64}$/', $user_name)) {
|
||||||
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_DOES_NOT_FIT_PATTERN'));
|
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_DOES_NOT_FIT_PATTERN'));
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
289
application/model/SqlModel.php
Normal file
289
application/model/SqlModel.php
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class SqlModel
|
||||||
|
*
|
||||||
|
* Model for executing raw SQL queries
|
||||||
|
*/
|
||||||
|
class SqlModel
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Execute a SQL query and return the result
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $sql_query
|
||||||
|
* @param int $user_id
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function executeQuery($database_name, $sql_query, $user_id = null)
|
||||||
|
{
|
||||||
|
$start_time = microtime(true);
|
||||||
|
|
||||||
|
// Save query to history if user_id is provided
|
||||||
|
if ($user_id) {
|
||||||
|
self::saveQueryToHistory($user_id, $database_name, $sql_query);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
// Determine query type
|
||||||
|
$query_type = self::getQueryType($sql_query);
|
||||||
|
|
||||||
|
// Execute the query
|
||||||
|
$query = $database->prepare($sql_query);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
$execution_time = number_format((microtime(true) - $start_time) * 1000, 2);
|
||||||
|
|
||||||
|
// Handle different query types
|
||||||
|
if ($query_type === 'SELECT' || $query_type === 'SHOW' || $query_type === 'DESCRIBE' || $query_type === 'EXPLAIN') {
|
||||||
|
// Return result set
|
||||||
|
$result = $query->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
$message = 'Query executed successfully. ' . count($result) . ' rows returned.';
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'success' => true,
|
||||||
|
'message' => $message,
|
||||||
|
'result' => $result,
|
||||||
|
'query_type' => $query_type,
|
||||||
|
'execution_time' => $execution_time,
|
||||||
|
'affected_rows' => 0
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Return affected rows count
|
||||||
|
$affected_rows = $query->rowCount();
|
||||||
|
$message = 'Query executed successfully. ' . $affected_rows . ' row(s) affected.';
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'success' => true,
|
||||||
|
'message' => $message,
|
||||||
|
'query_type' => $query_type,
|
||||||
|
'execution_time' => $execution_time,
|
||||||
|
'affected_rows' => $affected_rows,
|
||||||
|
'result' => array()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return array(
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Query execution failed',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'query_type' => 'ERROR',
|
||||||
|
'execution_time' => 0,
|
||||||
|
'result' => array()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save query to history
|
||||||
|
* @param int $user_id
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $sql_query
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private static function saveQueryToHistory($user_id, $database_name, $sql_query)
|
||||||
|
{
|
||||||
|
// Create history table if it doesn't exist
|
||||||
|
self::createHistoryTableIfNotExists();
|
||||||
|
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "INSERT INTO sql_query_history (user_id, database_name, query_text, query_timestamp)
|
||||||
|
VALUES (:user_id, :database_name, :query_text, NOW())";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute(array(
|
||||||
|
':user_id' => $user_id,
|
||||||
|
':database_name' => $database_name,
|
||||||
|
':query_text' => $sql_query
|
||||||
|
));
|
||||||
|
|
||||||
|
// Keep only last 50 queries per user
|
||||||
|
$sql = "DELETE FROM sql_query_history
|
||||||
|
WHERE user_id = :user_id
|
||||||
|
AND id NOT IN (
|
||||||
|
SELECT id FROM (
|
||||||
|
SELECT id FROM sql_query_history
|
||||||
|
WHERE user_id = :user_id
|
||||||
|
ORDER BY query_timestamp DESC
|
||||||
|
LIMIT 50
|
||||||
|
) AS temp
|
||||||
|
)";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute(array(':user_id' => $user_id));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create SQL query history table if it doesn't exist
|
||||||
|
*/
|
||||||
|
private static function createHistoryTableIfNotExists()
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "CREATE TABLE IF NOT EXISTS sql_query_history (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
database_name VARCHAR(64) NOT NULL,
|
||||||
|
query_text TEXT NOT NULL,
|
||||||
|
query_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_user_timestamp (user_id, query_timestamp)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
|
||||||
|
$database->exec($sql);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// Table creation failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get query history for a user
|
||||||
|
* @param int $user_id
|
||||||
|
* @param int $limit
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getQueryHistory($user_id, $limit = 50)
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "SELECT database_name, query_text, query_timestamp
|
||||||
|
FROM sql_query_history
|
||||||
|
WHERE user_id = :user_id
|
||||||
|
ORDER BY query_timestamp DESC
|
||||||
|
LIMIT :limit";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->bindValue(':user_id', $user_id, PDO::PARAM_INT);
|
||||||
|
$query->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
return $query->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear query history for a user
|
||||||
|
* @param int $user_id
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function clearQueryHistory($user_id)
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "DELETE FROM sql_query_history WHERE user_id = :user_id";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
return $query->execute(array(':user_id' => $user_id));
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get database schema for autocomplete
|
||||||
|
* @param string $database_name
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getDatabaseSchema($database_name)
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
$schema = array();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all tables
|
||||||
|
$sql = "SHOW TABLES FROM " . $database_name;
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute();
|
||||||
|
$tables = $query->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
// Get columns for each table
|
||||||
|
$sql = "SHOW COLUMNS FROM " . $database_name . "." . $table;
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute();
|
||||||
|
$columns = $query->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$schema[$table] = array_map(function($column) {
|
||||||
|
return $column['Field'];
|
||||||
|
}, $columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $schema;
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format SQL query (basic formatting)
|
||||||
|
* @param string $query
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function formatQuery($query)
|
||||||
|
{
|
||||||
|
// Basic SQL formatting
|
||||||
|
$query = trim($query);
|
||||||
|
|
||||||
|
// Uppercase SQL keywords
|
||||||
|
$keywords = array(
|
||||||
|
'SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'ORDER BY', 'GROUP BY',
|
||||||
|
'HAVING', 'LIMIT', 'OFFSET', 'INSERT', 'INTO', 'VALUES',
|
||||||
|
'UPDATE', 'SET', 'DELETE', 'CREATE', 'TABLE', 'ALTER', 'DROP',
|
||||||
|
'INDEX', 'PRIMARY KEY', 'FOREIGN KEY', 'REFERENCES', 'JOIN',
|
||||||
|
'INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'ON', 'AS', 'DISTINCT',
|
||||||
|
'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'EXISTS', 'IN', 'BETWEEN',
|
||||||
|
'LIKE', 'IS NULL', 'IS NOT NULL', 'UNION', 'ALL'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($keywords as $keyword) {
|
||||||
|
$query = preg_replace('/\b' . preg_quote($keyword) . '\b/i', $keyword, $query);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add line breaks for better readability
|
||||||
|
$query = preg_replace('/\b(FROM|WHERE|GROUP BY|ORDER BY|HAVING|LIMIT|UNION)\b/', "\n$1", $query);
|
||||||
|
$query = preg_replace('/,\s*(\w)/', ",\n $1", $query);
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the type of SQL query
|
||||||
|
* @param string $sql_query
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private static function getQueryType($sql_query)
|
||||||
|
{
|
||||||
|
$query = strtoupper(trim($sql_query));
|
||||||
|
|
||||||
|
if (strpos($query, 'SELECT') === 0) {
|
||||||
|
return 'SELECT';
|
||||||
|
} elseif (strpos($query, 'INSERT') === 0) {
|
||||||
|
return 'INSERT';
|
||||||
|
} elseif (strpos($query, 'UPDATE') === 0) {
|
||||||
|
return 'UPDATE';
|
||||||
|
} elseif (strpos($query, 'DELETE') === 0) {
|
||||||
|
return 'DELETE';
|
||||||
|
} elseif (strpos($query, 'CREATE') === 0) {
|
||||||
|
return 'CREATE';
|
||||||
|
} elseif (strpos($query, 'ALTER') === 0) {
|
||||||
|
return 'ALTER';
|
||||||
|
} elseif (strpos($query, 'DROP') === 0) {
|
||||||
|
return 'DROP';
|
||||||
|
} elseif (strpos($query, 'SHOW') === 0) {
|
||||||
|
return 'SHOW';
|
||||||
|
} elseif (strpos($query, 'DESCRIBE') === 0) {
|
||||||
|
return 'DESCRIBE';
|
||||||
|
} elseif (strpos($query, 'EXPLAIN') === 0) {
|
||||||
|
return 'EXPLAIN';
|
||||||
|
} else {
|
||||||
|
return 'OTHER';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
445
application/model/TableModel.php
Normal file
445
application/model/TableModel.php
Normal file
@@ -0,0 +1,445 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class TableModel
|
||||||
|
*
|
||||||
|
* Model for table operations using PDO
|
||||||
|
*/
|
||||||
|
class TableModel
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get table rows with pagination
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @param int $page
|
||||||
|
* @param int $per_page
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getTableRows($database_name, $table_name, $page = 1, $per_page = 20)
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
$offset = ($page - 1) * $per_page;
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM `" . $database_name . "`.`" . $table_name . "` 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_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total number of rows in a table
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public static function getTableRowCount($database_name, $table_name)
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
$sql = "SELECT COUNT(*) as count FROM `" . $database_name . "`.`" . $table_name . "`";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
$result = $query->fetch(PDO::FETCH_ASSOC);
|
||||||
|
return (int)$result['count'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get table column information
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getTableColumns($database_name, $table_name)
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
$sql = "SHOW COLUMNS FROM `" . $database_name . "`.`" . $table_name . "`";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
return $query->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get table indexes
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getTableIndexes($database_name, $table_name)
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
$sql = "SHOW INDEX FROM `" . $database_name . "`.`" . $table_name . "`";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
return $query->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get table information
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getTableInfo($database_name, $table_name)
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
$sql = "SHOW TABLE STATUS FROM `" . $database_name . "` LIKE :table_name";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute(array(':table_name' => $table_name));
|
||||||
|
|
||||||
|
$result = $query->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
return array(
|
||||||
|
'engine' => $result['Engine'],
|
||||||
|
'rows' => $result['Rows'],
|
||||||
|
'data_size' => self::formatBytes($result['Data_length']),
|
||||||
|
'index_size' => self::formatBytes($result['Index_length']),
|
||||||
|
'total_size' => self::formatBytes($result['Data_length'] + $result['Index_length']),
|
||||||
|
'collation' => $result['Collation'],
|
||||||
|
'comment' => $result['Comment'],
|
||||||
|
'auto_increment' => $result['Auto_increment'],
|
||||||
|
'create_time' => $result['Create_time'],
|
||||||
|
'update_time' => $result['Update_time']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new table
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @param array $columns
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function createTable($database_name, $table_name, $columns)
|
||||||
|
{
|
||||||
|
if (!$database_name || !$table_name || empty($columns)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
$column_definitions = array();
|
||||||
|
foreach ($columns as $column) {
|
||||||
|
$definition = "`" . $column['name'] . "` " . $column['type'];
|
||||||
|
|
||||||
|
if (isset($column['null']) && $column['null'] === 'NO') {
|
||||||
|
$definition .= " NOT NULL";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($column['default']) && $column['default'] !== '') {
|
||||||
|
$definition .= " DEFAULT '" . $column['default'] . "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($column['extra']) && $column['extra'] === 'auto_increment') {
|
||||||
|
$definition .= " AUTO_INCREMENT";
|
||||||
|
}
|
||||||
|
|
||||||
|
$column_definitions[] = $definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($columns as $column) {
|
||||||
|
if (isset($column['key']) && $column['key'] === 'PRI') {
|
||||||
|
$column_definitions[] = "PRIMARY KEY (`" . $column['name'] . "`)";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$columns_sql = implode(', ', $column_definitions);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "CREATE TABLE `" . $database_name . "`.`" . $table_name . "` (" . $columns_sql . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
return $query->execute();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a table
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function deleteTable($database_name, $table_name)
|
||||||
|
{
|
||||||
|
if (!$database_name || !$table_name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "DROP TABLE `" . $database_name . "`.`" . $table_name . "`";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
return $query->execute();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a column to a table
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @param string $column_name
|
||||||
|
* @param string $column_type
|
||||||
|
* @param string $column_null
|
||||||
|
* @param string $column_key
|
||||||
|
* @param string $column_default
|
||||||
|
* @param string $column_extra
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function addColumn($database_name, $table_name, $column_name, $column_type, $column_null, $column_key, $column_default, $column_extra)
|
||||||
|
{
|
||||||
|
if (!$database_name || !$table_name || !$column_name || !$column_type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
$definition = "`" . $column_name . "` " . $column_type;
|
||||||
|
|
||||||
|
if ($column_null === 'NO') {
|
||||||
|
$definition .= " NOT NULL";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($column_default !== '') {
|
||||||
|
$definition .= " DEFAULT '" . $column_default . "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($column_extra === 'auto_increment') {
|
||||||
|
$definition .= " AUTO_INCREMENT";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "ALTER TABLE `" . $database_name . "`.`" . $table_name . "` ADD " . $definition;
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
return $query->execute();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop a column from a table
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @param string $column_name
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function dropColumn($database_name, $table_name, $column_name)
|
||||||
|
{
|
||||||
|
if (!$database_name || !$table_name || !$column_name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "ALTER TABLE `" . $database_name . "`.`" . $table_name . "` DROP COLUMN `" . $column_name . "`";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
return $query->execute();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the primary key column name for a table
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public static function getPrimaryKeyColumn($database_name, $table_name)
|
||||||
|
{
|
||||||
|
$columns = self::getTableColumns($database_name, $table_name);
|
||||||
|
foreach ($columns as $column) {
|
||||||
|
if ($column['Key'] === 'PRI') {
|
||||||
|
return $column['Field'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single row by primary key
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @param mixed $pk_value
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public static function getRow($database_name, $table_name, $pk_value)
|
||||||
|
{
|
||||||
|
$pk_column = self::getPrimaryKeyColumn($database_name, $table_name);
|
||||||
|
if (!$pk_column) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM `" . $database_name . "`.`" . $table_name . "` WHERE `" . $pk_column . "` = :pk_value LIMIT 1";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute(array(':pk_value' => $pk_value));
|
||||||
|
|
||||||
|
return $query->fetch(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a row in the table
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @param mixed $pk_value
|
||||||
|
* @param array $data - associative array of column => value
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function updateRow($database_name, $table_name, $pk_value, $data)
|
||||||
|
{
|
||||||
|
if (!$database_name || !$table_name || !$pk_value || empty($data)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pk_column = self::getPrimaryKeyColumn($database_name, $table_name);
|
||||||
|
if (!$pk_column) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
$set_parts = array();
|
||||||
|
$params = array();
|
||||||
|
$i = 0;
|
||||||
|
foreach ($data as $column => $value) {
|
||||||
|
if ($column === $pk_column) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$param_name = ':param_' . $i;
|
||||||
|
$set_parts[] = "`" . $column . "` = " . $param_name;
|
||||||
|
$params[$param_name] = $value === '' ? null : $value;
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($set_parts)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$params[':pk_value'] = $pk_value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "UPDATE `" . $database_name . "`.`" . $table_name . "` SET " . implode(', ', $set_parts) . " WHERE `" . $pk_column . "` = :pk_value";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
return $query->execute($params);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a row from the table
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @param mixed $pk_value
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function deleteRow($database_name, $table_name, $pk_value)
|
||||||
|
{
|
||||||
|
if (!$database_name || !$table_name || !$pk_value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pk_column = self::getPrimaryKeyColumn($database_name, $table_name);
|
||||||
|
if (!$pk_column) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "DELETE FROM `" . $database_name . "`.`" . $table_name . "` WHERE `" . $pk_column . "` = :pk_value";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
return $query->execute(array(':pk_value' => $pk_value));
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a new row into the table
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @param array $data - associative array of column => value
|
||||||
|
* @return bool|int - returns insert ID on success, false on failure
|
||||||
|
*/
|
||||||
|
public static function insertRow($database_name, $table_name, $data)
|
||||||
|
{
|
||||||
|
if (!$database_name || !$table_name || empty($data)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
$columns = array();
|
||||||
|
$placeholders = array();
|
||||||
|
$params = array();
|
||||||
|
$i = 0;
|
||||||
|
|
||||||
|
foreach ($data as $column => $value) {
|
||||||
|
if ($value === '' || $value === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$columns[] = "`" . $column . "`";
|
||||||
|
$param_name = ':param_' . $i;
|
||||||
|
$placeholders[] = $param_name;
|
||||||
|
$params[$param_name] = $value;
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($columns)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "INSERT INTO `" . $database_name . "`.`" . $table_name . "` (" . implode(', ', $columns) . ") VALUES (" . implode(', ', $placeholders) . ")";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute($params);
|
||||||
|
return $database->lastInsertId();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format bytes to human readable format
|
||||||
|
* @param int $bytes
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private static function formatBytes($bytes)
|
||||||
|
{
|
||||||
|
if ($bytes >= 1073741824) {
|
||||||
|
return number_format($bytes / 1073741824, 2) . ' GB';
|
||||||
|
} elseif ($bytes >= 1048576) {
|
||||||
|
return number_format($bytes / 1048576, 2) . ' MB';
|
||||||
|
} elseif ($bytes >= 1024) {
|
||||||
|
return number_format($bytes / 1024, 2) . ' KB';
|
||||||
|
} elseif ($bytes > 1) {
|
||||||
|
return $bytes . ' bytes';
|
||||||
|
} elseif ($bytes == 1) {
|
||||||
|
return '1 byte';
|
||||||
|
} else {
|
||||||
|
return '0 bytes';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
105
application/view/_templates/dbmanager_footer.php
Normal file
105
application/view/_templates/dbmanager_footer.php
Normal 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>
|
||||||
107
application/view/_templates/dbmanager_header.php
Normal file
107
application/view/_templates/dbmanager_header.php
Normal 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">
|
||||||
@@ -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" ';
|
||||||
} ?> >
|
} ?> >
|
||||||
|
|||||||
86
application/view/database/index.php
Normal file
86
application/view/database/index.php
Normal 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>
|
||||||
76
application/view/database/show.php
Normal file
76
application/view/database/show.php
Normal 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>
|
||||||
68
application/view/dbuser/create.php
Normal file
68
application/view/dbuser/create.php
Normal 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>
|
||||||
92
application/view/dbuser/edit.php
Normal file
92
application/view/dbuser/edit.php
Normal 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>
|
||||||
101
application/view/dbuser/index.php
Normal file
101
application/view/dbuser/index.php
Normal 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'">×</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>
|
||||||
37
application/view/dbuser/privileges.php
Normal file
37
application/view/dbuser/privileges.php
Normal 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>
|
||||||
51
application/view/gallery/edit.php
Normal file
51
application/view/gallery/edit.php
Normal 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>
|
||||||
59
application/view/gallery/index.php
Normal file
59
application/view/gallery/index.php
Normal 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>
|
||||||
76
application/view/gallery/my.php
Normal file
76
application/view/gallery/my.php
Normal 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>
|
||||||
168
application/view/gallery/success.php
Normal file
168
application/view/gallery/success.php
Normal 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>
|
||||||
437
application/view/gallery/upload.php
Normal file
437
application/view/gallery/upload.php
Normal 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">×</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>
|
||||||
80
application/view/gallery/view.php
Normal file
80
application/view/gallery/view.php
Normal 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>
|
||||||
@@ -37,6 +37,9 @@
|
|||||||
<div class="link-forgot-my-password">
|
<div class="link-forgot-my-password">
|
||||||
<a href="<?php echo Config::get('URL'); ?>login/requestPasswordReset">I forgot my password</a>
|
<a href="<?php echo Config::get('URL'); ?>login/requestPasswordReset">I forgot my password</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="link-register" style="margin-top: 15px;">
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>register" class="button">Register new account</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,115 +1,17 @@
|
|||||||
<div class="container">
|
<?php $this->render('_templates/header'); ?>
|
||||||
<div class="row">
|
|
||||||
<!-- Back to Messenger -->
|
|
||||||
<div class="col-md-12">
|
|
||||||
<a href="<?= Config::get('URL') ?>message" class="btn btn-default">
|
|
||||||
<span class="glyphicon glyphicon-arrow-left"></span> Back to Messenger
|
|
||||||
</a>
|
|
||||||
<hr>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="box">
|
||||||
<!-- Chat Area -->
|
<?php $this->renderFeedbackMessages(); ?>
|
||||||
<div class="col-md-12">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
Conversation with <?= htmlspecialchars($this->other_user->user_name) ?>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body message-container">
|
|
||||||
<?php if (empty($this->messages)): ?>
|
|
||||||
<div class="text-center">
|
|
||||||
<em>No messages yet. Start a conversation!</em>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php foreach ($this->messages as $msg): ?>
|
|
||||||
<div class="message <?= $msg->message_type ?>">
|
|
||||||
<div class="message-bubble">
|
|
||||||
<?= htmlspecialchars($msg->message) ?>
|
|
||||||
</div>
|
|
||||||
<div class="message-time">
|
|
||||||
<?= date('M j, Y H:i', strtotime($msg->created_at)) ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Reply Form -->
|
<script>
|
||||||
<div class="panel-footer">
|
// Redirect to main messages page and load conversation
|
||||||
<form action="<?= Config::get('URL') ?>message/send" method="post" id="reply-form">
|
window.location.href = '<?= Config::get('URL') ?>message' + '#load-conversation-<?= isset($this->other_user) ? $this->other_user->user_id : '' ?>';
|
||||||
<input type="hidden" name="receiver_id" value="<?= $this->other_user->user_id ?>">
|
</script>
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" name="message" class="form-control" placeholder="Type your message..." required>
|
<div style="text-align: center; padding: 60px;">
|
||||||
<span class="input-group-btn">
|
<h3>Redirecting to Conversation...</h3>
|
||||||
<button type="submit" class="btn btn-primary">Send</button>
|
<p>If you're not redirected automatically, <a href="<?= Config::get('URL') ?>message">click here</a>.</p>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
.message-container {
|
|
||||||
height: 400px;
|
|
||||||
overflow-y: auto;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.sent {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.received {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-bubble {
|
|
||||||
display: inline-block;
|
|
||||||
max-width: 70%;
|
|
||||||
padding: 10px 15px;
|
|
||||||
border-radius: 18px;
|
|
||||||
position: relative;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.sent .message-bubble {
|
|
||||||
background-color: #007bff;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.received .message-bubble {
|
|
||||||
background-color: #e5e5ea;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-time {
|
|
||||||
font-size: 0.8em;
|
|
||||||
color: #666;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scroll to bottom of messages on load */
|
|
||||||
.message-container {
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Scroll to bottom of messages
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const container = document.querySelector('.message-container');
|
|
||||||
container.scrollTop = container.scrollHeight;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<?php $this->render('_templates/footer'); ?>
|
<?php $this->render('_templates/footer'); ?>
|
||||||
17
application/view/message/global.php
Normal file
17
application/view/message/global.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php $this->render('_templates/header'); ?>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<?php $this->renderFeedbackMessages(); ?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Redirect to main messages page and load global chat
|
||||||
|
window.location.href = '<?= Config::get('URL') ?>message' + '#load-global';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div style="text-align: center; padding: 60px;">
|
||||||
|
<h3>Redirecting to Global Chat...</h3>
|
||||||
|
<p>If you're not redirected automatically, <a href="<?= Config::get('URL') ?>message">click here</a>.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php $this->render('_templates/footer'); ?>
|
||||||
@@ -1,106 +1,83 @@
|
|||||||
<div class="container">
|
<div class="box">
|
||||||
<h1>Messenger</h1>
|
<h1>Messenger</h1>
|
||||||
|
<?php $this->renderFeedbackMessages(); ?>
|
||||||
|
|
||||||
<div class="row">
|
<meta name="user-id" content="<?= Session::get('user_id') ?>">
|
||||||
<!-- Conversations List -->
|
|
||||||
<div class="col-md-4">
|
<script src="<?= Config::get('URL') ?>public/js/messaging.js"></script>
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
<div class="messenger-container">
|
||||||
Conversations
|
<div style="width: 300px;" class="messaging-sidebar">
|
||||||
<?php if ($this->unread_count > 0): ?>
|
<div class="sidebar-section">
|
||||||
<span class="badge pull-right"><?= $this->unread_count ?></span>
|
<h3>Channels</h3>
|
||||||
<?php endif; ?>
|
<div onclick="loadGlobalChat()" class="chat-item" id="global-chat-link">
|
||||||
|
<div class="chat-item-title">
|
||||||
|
<span class="chat-item-icon" style="color: #2196F3;">#</span>
|
||||||
|
<strong>Global Chat</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body" style="padding: 0; max-height: 500px; overflow-y: auto;">
|
<div class="chat-item-meta">Public chatroom</div>
|
||||||
<div class="list-group">
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<h3>Direct Messages</h3>
|
||||||
<?php if (empty($this->conversations)): ?>
|
<?php if (empty($this->conversations)): ?>
|
||||||
<div class="list-group-item">
|
<p class="chat-item" style="text-align: center; cursor: default;">No conversations yet</p>
|
||||||
<em>No conversations yet</em>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php foreach ($this->conversations as $conv): ?>
|
<?php foreach ($this->conversations as $conv): ?>
|
||||||
<a href="<?= Config::get('URL') ?>message/conversation/<?= $conv->other_user_id ?>"
|
<div onclick="loadConversation(<?= $conv->other_user_id ?>, '<?= htmlspecialchars(addslashes($conv->user_name)) ?>')"
|
||||||
class="list-group-item <?= $conv->unread_count > 0 ? 'active' : '' ?>">
|
class="chat-item"
|
||||||
<h5 class="list-group-item-heading">
|
id="conversation-<?= $conv->other_user_id ?>">
|
||||||
<?= htmlspecialchars($conv->user_name) ?>
|
<div class="chat-item-header">
|
||||||
|
<div class="chat-item-title">
|
||||||
|
<span class="chat-item-icon" style="color: #4CAF50;">@</span>
|
||||||
|
<strong><?= htmlspecialchars($conv->user_name) ?></strong>
|
||||||
|
</div>
|
||||||
<?php if ($conv->unread_count > 0): ?>
|
<?php if ($conv->unread_count > 0): ?>
|
||||||
<span class="badge"><?= $conv->unread_count ?></span>
|
<span class="unread-badge"><?= $conv->unread_count ?></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</h5>
|
</div>
|
||||||
<p class="list-group-item-text">
|
<div class="chat-item-meta"><?= date('M j, H:i', strtotime($conv->last_message_time)) ?></div>
|
||||||
<small><?= date('M j, Y', strtotime($conv->last_message_time)) ?></small>
|
</div>
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- New Message -->
|
<div class="sidebar-section">
|
||||||
<div class="panel panel-default">
|
<h3>New Message</h3>
|
||||||
<div class="panel-heading">New Message</div>
|
<form action="<?= Config::get('URL') ?>message/send" method="post" id="message-form" class="new-message-form">
|
||||||
<div class="panel-body">
|
<select name="receiver_id" required>
|
||||||
<form action="<?= Config::get('URL') ?>message/send" method="post">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="receiver_id">To:</label>
|
|
||||||
<select name="receiver_id" id="receiver_id" class="form-control" required>
|
|
||||||
<option value="">Select user</option>
|
<option value="">Select user</option>
|
||||||
<?php foreach ($this->all_users as $user): ?>
|
<?php foreach ($this->all_users as $user): ?>
|
||||||
<option value="<?= $user->user_id ?>"><?= htmlspecialchars($user->user_name) ?></option>
|
<option value="<?= $user->user_id ?>"><?= htmlspecialchars($user->user_name) ?></option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
<input type="hidden" name="subject" value="Direct Message">
|
||||||
<div class="form-group">
|
<textarea name="message" placeholder="Type your message..." required></textarea>
|
||||||
<label for="subject">Subject:</label>
|
<button type="submit" class="button" style="width: 100%;">Send Message</button>
|
||||||
<input type="text" name="subject" id="subject" class="form-control" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="message">Message:</label>
|
|
||||||
<textarea name="message" id="message" class="form-control" rows="3" required></textarea>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">Send Message</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Send to Group -->
|
<div style="flex: 1;" class="chat-main-area">
|
||||||
<div class="panel panel-default">
|
<div id="chat-content">
|
||||||
<div class="panel-heading">Send to Group</div>
|
<div id="default-state">
|
||||||
<div class="panel-body">
|
<div class="default-welcome">
|
||||||
<form action="<?= Config::get('URL') ?>message/sendgroup" method="post">
|
<h3>Welcome to Messenger</h3>
|
||||||
<div class="form-group">
|
<p>Select Global Chat or a conversation to start messaging</p>
|
||||||
<label for="group_type">Group:</label>
|
<small>Click on any channel or conversation in the sidebar</small>
|
||||||
<select name="group_type" id="group_type" class="form-control" required>
|
|
||||||
<option value="">Select group</option>
|
|
||||||
<option value="all_users">All Users</option>
|
|
||||||
<option value="admins">Administrators</option>
|
|
||||||
<option value="moderators">Moderators</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="group_subject">Subject:</label>
|
|
||||||
<input type="text" name="subject" id="group_subject" class="form-control" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="group_message">Message:</label>
|
|
||||||
<textarea name="message" id="group_message" class="form-control" rows="3" required></textarea>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-warning">Send to Group</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chat Area -->
|
<div id="chat-interface">
|
||||||
<div class="col-md-8">
|
<div id="chat-header">
|
||||||
<div class="panel panel-default">
|
<div id="chat-title"></div>
|
||||||
<div class="panel-heading">
|
<div id="chat-actions"></div>
|
||||||
Chat Area
|
|
||||||
<span class="pull-right">Select a conversation to start messaging</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body" style="height: 500px; background-color: #f5f5f5; text-align: center; padding-top: 200px;">
|
|
||||||
<em>Select a conversation from the left to view messages</em>
|
<div id="messages-container"></div>
|
||||||
|
|
||||||
|
<div id="message-input-area"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,68 +85,773 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.message-container {
|
.messenger-container {
|
||||||
height: 400px;
|
display: flex;
|
||||||
overflow-y: auto;
|
gap: 20px;
|
||||||
background-color: #f5f5f5;
|
margin-top: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messaging-sidebar {
|
||||||
|
width: 300px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
border-radius: 5px;
|
background: linear-gradient(135deg, #f8f9fa 0%, #f0f2f5 100%);
|
||||||
margin-bottom: 15px;
|
border: 1px solid #e0e0e0;
|
||||||
}
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.message {
|
.sidebar-section h3 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-main-area {
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat-content {
|
||||||
|
height: 600px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#default-state {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(180deg, #f5f7fa 0%, #fafbfc 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat-interface {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat-header {
|
||||||
|
padding: 15px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #fff 100%);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#message-input-area {
|
||||||
|
padding: 15px;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
background: linear-gradient(180deg, #fff 0%, #f8f9fa 100%);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#messages-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(180deg, #f5f7fa 0%, #fafbfc 100%);
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-item {
|
||||||
|
display: block;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #333;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-item:hover {
|
||||||
|
background: #f5f8ff;
|
||||||
|
border-color: #2196F3;
|
||||||
|
transform: translateX(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-item.active {
|
||||||
|
background: #e3f2fd;
|
||||||
|
border-color: #2196F3;
|
||||||
|
box-shadow: 0 2px 4px rgba(33,150,243,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-item-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-item-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-item-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unread-badge {
|
||||||
|
background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-item-meta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 70%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 18px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
line-height: 1.5;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble.sent {
|
||||||
|
background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%);
|
||||||
|
color: white;
|
||||||
|
border-bottom-right-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble.received {
|
||||||
|
background: white;
|
||||||
|
color: #333;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-bottom-left-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-row {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-row.sent {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-row.received {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-message {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||||||
|
border: 1px solid #eee;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-message:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-message-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
clear: both;
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-message-sender {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-message-bubble {
|
||||||
|
color: #444;
|
||||||
|
line-height: 1.6;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 18px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #2196F3;
|
||||||
|
box-shadow: 0 0 0 3px rgba(33,150,243,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-button {
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-button:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-welcome {
|
||||||
|
text-align: center;
|
||||||
|
padding: 80px 20px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-welcome h3 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-welcome p {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-welcome small {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-message-form select,
|
||||||
|
.new-message-form textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-message-form select:focus,
|
||||||
|
.new-message-form textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #2196F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-message-form textarea {
|
||||||
|
height: 80px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.messenger-container {
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message.sent {
|
.messaging-sidebar {
|
||||||
text-align: right;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message.received {
|
#chat-content {
|
||||||
text-align: left;
|
height: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble {
|
.message-bubble {
|
||||||
display: inline-block;
|
max-width: 85%;
|
||||||
max-width: 70%;
|
|
||||||
padding: 10px 15px;
|
|
||||||
border-radius: 18px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.sent .message-bubble {
|
|
||||||
background-color: #007bff;
|
|
||||||
color: white;
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.received .message-bubble {
|
|
||||||
background-color: #e5e5ea;
|
|
||||||
color: black;
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-time {
|
|
||||||
font-size: 0.8em;
|
|
||||||
color: #666;
|
|
||||||
margin-top: 5px;
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
background-color: #d9534f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item.active {
|
|
||||||
background-color: #d9edf7;
|
|
||||||
border-color: #bce8f1;
|
|
||||||
color: #31708f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item.active .badge {
|
|
||||||
background-color: #d9534f;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentChatType = null;
|
||||||
|
let currentChatId = null;
|
||||||
|
let messagePollingInterval = null;
|
||||||
|
let isFirstLoad = true;
|
||||||
|
|
||||||
|
function scrollToBottom() {
|
||||||
|
const container = document.getElementById('messages-container');
|
||||||
|
if (container) {
|
||||||
|
setTimeout(() => {
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const hash = window.location.hash.substring(1);
|
||||||
|
|
||||||
|
if (hash === 'load-global') {
|
||||||
|
setTimeout(() => loadGlobalChat(), 100);
|
||||||
|
} else if (hash.startsWith('load-conversation-')) {
|
||||||
|
const userId = hash.split('-')[2];
|
||||||
|
if (userId) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const conversationEl = document.querySelector('#conversation-' + userId);
|
||||||
|
if (conversationEl) {
|
||||||
|
const userName = conversationEl.querySelector('strong').textContent;
|
||||||
|
loadConversation(userId, userName);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadGlobalChat() {
|
||||||
|
currentChatType = 'global';
|
||||||
|
currentChatId = 'global';
|
||||||
|
isFirstLoad = true;
|
||||||
|
|
||||||
|
document.getElementById('default-state').style.display = 'none';
|
||||||
|
document.getElementById('chat-interface').style.display = 'flex';
|
||||||
|
|
||||||
|
document.getElementById('chat-title').innerHTML = `
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<span style="color: #2196F3; font-size: 18px;">#</span>
|
||||||
|
<strong>Global Chat</strong>
|
||||||
|
</div>
|
||||||
|
<small style="color: #666;">Public chatroom</small>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById('chat-actions').innerHTML = `
|
||||||
|
<span style="font-size: 12px; color: #666;">
|
||||||
|
<i class="fa fa-users"></i> Public
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
highlightActiveChat('global');
|
||||||
|
loadGlobalMessages();
|
||||||
|
setTimeout(scrollToBottom, 300);
|
||||||
|
setupGlobalMessageInput();
|
||||||
|
startMessagePolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadConversation(userId, userName) {
|
||||||
|
currentChatType = 'conversation';
|
||||||
|
currentChatId = userId;
|
||||||
|
isFirstLoad = true;
|
||||||
|
|
||||||
|
document.getElementById('default-state').style.display = 'none';
|
||||||
|
document.getElementById('chat-interface').style.display = 'flex';
|
||||||
|
|
||||||
|
document.getElementById('chat-title').innerHTML = `
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<span style="color: #4CAF50; font-size: 18px;">@</span>
|
||||||
|
<strong>${userName}</strong>
|
||||||
|
</div>
|
||||||
|
<small style="color: #666;">Direct message</small>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById('chat-actions').innerHTML = `
|
||||||
|
<span style="font-size: 12px; color: #666;">
|
||||||
|
<i class="fa fa-user"></i> Private
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
highlightActiveChat(userId);
|
||||||
|
loadConversationMessages(userId);
|
||||||
|
setTimeout(scrollToBottom, 300);
|
||||||
|
setupConversationMessageInput(userId);
|
||||||
|
startMessagePolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightActiveChat(chatId) {
|
||||||
|
document.querySelectorAll('.chat-item').forEach(el => el.classList.remove('active'));
|
||||||
|
|
||||||
|
const activeEl = chatId === 'global' ?
|
||||||
|
document.getElementById('global-chat-link') :
|
||||||
|
document.getElementById('conversation-' + chatId);
|
||||||
|
|
||||||
|
if (activeEl) activeEl.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadGlobalMessages(silent = false) {
|
||||||
|
const container = document.getElementById('messages-container');
|
||||||
|
|
||||||
|
if (isFirstLoad && !silent) {
|
||||||
|
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #666;">Loading messages...</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('<?= Config::get('URL') ?>message/getGlobalMessages')
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) throw new Error('HTTP error: ' + response.status);
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.success && data.messages) {
|
||||||
|
const wasAtBottom = container.scrollHeight - container.scrollTop <= container.clientHeight + 50;
|
||||||
|
displayGlobalMessages(data.messages);
|
||||||
|
if (wasAtBottom) scrollToBottom();
|
||||||
|
isFirstLoad = false;
|
||||||
|
} else if (isFirstLoad) {
|
||||||
|
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #f44336;">Error loading messages</div>';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading global messages:', error);
|
||||||
|
if (isFirstLoad) {
|
||||||
|
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #f44336;">Network error</div>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadConversationMessages(userId, silent = false) {
|
||||||
|
const container = document.getElementById('messages-container');
|
||||||
|
|
||||||
|
if (isFirstLoad && !silent) {
|
||||||
|
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #666;">Loading messages...</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`<?= Config::get('URL') ?>message/getConversationMessages/${userId}`)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) throw new Error('HTTP error: ' + response.status);
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.success && data.messages) {
|
||||||
|
const wasAtBottom = container.scrollHeight - container.scrollTop <= container.clientHeight + 50;
|
||||||
|
displayConversationMessages(data.messages);
|
||||||
|
if (wasAtBottom) scrollToBottom();
|
||||||
|
isFirstLoad = false;
|
||||||
|
} else if (isFirstLoad) {
|
||||||
|
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #f44336;">Error loading messages</div>';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading conversation messages:', error);
|
||||||
|
if (isFirstLoad) {
|
||||||
|
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #f44336;">Network error</div>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayGlobalMessages(messages) {
|
||||||
|
const container = document.getElementById('messages-container');
|
||||||
|
|
||||||
|
if (messages.length === 0) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="default-welcome">
|
||||||
|
<h3>No messages yet</h3>
|
||||||
|
<p>Start the conversation!</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = messages.map(message => {
|
||||||
|
const timeString = new Date(message.created_at).toLocaleString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
|
||||||
|
const isOwn = message.sender_id == document.querySelector('meta[name="user-id"]').content;
|
||||||
|
const initial = message.sender_name.charAt(0).toUpperCase();
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="global-message">
|
||||||
|
<div class="global-message-header">
|
||||||
|
<div class="global-message-sender">
|
||||||
|
<div class="sender-avatar" style="${isOwn ? 'background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%);' : ''}">${initial}</div>
|
||||||
|
<strong style="color: ${isOwn ? '#2196F3' : '#333'}">
|
||||||
|
${escapeHtml(message.sender_name)}
|
||||||
|
</strong>
|
||||||
|
${isOwn ? '<span style="font-size: 10px; background: #2196F3; color: white; padding: 2px 6px; border-radius: 4px;">You</span>' : ''}
|
||||||
|
</div>
|
||||||
|
<span class="message-time">${timeString}</span>
|
||||||
|
</div>
|
||||||
|
<div class="global-message-bubble">
|
||||||
|
${escapeHtml(message.message)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayConversationMessages(messages) {
|
||||||
|
const container = document.getElementById('messages-container');
|
||||||
|
const currentUserId = document.querySelector('meta[name="user-id"]').content;
|
||||||
|
|
||||||
|
if (messages.length === 0) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="default-welcome">
|
||||||
|
<h3>No messages yet</h3>
|
||||||
|
<p>Send a message to start the conversation!</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = messages.map(message => {
|
||||||
|
const isSent = message.sender_id == currentUserId;
|
||||||
|
const timeString = new Date(message.created_at).toLocaleString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="message-row ${isSent ? 'sent' : 'received'}">
|
||||||
|
<div>
|
||||||
|
<div class="message-bubble ${isSent ? 'sent' : 'received'}">
|
||||||
|
${escapeHtml(message.message)}
|
||||||
|
</div>
|
||||||
|
<div class="message-time" style="text-align: ${isSent ? 'right' : 'left'}">
|
||||||
|
${timeString}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupGlobalMessageInput() {
|
||||||
|
const inputArea = document.getElementById('message-input-area');
|
||||||
|
inputArea.innerHTML = `
|
||||||
|
<form id="global-message-form" onsubmit="sendGlobalMessage(event)">
|
||||||
|
<div style="display: flex; gap: 10px;">
|
||||||
|
<input type="text"
|
||||||
|
name="message"
|
||||||
|
placeholder="Type your global message..."
|
||||||
|
class="message-input"
|
||||||
|
required>
|
||||||
|
<button type="submit" class="button send-button">Send</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupConversationMessageInput(userId) {
|
||||||
|
const inputArea = document.getElementById('message-input-area');
|
||||||
|
inputArea.innerHTML = `
|
||||||
|
<form id="conversation-message-form" onsubmit="sendConversationMessage(event, ${userId})">
|
||||||
|
<div style="display: flex; gap: 10px;">
|
||||||
|
<input type="text"
|
||||||
|
name="message"
|
||||||
|
placeholder="Type your message..."
|
||||||
|
class="message-input"
|
||||||
|
required>
|
||||||
|
<button type="submit" class="button send-button">Send</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendGlobalMessage(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const form = event.target;
|
||||||
|
const message = form.message.value;
|
||||||
|
const submitButton = form.querySelector('button[type="submit"]');
|
||||||
|
const originalText = submitButton.textContent;
|
||||||
|
|
||||||
|
submitButton.textContent = 'Sending...';
|
||||||
|
submitButton.disabled = true;
|
||||||
|
|
||||||
|
fetch('<?= Config::get('URL') ?>message/sendToGlobal', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
},
|
||||||
|
body: 'message=' + encodeURIComponent(message)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) throw new Error('HTTP error: ' + response.status);
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
form.reset();
|
||||||
|
loadGlobalMessages(true);
|
||||||
|
setTimeout(scrollToBottom, 300);
|
||||||
|
} else {
|
||||||
|
alert('Failed to send message: ' + (data.message || 'Unknown error'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('Network error. Please try again.');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
submitButton.textContent = originalText;
|
||||||
|
submitButton.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendConversationMessage(event, userId) {
|
||||||
|
event.preventDefault();
|
||||||
|
const form = event.target;
|
||||||
|
const message = form.message.value;
|
||||||
|
const submitButton = form.querySelector('button[type="submit"]');
|
||||||
|
const originalText = submitButton.textContent;
|
||||||
|
|
||||||
|
submitButton.textContent = 'Sending...';
|
||||||
|
submitButton.disabled = true;
|
||||||
|
|
||||||
|
fetch('<?= Config::get('URL') ?>message/reply', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
},
|
||||||
|
body: 'receiver_id=' + userId + '&message=' + encodeURIComponent(message)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) throw new Error('HTTP error: ' + response.status);
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
form.reset();
|
||||||
|
loadConversationMessages(userId, true);
|
||||||
|
setTimeout(scrollToBottom, 300);
|
||||||
|
} else {
|
||||||
|
alert('Failed to send message: ' + (data.message || 'Unknown error'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('Network error. Please try again.');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
submitButton.textContent = originalText;
|
||||||
|
submitButton.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startMessagePolling() {
|
||||||
|
if (messagePollingInterval) clearInterval(messagePollingInterval);
|
||||||
|
|
||||||
|
messagePollingInterval = setInterval(() => {
|
||||||
|
if (currentChatType === 'global') {
|
||||||
|
loadGlobalMessages(true);
|
||||||
|
} else if (currentChatType === 'conversation') {
|
||||||
|
loadConversationMessages(currentChatId, true);
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('message-form').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
const submitButton = this.querySelector('button[type="submit"]');
|
||||||
|
const originalText = submitButton.textContent;
|
||||||
|
|
||||||
|
submitButton.textContent = 'Sending...';
|
||||||
|
submitButton.disabled = true;
|
||||||
|
|
||||||
|
fetch(this.action, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
this.reset();
|
||||||
|
showFeedback('Message sent successfully!', 'success');
|
||||||
|
} else {
|
||||||
|
showFeedback(data.message || 'Failed to send message', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showFeedback('Network error. Please try again.', 'error');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
submitButton.textContent = originalText;
|
||||||
|
submitButton.disabled = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function showFeedback(message, type) {
|
||||||
|
const feedback = document.createElement('div');
|
||||||
|
feedback.textContent = message;
|
||||||
|
feedback.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: ${type === 'success' ? '#4caf50' : '#f44336'};
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(feedback);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
feedback.style.animation = 'slideOut 0.3s ease';
|
||||||
|
setTimeout(() => feedback.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const animationStyle = document.createElement('style');
|
||||||
|
animationStyle.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(animationStyle);
|
||||||
|
</script>
|
||||||
|
|
||||||
<?php $this->render('_templates/footer'); ?>
|
<?php $this->render('_templates/footer'); ?>
|
||||||
|
|||||||
@@ -1,44 +1,459 @@
|
|||||||
<div class="container">
|
<div class="box">
|
||||||
<h1>NoteController/index</h1>
|
<h1>My Notes</h1>
|
||||||
<div class="box">
|
|
||||||
|
|
||||||
<!-- echo out the system feedback (error and success messages) -->
|
|
||||||
<?php $this->renderFeedbackMessages(); ?>
|
<?php $this->renderFeedbackMessages(); ?>
|
||||||
|
|
||||||
<h3>What happens here ?</h3>
|
<div style="display: flex; gap: 20px; margin-top: 20px;">
|
||||||
<p>
|
<!-- Notes Sidebar -->
|
||||||
This is just a simple CRUD implementation. Creating, reading, updating and deleting things.
|
<div style="width: 300px;" class="notes-sidebar">
|
||||||
</p>
|
<div style="padding: 15px; background: #f5f5f5; border: 1px solid #ddd; margin-bottom: 20px;">
|
||||||
<p>
|
<form action="<?= Config::get('URL') ?>note/create" method="post" id="create-note-form" style="margin-bottom: 15px;">
|
||||||
<form method="post" action="<?php echo Config::get('URL');?>note/create">
|
<textarea name="note_text" placeholder="Write your note here..." style="width: 100%; max-width: 250px; padding: 8px; border: 1px solid #ddd; height: 80px; resize: vertical; margin-bottom: 10px;" required></textarea>
|
||||||
<label>Text of new note: </label><input type="text" name="note_text" />
|
<button type="submit" class="button">Create Note</button>
|
||||||
<input type="submit" value='Create this note' autocomplete="off" />
|
|
||||||
</form>
|
</form>
|
||||||
</p>
|
</div>
|
||||||
|
|
||||||
<?php if ($this->notes) { ?>
|
<div style="padding: 15px; background: #f5f5f5; border: 1px solid #ddd;">
|
||||||
<table class="note-table">
|
<h3 style="margin: 0 0 15px 0;">Your Notes</h3>
|
||||||
<thead>
|
<?php if ($this->notes): ?>
|
||||||
<tr>
|
<?php foreach ($this->notes as $note): ?>
|
||||||
<td>Id</td>
|
<div onclick="viewNote(<?= $note->note_id ?>)" style="padding: 12px; background: white; border: 1px solid #ddd; margin-bottom: 8px; cursor: pointer; transition: background-color 0.2s;" onmouseover="this.style.backgroundColor='#f0f0f0'" onmouseout="this.style.backgroundColor='white'">
|
||||||
<td>Note</td>
|
<div style="font-weight: bold; margin-bottom: 5px; word-wrap: break-word;">
|
||||||
<td>EDIT</td>
|
<?= htmlspecialchars(substr($note->note_text, 0, 50)) ?><?= strlen($note->note_text) > 50 ? '...' : '' ?>
|
||||||
<td>DELETE</td>
|
</div>
|
||||||
</tr>
|
<div style="font-size: 12px; color: #666; margin-bottom: 8px;">
|
||||||
</thead>
|
<?= date('M j, Y H:i', strtotime($note->note_timestamp)) ?>
|
||||||
<tbody>
|
</div>
|
||||||
<?php foreach($this->notes as $key => $value) { ?>
|
<div style="font-size: 11px; color: #999; font-style: italic; word-wrap: break-word;">
|
||||||
<tr>
|
<?= htmlspecialchars(substr(strip_tags($note->note_text), 0, 80)) ?><?= strlen(strip_tags($note->note_text)) > 80 ? '...' : '' ?>
|
||||||
<td><?= $value->note_id; ?></td>
|
</div>
|
||||||
<td><?= htmlentities($value->note_text); ?></td>
|
<div style="display: flex; gap: 8px; margin-top: 8px;" onclick="event.stopPropagation()">
|
||||||
<td><a href="<?= Config::get('URL') . 'note/edit/' . $value->note_id; ?>">Edit</a></td>
|
<a onclick="editNote(<?= $note->note_id ?>)" style="font-size: 12px; padding: 4px 8px; background: #4CAF50; color: white; text-decoration: none; border-radius: 3px;">Edit</a>
|
||||||
<td><a href="<?= Config::get('URL') . 'note/delete/' . $value->note_id; ?>">Delete</a></td>
|
<a href="<?= Config::get('URL') ?>note/delete/<?= $note->note_id ?>" style="font-size: 12px; padding: 4px 8px; background: #f44336; color: white; text-decoration: none; border-radius: 3px;" onclick="return confirm('Are you sure you want to delete this note?');">Delete</a>
|
||||||
</tr>
|
</div>
|
||||||
<?php } ?>
|
</div>
|
||||||
</tbody>
|
<?php endforeach; ?>
|
||||||
</table>
|
<?php else: ?>
|
||||||
<?php } else { ?>
|
<p style="text-align: center; padding: 20px; background: white; border: 1px solid #ddd;">No notes yet. Create your first note!</p>
|
||||||
<div>No notes yet. Create some !</div>
|
<?php endif; ?>
|
||||||
<?php } ?>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notes Main Content -->
|
||||||
|
<div style="flex: 1; border: 1px solid #ddd; background: white;" class="note-main-content">
|
||||||
|
<div id="note-content" style="padding: 20px;">
|
||||||
|
<div style="text-align: center; padding: 60px 20px; color: #666; background: #f9f9f9;">
|
||||||
|
<h3>Note Viewer</h3>
|
||||||
|
<p>Select a note from the left to view its contents</p>
|
||||||
|
<small style="color: #999;">Click anywhere on a note card to view it</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Note Form (Hidden by default) -->
|
||||||
|
<div id="note-edit-form" style="display: none; padding: 20px;">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #ddd;">
|
||||||
|
<h3 style="margin: 0;">Editing Note</h3>
|
||||||
|
<div style="display: flex; gap: 8px;">
|
||||||
|
<button onclick="cancelEdit()" class="button" style="background: #757575; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer;">Cancel</button>
|
||||||
|
<button onclick="saveEdit()" class="button" style="background: #4CAF50; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer;">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 15px;">
|
||||||
|
<label style="display: block; margin-bottom: 8px; font-weight: bold; color: #333;">
|
||||||
|
Note Content (Markdown supported):
|
||||||
|
</label>
|
||||||
|
<textarea id="edit-note-text" style="width: 95%; padding: 12px; border: 1px solid #ddd; border-radius: 4px; font-family: 'Courier New', monospace; font-size: 14px; line-height: 1.5; resize: vertical; min-height: 300px;" placeholder="Write your note here...
|
||||||
|
|
||||||
|
Markdown syntax supported:
|
||||||
|
# Header
|
||||||
|
**Bold** *italic* `code`
|
||||||
|
- List item
|
||||||
|
[Link](url)
|
||||||
|
> Quote"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 20px; padding: 15px; background: #f5f5f5; border-radius: 4px;">
|
||||||
|
<h4 style="margin: 0 0 10px 0; color: #333;">Preview:</h4>
|
||||||
|
<div id="edit-preview" style="padding: 15px; background: white; border: 1px solid #ddd; border-radius: 4px; min-height: 100px; font-size: 14px; line-height: 1.6;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentNoteId = null;
|
||||||
|
let isEditMode = false;
|
||||||
|
|
||||||
|
function viewNote(noteId) {
|
||||||
|
if (currentNoteId === noteId && !isEditMode) return; // Don't reload if same note and not in edit mode
|
||||||
|
|
||||||
|
currentNoteId = noteId;
|
||||||
|
isEditMode = false;
|
||||||
|
|
||||||
|
// Show view mode
|
||||||
|
document.getElementById('note-content').style.display = 'block';
|
||||||
|
document.getElementById('note-edit-form').style.display = 'none';
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
const contentDiv = document.getElementById('note-content');
|
||||||
|
contentDiv.innerHTML = `
|
||||||
|
<div style="text-align: center; padding: 40px;">
|
||||||
|
<div style="display: inline-block; padding: 10px; background: #f0f0f0; border-radius: 4px;">
|
||||||
|
Loading note...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Load note via AJAX
|
||||||
|
fetch(`<?= Config::get('URL') ?>note/getNote/${noteId}`)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) throw new Error('Network response was not ok');
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const note = data.note;
|
||||||
|
|
||||||
|
contentDiv.innerHTML = `
|
||||||
|
<div style="padding-bottom: 15px; border-bottom: 1px solid #ddd; margin-bottom: 15px; display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<div>
|
||||||
|
<h3 style="margin: 0; color: #333;">Note</h3>
|
||||||
|
<small style="color: #666;">
|
||||||
|
Created: ${new Date(note.note_timestamp).toLocaleString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 8px;">
|
||||||
|
<button onclick="editNote(${note.note_id})" style="padding: 8px 12px; background: #2196F3; color: white; border: none; border-radius: 4px; font-size: 12px; cursor: pointer;">Edit</button>
|
||||||
|
<a href="<?= Config::get('URL') ?>note/delete/${note.note_id}" style="padding: 8px 12px; background: #f44336; color: white; text-decoration: none; border-radius: 4px; font-size: 12px;" onclick="return confirm('Are you sure you want to delete this note?');">Delete</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="white-space: pre-wrap; word-wrap: break-word; line-height: 1.6; font-size: 14px; color: #333;">
|
||||||
|
${note.note_html}
|
||||||
|
</div>
|
||||||
|
${note.note_text.length > 100 ? `
|
||||||
|
<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #eee; font-size: 12px; color: #666;">
|
||||||
|
<i class="fa fa-file-text-o"></i> ${note.note_text.length} characters
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Highlight the active note in the sidebar
|
||||||
|
document.querySelectorAll('.notes-sidebar .note-card').forEach(card => {
|
||||||
|
card.style.backgroundColor = card.dataset.noteId == noteId ? '#e8f4fd' : 'white';
|
||||||
|
card.style.borderColor = card.dataset.noteId == noteId ? '#2196F3' : '#ddd';
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
contentDiv.innerHTML = `
|
||||||
|
<div style="text-align: center; padding: 40px; color: #f44336;">
|
||||||
|
<div style="font-size: 48px; margin-bottom: 10px;">⚠️</div>
|
||||||
|
<h4>Error loading note</h4>
|
||||||
|
<p>${data.message || 'Note not found'}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading note:', error);
|
||||||
|
contentDiv.innerHTML = `
|
||||||
|
<div style="text-align: center; padding: 40px; color: #f44336;">
|
||||||
|
<div style="font-size: 48px; margin-bottom: 10px;">❌</div>
|
||||||
|
<h4>Network Error</h4>
|
||||||
|
<p>Could not load note. Please try again.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function editNote(noteId) {
|
||||||
|
isEditMode = true;
|
||||||
|
|
||||||
|
// Hide view mode, show edit form
|
||||||
|
document.getElementById('note-content').style.display = 'none';
|
||||||
|
document.getElementById('note-edit-form').style.display = 'block';
|
||||||
|
|
||||||
|
// Load the note content
|
||||||
|
fetch(`<?= Config::get('URL') ?>note/getNote/${noteId}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const note = data.note;
|
||||||
|
const textarea = document.getElementById('edit-note-text');
|
||||||
|
const preview = document.getElementById('edit-preview');
|
||||||
|
|
||||||
|
textarea.value = note.note_text;
|
||||||
|
preview.innerHTML = note.note_html;
|
||||||
|
|
||||||
|
// Setup live preview
|
||||||
|
textarea.oninput = function() {
|
||||||
|
preview.innerHTML = SimpleMarkdown.parse(this.value);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading note for editing:', error);
|
||||||
|
alert('Error loading note for editing');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEdit() {
|
||||||
|
isEditMode = false;
|
||||||
|
document.getElementById('note-content').style.display = 'block';
|
||||||
|
document.getElementById('note-edit-form').style.display = 'none';
|
||||||
|
viewNote(currentNoteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveEdit() {
|
||||||
|
const textarea = document.getElementById('edit-note-text');
|
||||||
|
const noteText = textarea.value;
|
||||||
|
|
||||||
|
if (!noteText.trim()) {
|
||||||
|
alert('Note content cannot be empty');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveButton = document.querySelector('button[onclick="saveEdit()"]');
|
||||||
|
const originalText = saveButton.textContent;
|
||||||
|
saveButton.textContent = 'Saving...';
|
||||||
|
saveButton.disabled = true;
|
||||||
|
|
||||||
|
fetch(`<?= Config::get('URL') ?>note/editSave`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
note_id: currentNoteId,
|
||||||
|
note_text: noteText
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showFeedback('Note saved successfully!', 'success');
|
||||||
|
isEditMode = false;
|
||||||
|
document.getElementById('note-content').style.display = 'block';
|
||||||
|
document.getElementById('note-edit-form').style.display = 'none';
|
||||||
|
viewNote(currentNoteId); // Reload the note view
|
||||||
|
|
||||||
|
// Refresh the notes list to show the updated preview
|
||||||
|
setTimeout(() => {
|
||||||
|
refreshNotesList();
|
||||||
|
}, 500);
|
||||||
|
} else {
|
||||||
|
showFeedback(data.message || 'Failed to save note', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error saving note:', error);
|
||||||
|
showFeedback('Network error. Please try again.', 'error');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
saveButton.textContent = originalText;
|
||||||
|
saveButton.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple markdown parser for browser use
|
||||||
|
class SimpleMarkdown {
|
||||||
|
static parse(text) {
|
||||||
|
text = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
text = text.replace(/^#{1,6}\s+(.+)$/gm, '<h3>$1</h3>');
|
||||||
|
|
||||||
|
// Bold
|
||||||
|
text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
||||||
|
|
||||||
|
// Italic
|
||||||
|
text = text.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
||||||
|
|
||||||
|
// Code
|
||||||
|
text = text.replace(/`(.+?)`/g, '<code>$1</code>');
|
||||||
|
|
||||||
|
// Code blocks
|
||||||
|
text = text.replace(/```(.+?)```/gs, '<pre><code>$1</code></pre>');
|
||||||
|
|
||||||
|
// Links
|
||||||
|
text = text.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2" target="_blank">$1</a>');
|
||||||
|
|
||||||
|
// Blockquotes
|
||||||
|
text = text.replace(/^>\s+(.+)$/gm, '<blockquote>$1</blockquote>');
|
||||||
|
|
||||||
|
// Lists
|
||||||
|
text = text.replace(/^\-\s+(.+)$/gm, '<li>$1</li>');
|
||||||
|
text = text.replace(/(<li>.*<\/li>)/gs, '<ul>$1</ul>');
|
||||||
|
|
||||||
|
// Line breaks
|
||||||
|
text = text.replace(/\n/g, '<br>');
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add note IDs to sidebar cards for highlighting
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Handle note creation form submission
|
||||||
|
const createForm = document.getElementById('create-note-form');
|
||||||
|
if (createForm) {
|
||||||
|
createForm.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const noteText = this.note_text.value;
|
||||||
|
const submitButton = this.querySelector('button[type="submit"]');
|
||||||
|
const originalText = submitButton.textContent;
|
||||||
|
|
||||||
|
if (!noteText.trim()) {
|
||||||
|
alert('Please enter a note');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
submitButton.textContent = 'Creating...';
|
||||||
|
submitButton.disabled = true;
|
||||||
|
|
||||||
|
fetch(`<?= Config::get('URL') ?>note/create`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({ note_text: noteText })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
// Clear form
|
||||||
|
this.reset();
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
showFeedback('Note created successfully!', 'success');
|
||||||
|
|
||||||
|
// Refresh notes list after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
refreshNotesList();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
showFeedback(data.message || 'Failed to create note', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error creating note:', error);
|
||||||
|
showFeedback('Network error. Please try again.', 'error');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
// Restore button state
|
||||||
|
submitButton.textContent = originalText;
|
||||||
|
submitButton.disabled = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add note IDs to sidebar cards for highlighting
|
||||||
|
const noteCards = document.querySelectorAll('.notes-sidebar > div:nth-child(2) > div');
|
||||||
|
noteCards.forEach((card, index) => {
|
||||||
|
// Skip the "Your Notes" header and the form
|
||||||
|
if (card.querySelector('a[href*="note/edit"]')) {
|
||||||
|
const editLink = card.querySelector('a[href*="note/edit"]');
|
||||||
|
const noteId = editLink.href.split('/').pop();
|
||||||
|
card.classList.add('note-card');
|
||||||
|
card.dataset.noteId = noteId;
|
||||||
|
card.style.cursor = 'pointer';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function refreshNotesList() {
|
||||||
|
// Reload the page to refresh the notes list
|
||||||
|
// This is simpler than trying to rebuild the sidebar via AJAX
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFeedback(message, type) {
|
||||||
|
// Create feedback element
|
||||||
|
const feedback = document.createElement('div');
|
||||||
|
feedback.className = `feedback-${type}`;
|
||||||
|
feedback.textContent = message;
|
||||||
|
feedback.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: ${type === 'success' ? '#4caf50' : '#f44336'};
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(feedback);
|
||||||
|
|
||||||
|
// Remove after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
feedback.style.animation = 'slideOut 0.3s ease';
|
||||||
|
setTimeout(() => feedback.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add CSS for animations
|
||||||
|
const animationStyle = document.createElement('style');
|
||||||
|
animationStyle.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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-success {
|
||||||
|
background: linear-gradient(135deg, #4caf50, #45a049) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-error {
|
||||||
|
background: linear-gradient(135deg, #f44336, #d32f2f) !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(animationStyle);
|
||||||
|
|
||||||
|
// Add some CSS for better styling
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
.note-card {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-card:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-main-content {
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.note-main-content {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,18 +1,30 @@
|
|||||||
<div class="container">
|
<script src="https://www.google.com/recaptcha/api.js?render=<?php echo Config::get('RECAPTCHA_SITE_KEY'); ?>"></script>
|
||||||
|
|
||||||
<!-- echo out the system feedback (error and success messages) -->
|
<div class="container">
|
||||||
<?php $this->renderFeedbackMessages(); ?>
|
<?php $this->renderFeedbackMessages(); ?>
|
||||||
|
|
||||||
<!-- login box on left side -->
|
|
||||||
<div class="login-box" style="width: 50%; display: block;">
|
<div class="login-box" style="width: 50%; display: block;">
|
||||||
<h2>Register a new account</h2>
|
<h2>Register a new account</h2>
|
||||||
|
|
||||||
<!-- register form -->
|
<form method="post" action="<?php echo Config::get('URL'); ?>register/register_action" id="register-form">
|
||||||
<form method="post" action="<?php echo Config::get('URL'); ?>register/register_action">
|
|
||||||
<input type="text" pattern="[a-zA-Z0-9]{2,64}" name="user_name" placeholder="Username (letters/numbers, 2-64 chars)" required />
|
<input type="text" pattern="[a-zA-Z0-9]{2,64}" name="user_name" placeholder="Username (letters/numbers, 2-64 chars)" required />
|
||||||
<input type="text" name="user_email" placeholder="email address (a real address)" required />
|
<input type="text" name="user_email" placeholder="email address (a real address)" required />
|
||||||
<input type="password" name="user_password_new" pattern=".{6,}" placeholder="Password (6+ characters)" required autocomplete="off" />
|
<input type="password" name="user_password_new" pattern=".{6,}" placeholder="Password (6+ characters)" required autocomplete="off" />
|
||||||
|
<input type="hidden" name="g-recaptcha-response" id="recaptcha-response" />
|
||||||
<input type="submit" value="Register" />
|
<input type="submit" value="Register" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('register-form').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var form = this;
|
||||||
|
grecaptcha.ready(function() {
|
||||||
|
grecaptcha.execute('<?php echo Config::get('RECAPTCHA_SITE_KEY'); ?>', {action: 'register'}).then(function(token) {
|
||||||
|
document.getElementById('recaptcha-response').value = token;
|
||||||
|
form.submit();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|||||||
473
application/view/sql/index.php
Normal file
473
application/view/sql/index.php
Normal 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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
|
||||||
|
// 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>
|
||||||
68
application/view/table/add_column.php
Normal file
68
application/view/table/add_column.php
Normal 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>
|
||||||
122
application/view/table/create.php
Normal file
122
application/view/table/create.php
Normal 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>
|
||||||
435
application/view/table/show.php
Normal file
435
application/view/table/show.php
Normal 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>
|
||||||
112
application/view/table/structure.php
Normal file
112
application/view/table/structure.php
Normal 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>
|
||||||
12
assets/footer/gray0_ctp_on_line.svg
Normal file
12
assets/footer/gray0_ctp_on_line.svg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<svg width="600" height="75" viewBox="0 0 600 75" version="1.1" xmlns="http://www.w3.org/2000/svg" style="stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 1.5;">
|
||||||
|
<path transform="matrix(1,0,0,1,92.3579,4.11772)" d="M105.809,48.397C105.809,44.506 102.473,43.931 102.473,33.503" style="fill: none; stroke: rgb(110, 108, 126); stroke-width: 1.5px;"/>
|
||||||
|
<path transform="matrix(1,0,0,1,92.3579,4.11772)" d="M109.397,38.324L109.397,48.321" style="fill: none; stroke: rgb(110, 108, 126); stroke-width: 1.5px;"/>
|
||||||
|
<path transform="matrix(1,0,0,1,92.3579,4.11772)" d="M112.883,48.152C112.883,44.717 115.053,40.554 115.053,35.084C115.053,29.613 114.393,24.795 114.216,21.81" style="fill: none; stroke: rgb(110, 108, 126); stroke-width: 1.5px;"/>
|
||||||
|
<path transform="matrix(1,0,0,1,92.3579,4.11772)" d="M112.951,22.241C112.951,22.241 116.335,21.976 117.504,16.695" style="fill: none; stroke: rgb(110, 108, 126); stroke-width: 1.5px;"/>
|
||||||
|
<path transform="matrix(1,0,0,1,92.3579,4.11772)" d="M107.788,11.843C107.788,11.843 106.369,7.434 105.169,7.434C103.969,7.434 101.87,13.187 101.87,21.862C101.87,24.103 90.181,29.985 92.659,43.571C93.057,45.751 94.053,49.908 94.053,49.924C94.053,49.94 96.571,59.453 91.184,59.453C90.063,59.453 89.526,58.833 88.405,58.833C87.285,58.833 86.381,59.598 86.381,60.591C86.381,61.584 87.491,64.025 91.446,64.025C98.593,64.025 98.865,58.038 98.865,54.158C98.865,50.278 98.829,51.479 98.829,50.844C98.829,48.717 100.601,48.284 101.259,48.043" style="fill: none; stroke: rgb(110, 108, 126); stroke-width: 1.5px;"/>
|
||||||
|
<ellipse transform="matrix(1.00474,-0.404483,0.370766,0.920982,85.4108,49.8267)" cx="111.892" cy="15.766" rx="1.032" ry="1.449" style="fill: rgb(47, 44, 62);"/>
|
||||||
|
<path transform="matrix(1,0,0,1,92.3579,4.11772)" d="M110.074,10.347C113.617,10.347 114.448,14.635 117.14,14.635" style="fill: none; stroke: rgb(110, 108, 126); stroke-width: 1.5px;"/>
|
||||||
|
<path transform="matrix(1,0,0,1,92.3579,4.11772)" d="M112.568,9.074C112.568,9.074 111.553,6.74 110.677,6.74C109.801,6.74 108.537,9.169 108.537,9.169" style="fill: none; stroke: rgb(110, 108, 126); stroke-width: 1.5px;"/>
|
||||||
|
<path transform="matrix(3.96613,0,0,5.89452,-177.012,-336.835)" d="M93.717,66.428L195.647,66.428" style="fill: none; stroke: rgb(110, 108, 126); stroke-width: 0.3px;"/>
|
||||||
|
<path transform="matrix(1.78906,0,0,2.78204,-166.7,-130.078)" d="M93.717,66.428L195.647,66.428" style="fill: none; stroke: rgb(110, 108, 126); stroke-width: 0.64px;"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
BIN
assets/screenshots/img.png
Normal file
BIN
assets/screenshots/img.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
BIN
assets/screenshots/img2.png
Normal file
BIN
assets/screenshots/img2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/screenshots/img3.png
Normal file
BIN
assets/screenshots/img3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
BIN
assets/screenshots/notes_1.png
Normal file
BIN
assets/screenshots/notes_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
BIN
assets/screenshots/notes_2.png
Normal file
BIN
assets/screenshots/notes_2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
953
public/css/dbmanager.css
Normal file
953
public/css/dbmanager.css
Normal 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
474
public/css/gallery.css
Normal 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;
|
||||||
|
}
|
||||||
@@ -85,9 +85,12 @@ body {
|
|||||||
}
|
}
|
||||||
/* TODO */
|
/* TODO */
|
||||||
.navigation {
|
.navigation {
|
||||||
|
|
||||||
}
|
}
|
||||||
.navigation.right {
|
.navigation.right {
|
||||||
float: right;
|
float: right;
|
||||||
|
position: relative;
|
||||||
|
z-index: 99;
|
||||||
}
|
}
|
||||||
.navigation li {
|
.navigation li {
|
||||||
float: left;
|
float: left;
|
||||||
|
|||||||
12
public/gallery_uploads/.htaccess
Normal file
12
public/gallery_uploads/.htaccess
Normal 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>
|
||||||
1
public/gallery_uploads/enc_69773206330d4_1769419270.bin
Normal file
1
public/gallery_uploads/enc_69773206330d4_1769419270.bin
Normal file
File diff suppressed because one or more lines are too long
1
public/gallery_uploads/enc_69773211625f5_1769419281.bin
Normal file
1
public/gallery_uploads/enc_69773211625f5_1769419281.bin
Normal file
File diff suppressed because one or more lines are too long
1
public/gallery_uploads/enc_697733fc9d48c_1769419772.bin
Normal file
1
public/gallery_uploads/enc_697733fc9d48c_1769419772.bin
Normal file
File diff suppressed because one or more lines are too long
1
public/gallery_uploads/enc_69773584ad61d_1769420164.bin
Normal file
1
public/gallery_uploads/enc_69773584ad61d_1769420164.bin
Normal file
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
433
public/js/dbmanager.js
Normal 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);
|
||||||
293
public/js/messaging.js
Normal file
293
public/js/messaging.js
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
// Messaging JavaScript functionality
|
||||||
|
class MessagingSystem {
|
||||||
|
constructor() {
|
||||||
|
this.currentUserId = null;
|
||||||
|
this.currentConversationUserId = null;
|
||||||
|
this.pollingInterval = null;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Get current user ID from the page
|
||||||
|
this.currentUserId = document.querySelector('meta[name="user-id"]')?.content;
|
||||||
|
|
||||||
|
if (this.currentUserId) {
|
||||||
|
this.setupEventListeners();
|
||||||
|
this.startPolling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// Handle message form submissions
|
||||||
|
document.querySelectorAll('form[id$="-form"], form[action*="message/send"]').forEach(form => {
|
||||||
|
form.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.sendMessage(form);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle conversation switching
|
||||||
|
document.querySelectorAll('a[href*="message/conversation/"]').forEach(link => {
|
||||||
|
link.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const userId = link.href.split('/').pop();
|
||||||
|
this.loadConversation(userId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessage(form) {
|
||||||
|
try {
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
const submitButton = form.querySelector('button[type="submit"]');
|
||||||
|
const originalText = submitButton.textContent;
|
||||||
|
submitButton.textContent = 'Sending...';
|
||||||
|
submitButton.disabled = true;
|
||||||
|
|
||||||
|
const response = await fetch('index.php/message/send', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
},
|
||||||
|
body: new URLSearchParams(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// Clear form
|
||||||
|
form.reset();
|
||||||
|
|
||||||
|
// Reload messages or show success
|
||||||
|
if (this.currentConversationUserId) {
|
||||||
|
await this.loadMessages(this.currentConversationUserId);
|
||||||
|
} else {
|
||||||
|
this.showFeedback('Message sent successfully!', 'success');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.showFeedback(result.message || 'Failed to send message', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending message:', error);
|
||||||
|
this.showFeedback('Network error. Please try again.', 'error');
|
||||||
|
} finally {
|
||||||
|
// Restore button state
|
||||||
|
const submitButton = form.querySelector('button[type="submit"]');
|
||||||
|
submitButton.textContent = originalText;
|
||||||
|
submitButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadConversation(userId) {
|
||||||
|
this.currentConversationUserId = userId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`index.php/message/conversation/${userId}`, {
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const html = await response.text();
|
||||||
|
const conversationContainer = document.querySelector('.conversation-container') ||
|
||||||
|
document.querySelector('.panel-default');
|
||||||
|
|
||||||
|
if (conversationContainer) {
|
||||||
|
conversationContainer.innerHTML = html;
|
||||||
|
this.setupEventListeners(); // Re-setup event listeners for new content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading conversation:', error);
|
||||||
|
this.showFeedback('Failed to load conversation', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadMessages(userId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`index.php/message/getConversationMessages/${userId}`, {
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const messages = await response.json();
|
||||||
|
this.displayMessages(messages);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading messages:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadGlobalMessages() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('index.php/message/getGlobalMessages', {
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const messages = await response.json();
|
||||||
|
this.displayGlobalMessages(messages);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading global messages:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
displayMessages(messages) {
|
||||||
|
const messageContainer = document.querySelector('.message-container');
|
||||||
|
if (!messageContainer) return;
|
||||||
|
|
||||||
|
messageContainer.innerHTML = '';
|
||||||
|
|
||||||
|
messages.forEach(message => {
|
||||||
|
const messageDiv = document.createElement('div');
|
||||||
|
messageDiv.className = `message ${message.sender_id == this.currentUserId ? 'sent' : 'received'}`;
|
||||||
|
|
||||||
|
messageDiv.innerHTML = `
|
||||||
|
<div class="message-bubble">
|
||||||
|
${this.escapeHtml(message.message)}
|
||||||
|
</div>
|
||||||
|
<div class="message-time">
|
||||||
|
${this.formatDate(message.created_at)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
messageContainer.appendChild(messageDiv);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Scroll to bottom
|
||||||
|
messageContainer.scrollTop = messageContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayGlobalMessages(messages) {
|
||||||
|
const globalContainer = document.querySelector('.global-messages-container');
|
||||||
|
if (!globalContainer) return;
|
||||||
|
|
||||||
|
globalContainer.innerHTML = '';
|
||||||
|
|
||||||
|
messages.forEach(message => {
|
||||||
|
const messageDiv = document.createElement('div');
|
||||||
|
messageDiv.className = 'global-message';
|
||||||
|
|
||||||
|
messageDiv.innerHTML = `
|
||||||
|
<div class="message-header">
|
||||||
|
<strong>${this.escapeHtml(message.sender_name)}</strong>
|
||||||
|
<span class="message-time">${this.formatDate(message.created_at)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="message-bubble">
|
||||||
|
${this.escapeHtml(message.message)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
globalContainer.appendChild(messageDiv);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Scroll to bottom
|
||||||
|
globalContainer.scrollTop = globalContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
startPolling() {
|
||||||
|
// Poll for new messages every 5 seconds
|
||||||
|
this.pollingInterval = setInterval(() => {
|
||||||
|
this.checkForNewMessages();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
// Also check for unread count
|
||||||
|
setInterval(() => {
|
||||||
|
this.updateUnreadCount();
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkForNewMessages() {
|
||||||
|
if (this.currentConversationUserId) {
|
||||||
|
await this.loadMessages(this.currentConversationUserId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUnreadCount() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('index.php/message/unreadcount', {
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
this.updateUnreadCountDisplay(data.count);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating unread count:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUnreadCountDisplay(count) {
|
||||||
|
const unreadElements = document.querySelectorAll('.unread-count');
|
||||||
|
unreadElements.forEach(element => {
|
||||||
|
if (count > 0) {
|
||||||
|
element.textContent = count;
|
||||||
|
element.style.display = 'inline-block';
|
||||||
|
} else {
|
||||||
|
element.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showFeedback(message, type) {
|
||||||
|
// Create feedback element
|
||||||
|
const feedback = document.createElement('div');
|
||||||
|
feedback.className = `feedback-${type}`;
|
||||||
|
feedback.textContent = message;
|
||||||
|
feedback.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: ${type === 'success' ? '#4caf50' : '#f44336'};
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(feedback);
|
||||||
|
|
||||||
|
// Remove after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
feedback.remove();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDate(dateString) {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the messaging system when DOM is loaded
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
window.messagingSystem = new MessagingSystem();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make it available globally for any other scripts that might need it
|
||||||
|
window.MessagingSystem = MessagingSystem;
|
||||||
Reference in New Issue
Block a user