diff --git a/README.md b/README.md index 0530dc4..b7dcdaa 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,9 @@ if (!$response_data->success || $response_data->score < 0.5) { - Bessere User Experience - Intelligente Bot-Erkennung durch Verhaltensanalyse -``` - -[📸 Screenshot: Registrierungsformular] -[📸 Screenshot: reCAPTCHA Badge] -``` - + + + --- ## Erweiterung der Admin-Funktionen @@ -123,6 +120,79 @@ Implementierung einer vollständigen Notizen-Anwendung mit CRUD-Funktionalität --- +## Bildergalerie mit Verschlüsselung + +#### Sichere Bildergalerie mit AES-256 Verschlüsselung + +**Beschreibung:** +Implementierung einer vollständigen Bildergalerie mit clientseitiger Vorschau und serverseitiger AES-256-CBC Verschlüsselung. Alle hochgeladenen Bilder werden verschlüsselt gespeichert und erst beim Abruf entschlüsselt. + +**Features:** +- **AES-256-CBC Verschlüsselung**: Jedes Bild wird mit einem einzigartigen Schlüssel verschlüsselt +- **Automatische Thumbnail-Generierung**: Optimierte Vorschaubilder für schnelles Laden +- **Drag & Drop Upload**: Moderne Upload-Oberfläche mit Datei-Drag & Drop +- **AJAX-Upload mit Fortschrittsanzeige**: Echtzeit-Feedback während des Uploads +- **Öffentliche/Private Bilder**: Benutzer können Sichtbarkeit ihrer Bilder steuern +- **Bildvorschau vor Upload**: Clientseitige Vorschau des ausgewählten Bildes + +**Technische Umsetzung:** + +*Verschlüsselung (Upload):* +```php +$encryption_key = bin2hex(random_bytes(32)); +$iv = random_bytes(openssl_cipher_iv_length('AES-256-CBC')); +$encrypted_image = openssl_encrypt($image_data, 'AES-256-CBC', $encryption_key, OPENSSL_RAW_DATA, $iv); +$encrypted_image = base64_encode($iv . $encrypted_image); +``` + +*Entschlüsselung (Anzeige):* +```php +$encrypted_data = base64_decode(file_get_contents($filepath)); +$iv = substr($encrypted_data, 0, $iv_length); +$encrypted_data = substr($encrypted_data, $iv_length); +$decrypted = openssl_decrypt($encrypted_data, 'AES-256-CBC', $encryption_key, OPENSSL_RAW_DATA, $iv); +``` + +**Datenbankstruktur:** +```sql +CREATE TABLE `gallery` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` int(11) NOT NULL, + `filename` varchar(255) NOT NULL, + `thumb_filename` varchar(255) NOT NULL, + `title` varchar(255) NOT NULL, + `description` text, + `is_public` tinyint(1) NOT NULL DEFAULT 1, + `file_size` int(11) NOT NULL DEFAULT 0, + `mime_type` varchar(100) NOT NULL, + `encryption_key` varchar(64) NOT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +); +``` + +**Unterstützte Formate:** +- JPEG / JPG +- PNG (mit Transparenz-Erhaltung) +- GIF +- WebP + +**Zugriffsrechte:** +- **Öffentliche Galerie**: Alle öffentlichen Bilder für jeden sichtbar +- **Meine Bilder**: Nur eigene Bilder (öffentlich und privat) +- **Upload/Bearbeiten/Löschen**: Nur für angemeldete Benutzer + +``` + +[📸 Screenshot: Galerie-Übersicht mit Bildraster] +[📸 Screenshot: Upload-Formular mit Drag & Drop Zone] +[📸 Screenshot: Upload-Fortschrittsanzeige] +[📸 Screenshot: Einzelbildansicht] +[📸 Screenshot: Meine Bilder - Verwaltung] +``` + +--- + ## jQuery - Einführung und Grundlagen #### JavaScript Basics diff --git a/_installation/gallery_table.sql b/_installation/gallery_table.sql new file mode 100644 index 0000000..106985f --- /dev/null +++ b/_installation/gallery_table.sql @@ -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; diff --git a/application/config/config.development.php b/application/config/config.development.php index 71ce7ef..af8a81f 100644 --- a/application/config/config.development.php +++ b/application/config/config.development.php @@ -47,6 +47,8 @@ return array( */ 'PATH_AVATARS' => realpath(dirname(__FILE__).'/../../') . '/public/avatars/', 'PATH_AVATARS_PUBLIC' => 'avatars/', + 'PATH_GALLERY' => dirname(__FILE__) . '/../../public/gallery_uploads/', + 'PATH_GALLERY_PUBLIC' => 'gallery_uploads/', /** * Configuration for: Default controller and action */ diff --git a/application/controller/DatabaseController.php b/application/controller/DatabaseController.php index bb078c4..ca36318 100644 --- a/application/controller/DatabaseController.php +++ b/application/controller/DatabaseController.php @@ -126,7 +126,7 @@ class DatabaseController extends Controller } $structure = DatabaseModel::getDatabaseStructure($database_name); - + header('Content-Type: application/json'); echo json_encode([ 'success' => true, @@ -134,6 +134,22 @@ class DatabaseController extends Controller ]); } + /** + * Get columns for a specific table (AJAX endpoint) + * @param string $database_name + * @param string $table_name + */ + public function getColumns($database_name, $table_name) + { + $columns = TableModel::getTableColumns($database_name, $table_name); + + header('Content-Type: application/json'); + echo json_encode([ + 'success' => true, + 'columns' => $columns + ]); + } + /** * Export database as raw SQL text * @param string $database_name diff --git a/application/controller/GalleryController.php b/application/controller/GalleryController.php new file mode 100644 index 0000000..cef4438 --- /dev/null +++ b/application/controller/GalleryController.php @@ -0,0 +1,226 @@ +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'; + } +} diff --git a/application/core/View.php b/application/core/View.php index cabc254..7e01b03 100644 --- a/application/core/View.php +++ b/application/core/View.php @@ -47,6 +47,11 @@ class View public $privileges; public $current_user; + /* Gallery properties */ + public $images; + public $total_images; + public $image; + /** * Static property to track if header has been rendered */ diff --git a/application/model/GalleryModel.php b/application/model/GalleryModel.php new file mode 100644 index 0000000..398ca1d --- /dev/null +++ b/application/model/GalleryModel.php @@ -0,0 +1,323 @@ +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'; + } +} diff --git a/application/view/_templates/dbmanager_header.php b/application/view/_templates/dbmanager_header.php index 830cf94..42001a2 100644 --- a/application/view/_templates/dbmanager_header.php +++ b/application/view/_templates/dbmanager_header.php @@ -76,7 +76,7 @@ -