'r.created_at ASC', 'rating' => 'r.rating DESC, r.created_at DESC', default => 'r.created_at DESC' }; // Gesamtanzahl $countSql = " SELECT COUNT(*) as total FROM ws_review r WHERE $whereClause "; $stmt = $this->conn->prepare($countSql); $stmt->execute($params); $totalCount = $stmt->fetchAssociative()['total']; // Bewertungen laden $sql = " SELECT r.*, c.first_name, c.last_name FROM ws_review r LEFT JOIN ws_customer c ON r.customer_id = c.id WHERE $whereClause ORDER BY $orderBy LIMIT $limit OFFSET $offset "; $stmt = $this->conn->prepare($sql); $stmt->execute($params); $reviews = $stmt->fetchAllAssociative(); // Durchschnittsbewertung berechnen $avgSql = " SELECT AVG(rating) as avg_rating, COUNT(*) as total_reviews FROM ws_review r WHERE r.product_id = ? AND r.active = 1 "; $stmt = $this->conn->prepare($avgSql); $stmt->execute([$productId]); $avgData = $stmt->fetchAssociative(); // Bewertungsverteilung $distributionSql = " SELECT rating, COUNT(*) as count FROM ws_review r WHERE r.product_id = ? AND r.active = 1 GROUP BY rating ORDER BY rating DESC "; $stmt = $this->conn->prepare($distributionSql); $stmt->execute([$productId]); $distribution = $stmt->fetchAllAssociative(); $this->sendResponse([ 'success' => true, 'data' => [ 'reviews' => $reviews, 'pagination' => [ 'page' => $page, 'limit' => $limit, 'total' => $totalCount, 'pages' => ceil($totalCount / $limit) ], 'summary' => [ 'average_rating' => round($avgData['avg_rating'] ?? 0, 1), 'total_reviews' => $avgData['total_reviews'] ?? 0, 'rating_distribution' => $distribution ] ] ]); } catch (Exception $e) { $this->sendError('Failed to fetch reviews: ' . $e->getMessage(), 500); } } /** * Bewertung erstellen */ public function createReview($productId) { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->sendError('Method not allowed', 405); } $token = $this->getBearerToken(); $decodedToken = $this->validateJWT($token); if (!$decodedToken) { $this->sendError('Authentication required', 401); } $input = json_decode(file_get_contents('php://input'), true); if (!$input) { $this->sendError('Invalid JSON input', 400); } if (empty($input['rating']) || empty($input['title']) || empty($input['comment'])) { $this->sendError('Rating, title and comment are required', 400); } if (!is_numeric($input['rating']) || $input['rating'] < 1 || $input['rating'] > 5) { $this->sendError('Rating must be between 1 and 5', 400); } // Spam-Schutz if ($this->isSpam($input['comment'])) { $this->sendError('Review contains spam content', 400); } try { // Prüfen ob Produkt existiert $stmt = $this->conn->prepare('SELECT id FROM ws_product WHERE id = ? AND active = 1'); $stmt->execute([$productId]); $product = $stmt->fetchAssociative(); if (!$product) { $this->sendError('Product not found', 404); } // Prüfen ob Kunde bereits bewertet hat $stmt = $this->conn->prepare(' SELECT id FROM ws_review WHERE product_id = ? AND customer_id = ? '); $stmt->execute([$productId, $decodedToken['customer_id']]); $existingReview = $stmt->fetchAssociative(); if ($existingReview) { $this->sendError('You have already reviewed this product', 409); } // Bewertung erstellen $stmt = $this->conn->prepare(' INSERT INTO ws_review (product_id, customer_id, rating, title, comment, active, created_at) VALUES (?, ?, ?, ?, ?, 1, NOW()) '); $stmt->execute([ $productId, $decodedToken['customer_id'], $input['rating'], $input['title'], $input['comment'] ]); $reviewId = $this->conn->lastInsertId(); // Produkt-Bewertung aktualisieren $this->updateProductRating($productId); $this->sendResponse([ 'success' => true, 'data' => [ 'review_id' => $reviewId ], 'message' => 'Review created successfully' ]); } catch (Exception $e) { $this->sendError('Failed to create review: ' . $e->getMessage(), 500); } } /** * Bewertung aktualisieren */ public function updateReview($reviewId) { if ($_SERVER['REQUEST_METHOD'] !== 'PUT') { $this->sendError('Method not allowed', 405); } $token = $this->getBearerToken(); $decodedToken = $this->validateJWT($token); if (!$decodedToken) { $this->sendError('Authentication required', 401); } $input = json_decode(file_get_contents('php://input'), true); if (!$input) { $this->sendError('Invalid JSON input', 400); } try { // Prüfen ob Bewertung dem Kunden gehört $stmt = $this->conn->prepare(' SELECT id, product_id FROM ws_review WHERE id = ? AND customer_id = ? '); $stmt->execute([$reviewId, $decodedToken['customer_id']]); $review = $stmt->fetchAssociative(); if (!$review) { $this->sendError('Review not found or unauthorized', 404); } $updateFields = []; $params = []; if (isset($input['rating'])) { if (!is_numeric($input['rating']) || $input['rating'] < 1 || $input['rating'] > 5) { $this->sendError('Rating must be between 1 and 5', 400); } $updateFields[] = 'rating = ?'; $params[] = $input['rating']; } if (isset($input['title'])) { $updateFields[] = 'title = ?'; $params[] = $input['title']; } if (isset($input['comment'])) { // Spam-Schutz if ($this->isSpam($input['comment'])) { $this->sendError('Review contains spam content', 400); } $updateFields[] = 'comment = ?'; $params[] = $input['comment']; } if (empty($updateFields)) { $this->sendError('No fields to update', 400); } $updateFields[] = 'updated_at = NOW()'; $params[] = $reviewId; $sql = 'UPDATE ws_review SET ' . implode(', ', $updateFields) . ' WHERE id = ?'; $stmt = $this->conn->prepare($sql); $stmt->execute($params); // Produkt-Bewertung aktualisieren $this->updateProductRating($review['product_id']); $this->sendResponse([ 'success' => true, 'message' => 'Review updated successfully' ]); } catch (Exception $e) { $this->sendError('Failed to update review: ' . $e->getMessage(), 500); } } /** * Bewertung löschen */ public function deleteReview($reviewId) { if ($_SERVER['REQUEST_METHOD'] !== 'DELETE') { $this->sendError('Method not allowed', 405); } $token = $this->getBearerToken(); $decodedToken = $this->validateJWT($token); if (!$decodedToken) { $this->sendError('Authentication required', 401); } try { // Prüfen ob Bewertung dem Kunden gehört $stmt = $this->conn->prepare(' SELECT id, product_id FROM ws_review WHERE id = ? AND customer_id = ? '); $stmt->execute([$reviewId, $decodedToken['customer_id']]); $review = $stmt->fetchAssociative(); if (!$review) { $this->sendError('Review not found or unauthorized', 404); } // Bewertung löschen (soft delete) $stmt = $this->conn->prepare(' UPDATE ws_review SET active = 0, deleted_at = NOW() WHERE id = ? '); $stmt->execute([$reviewId]); // Produkt-Bewertung aktualisieren $this->updateProductRating($review['product_id']); $this->sendResponse([ 'success' => true, 'message' => 'Review deleted successfully' ]); } catch (Exception $e) { $this->sendError('Failed to delete review: ' . $e->getMessage(), 500); } } /** * Bewertung melden */ public function reportReview($reviewId) { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->sendError('Method not allowed', 405); } $token = $this->getBearerToken(); $decodedToken = $this->validateJWT($token); if (!$decodedToken) { $this->sendError('Authentication required', 401); } $input = json_decode(file_get_contents('php://input'), true); if (!$input || empty($input['reason'])) { $this->sendError('Reason is required', 400); } try { // Prüfen ob Bewertung existiert $stmt = $this->conn->prepare('SELECT id FROM ws_review WHERE id = ? AND active = 1'); $stmt->execute([$reviewId]); $review = $stmt->fetchAssociative(); if (!$review) { $this->sendError('Review not found', 404); } // Prüfen ob bereits gemeldet $stmt = $this->conn->prepare(' SELECT id FROM ws_review_report WHERE review_id = ? AND reporter_id = ? '); $stmt->execute([$reviewId, $decodedToken['customer_id']]); $existingReport = $stmt->fetchAssociative(); if ($existingReport) { $this->sendError('You have already reported this review', 409); } // Meldung erstellen $stmt = $this->conn->prepare(' INSERT INTO ws_review_report (review_id, reporter_id, reason, created_at) VALUES (?, ?, ?, NOW()) '); $stmt->execute([ $reviewId, $decodedToken['customer_id'], $input['reason'] ]); $this->sendResponse([ 'success' => true, 'message' => 'Review reported successfully' ]); } catch (Exception $e) { $this->sendError('Failed to report review: ' . $e->getMessage(), 500); } } /** * Bewertung als hilfreich markieren */ public function helpfulReview($reviewId) { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->sendError('Method not allowed', 405); } $token = $this->getBearerToken(); $decodedToken = $this->validateJWT($token); if (!$decodedToken) { $this->sendError('Authentication required', 401); } try { // Prüfen ob Bewertung existiert $stmt = $this->conn->prepare('SELECT id FROM ws_review WHERE id = ? AND active = 1'); $stmt->execute([$reviewId]); $review = $stmt->fetchAssociative(); if (!$review) { $this->sendError('Review not found', 404); } // Prüfen ob bereits als hilfreich markiert $stmt = $this->conn->prepare(' SELECT id FROM ws_review_helpful WHERE review_id = ? AND customer_id = ? '); $stmt->execute([$reviewId, $decodedToken['customer_id']]); $existingHelpful = $stmt->fetchAssociative(); if ($existingHelpful) { $this->sendError('You have already marked this review as helpful', 409); } // Als hilfreich markieren $stmt = $this->conn->prepare(' INSERT INTO ws_review_helpful (review_id, customer_id, created_at) VALUES (?, ?, NOW()) '); $stmt->execute([$reviewId, $decodedToken['customer_id']]); // Hilfreich-Zähler aktualisieren $stmt = $this->conn->prepare(' UPDATE ws_review SET helpful_count = helpful_count + 1 WHERE id = ? '); $stmt->execute([$reviewId]); $this->sendResponse([ 'success' => true, 'message' => 'Review marked as helpful' ]); } catch (Exception $e) { $this->sendError('Failed to mark review as helpful: ' . $e->getMessage(), 500); } } /** * Produkt-Bewertung aktualisieren */ private function updateProductRating($productId) { try { $stmt = $this->conn->prepare(' SELECT AVG(rating) as avg_rating, COUNT(*) as total_reviews FROM ws_review WHERE product_id = ? AND active = 1 '); $stmt->execute([$productId]); $data = $stmt->fetchAssociative(); $stmt = $this->conn->prepare(' UPDATE ws_product SET avg_rating = ?, review_count = ? WHERE id = ? '); $stmt->execute([ round($data['avg_rating'] ?? 0, 1), $data['total_reviews'] ?? 0, $productId ]); } catch (Exception $e) { // Ignore rating update errors } } /** * Spam-Erkennung */ private function isSpam($text) { $spamKeywords = [ 'buy now', 'click here', 'free money', 'make money fast', 'earn money', 'work from home', 'get rich quick', 'viagra', 'casino', 'poker', 'lottery', 'winner', 'limited time', 'act now', 'urgent', 'exclusive offer' ]; $text = strtolower($text); foreach ($spamKeywords as $keyword) { if (strpos($text, $keyword) !== false) { return true; } } // URL-Erkennung if (preg_match('/https?:\/\/[^\s]+/', $text)) { return true; } // Caps-Lock-Erkennung $uppercaseCount = strlen(preg_replace('/[^A-Z]/', '', $text)); $totalCount = strlen($text); if ($totalCount > 0 && ($uppercaseCount / $totalCount) > 0.7) { return true; } return false; } /** * JWT-Token validieren (aus CustomerApiController) */ private function validateJWT($token) { if (!$token) { return false; } $parts = explode('.', $token); if (count($parts) !== 3) { return false; } list($header, $payload, $signature) = $parts; $validSignature = hash_hmac('sha256', $header . "." . $payload, getenv('JWT_SECRET') ?: 'webshop_secret_key', true); $validSignature = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($validSignature)); if ($signature !== $validSignature) { return false; } $payload = json_decode(base64_decode(str_replace(['-', '_'], ['+', '/'], $payload)), true); if (!$payload || $payload['exp'] < time()) { return false; } return $payload; } /** * Bearer-Token aus Header extrahieren */ private function getBearerToken() { $headers = getallheaders(); $authHeader = $headers['Authorization'] ?? ''; if (preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) { return $matches[1]; } return null; } }