Newwebshop/app/API/controllers/ReviewApiController.php

587 lines
19 KiB
PHP

<?php
/**
* Copyright seit 2024 Webshop System
*
* Review API Controller für das Webshop-System
*
* @author Webshop System
* @license GPL v3
*/
namespace App\API\Controllers;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception;
class ReviewApiController extends ApiController
{
/**
* Bewertungen für ein Produkt abrufen
*/
public function getProductReviews($productId)
{
$page = max(1, intval($_GET['page'] ?? 1));
$limit = min(50, max(1, intval($_GET['limit'] ?? 10)));
$sort = $_GET['sort'] ?? 'newest'; // newest, oldest, rating
$rating = $_GET['rating'] ?? null;
$offset = ($page - 1) * $limit;
try {
$whereConditions = ['r.product_id = ?', 'r.active = 1'];
$params = [$productId];
if ($rating) {
$whereConditions[] = 'r.rating = ?';
$params[] = $rating;
}
$whereClause = implode(' AND ', $whereConditions);
// Sortierung
$orderBy = match($sort) {
'oldest' => '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;
}
}