587 lines
19 KiB
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;
|
|
}
|
|
}
|