470 lines
14 KiB
PHP
470 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* Copyright seit 2024 Webshop System
|
|
*
|
|
* Webhook 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 WebhookController extends ApiController
|
|
{
|
|
/**
|
|
* Webhook registrieren
|
|
*/
|
|
public function register()
|
|
{
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
$this->sendError('Method not allowed', 405);
|
|
}
|
|
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
|
|
if (!$input) {
|
|
$this->sendError('Invalid JSON input', 400);
|
|
}
|
|
|
|
if (empty($input['url']) || empty($input['events'])) {
|
|
$this->sendError('URL and events are required', 400);
|
|
}
|
|
|
|
if (!filter_var($input['url'], FILTER_VALIDATE_URL)) {
|
|
$this->sendError('Invalid URL', 400);
|
|
}
|
|
|
|
try {
|
|
// Webhook erstellen
|
|
$stmt = $this->conn->prepare('
|
|
INSERT INTO ws_webhook (url, events, secret, active, created_at)
|
|
VALUES (?, ?, ?, 1, NOW())
|
|
');
|
|
|
|
$secret = bin2hex(random_bytes(32));
|
|
|
|
$stmt->execute([
|
|
$input['url'],
|
|
json_encode($input['events']),
|
|
$secret
|
|
]);
|
|
|
|
$webhookId = $this->conn->lastInsertId();
|
|
|
|
$this->sendResponse([
|
|
'success' => true,
|
|
'data' => [
|
|
'webhook_id' => $webhookId,
|
|
'secret' => $secret
|
|
],
|
|
'message' => 'Webhook registered successfully'
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
$this->sendError('Failed to register webhook: ' . $e->getMessage(), 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Webhook-Liste abrufen
|
|
*/
|
|
public function getWebhooks()
|
|
{
|
|
try {
|
|
$stmt = $this->conn->prepare('
|
|
SELECT id, url, events, active, created_at, last_triggered
|
|
FROM ws_webhook
|
|
ORDER BY created_at DESC
|
|
');
|
|
$stmt->execute();
|
|
$webhooks = $stmt->fetchAllAssociative();
|
|
|
|
$this->sendResponse([
|
|
'success' => true,
|
|
'data' => $webhooks
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
$this->sendError('Failed to fetch webhooks: ' . $e->getMessage(), 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Webhook aktualisieren
|
|
*/
|
|
public function updateWebhook($webhookId)
|
|
{
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'PUT') {
|
|
$this->sendError('Method not allowed', 405);
|
|
}
|
|
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
|
|
if (!$input) {
|
|
$this->sendError('Invalid JSON input', 400);
|
|
}
|
|
|
|
try {
|
|
$updateFields = [];
|
|
$params = [];
|
|
|
|
if (isset($input['url'])) {
|
|
if (!filter_var($input['url'], FILTER_VALIDATE_URL)) {
|
|
$this->sendError('Invalid URL', 400);
|
|
}
|
|
$updateFields[] = 'url = ?';
|
|
$params[] = $input['url'];
|
|
}
|
|
|
|
if (isset($input['events'])) {
|
|
$updateFields[] = 'events = ?';
|
|
$params[] = json_encode($input['events']);
|
|
}
|
|
|
|
if (isset($input['active'])) {
|
|
$updateFields[] = 'active = ?';
|
|
$params[] = $input['active'] ? 1 : 0;
|
|
}
|
|
|
|
if (empty($updateFields)) {
|
|
$this->sendError('No fields to update', 400);
|
|
}
|
|
|
|
$updateFields[] = 'updated_at = NOW()';
|
|
$params[] = $webhookId;
|
|
|
|
$sql = 'UPDATE ws_webhook SET ' . implode(', ', $updateFields) . ' WHERE id = ?';
|
|
$stmt = $this->conn->prepare($sql);
|
|
$stmt->execute($params);
|
|
|
|
if ($stmt->rowCount() === 0) {
|
|
$this->sendError('Webhook not found', 404);
|
|
}
|
|
|
|
$this->sendResponse([
|
|
'success' => true,
|
|
'message' => 'Webhook updated successfully'
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
$this->sendError('Failed to update webhook: ' . $e->getMessage(), 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Webhook löschen
|
|
*/
|
|
public function deleteWebhook($webhookId)
|
|
{
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'DELETE') {
|
|
$this->sendError('Method not allowed', 405);
|
|
}
|
|
|
|
try {
|
|
$stmt = $this->conn->prepare('DELETE FROM ws_webhook WHERE id = ?');
|
|
$stmt->execute([$webhookId]);
|
|
|
|
if ($stmt->rowCount() === 0) {
|
|
$this->sendError('Webhook not found', 404);
|
|
}
|
|
|
|
$this->sendResponse([
|
|
'success' => true,
|
|
'message' => 'Webhook deleted successfully'
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
$this->sendError('Failed to delete webhook: ' . $e->getMessage(), 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Event auslösen
|
|
*/
|
|
public function triggerEvent($event, $data = [])
|
|
{
|
|
try {
|
|
// Aktive Webhooks für dieses Event abrufen
|
|
$stmt = $this->conn->prepare('
|
|
SELECT id, url, secret, events
|
|
FROM ws_webhook
|
|
WHERE active = 1
|
|
');
|
|
$stmt->execute();
|
|
$webhooks = $stmt->fetchAllAssociative();
|
|
|
|
$triggeredCount = 0;
|
|
$failedCount = 0;
|
|
|
|
foreach ($webhooks as $webhook) {
|
|
$events = json_decode($webhook['events'], true) ?: [];
|
|
|
|
if (in_array($event, $events) || in_array('*', $events)) {
|
|
try {
|
|
$this->sendWebhook($webhook, $event, $data);
|
|
$triggeredCount++;
|
|
|
|
// Last triggered aktualisieren
|
|
$stmt = $this->conn->prepare('
|
|
UPDATE ws_webhook
|
|
SET last_triggered = NOW()
|
|
WHERE id = ?
|
|
');
|
|
$stmt->execute([$webhook['id']]);
|
|
|
|
} catch (Exception $e) {
|
|
$failedCount++;
|
|
$this->logWebhookError($webhook['id'], $event, $e->getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
return [
|
|
'triggered' => $triggeredCount,
|
|
'failed' => $failedCount
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
throw new Exception('Failed to trigger event: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Webhook senden
|
|
*/
|
|
private function sendWebhook($webhook, $event, $data)
|
|
{
|
|
$payload = [
|
|
'event' => $event,
|
|
'timestamp' => time(),
|
|
'data' => $data
|
|
];
|
|
|
|
$jsonPayload = json_encode($payload);
|
|
$signature = hash_hmac('sha256', $jsonPayload, $webhook['secret']);
|
|
|
|
$headers = [
|
|
'Content-Type: application/json',
|
|
'Content-Length: ' . strlen($jsonPayload),
|
|
'X-Webhook-Signature: ' . $signature,
|
|
'User-Agent: Webshop-System/1.0'
|
|
];
|
|
|
|
$context = stream_context_create([
|
|
'http' => [
|
|
'method' => 'POST',
|
|
'header' => implode("\r\n", $headers),
|
|
'content' => $jsonPayload,
|
|
'timeout' => 30
|
|
]
|
|
]);
|
|
|
|
$response = file_get_contents($webhook['url'], false, $context);
|
|
|
|
if ($response === false) {
|
|
throw new Exception('Failed to send webhook to ' . $webhook['url']);
|
|
}
|
|
|
|
$httpCode = $this->getHttpResponseCode($http_response_header);
|
|
|
|
if ($httpCode < 200 || $httpCode >= 300) {
|
|
throw new Exception('Webhook returned HTTP ' . $httpCode);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* HTTP-Response-Code extrahieren
|
|
*/
|
|
private function getHttpResponseCode($headers)
|
|
{
|
|
if (empty($headers)) {
|
|
return 0;
|
|
}
|
|
|
|
$statusLine = $headers[0];
|
|
preg_match('/HTTP\/\d\.\d\s+(\d+)/', $statusLine, $matches);
|
|
|
|
return isset($matches[1]) ? intval($matches[1]) : 0;
|
|
}
|
|
|
|
/**
|
|
* Webhook-Fehler loggen
|
|
*/
|
|
private function logWebhookError($webhookId, $event, $error)
|
|
{
|
|
try {
|
|
$stmt = $this->conn->prepare('
|
|
INSERT INTO ws_webhook_log (webhook_id, event, error, created_at)
|
|
VALUES (?, ?, ?, NOW())
|
|
');
|
|
$stmt->execute([$webhookId, $event, $error]);
|
|
} catch (Exception $e) {
|
|
// Ignore logging errors
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Webhook-Logs abrufen
|
|
*/
|
|
public function getWebhookLogs($webhookId = null)
|
|
{
|
|
try {
|
|
if ($webhookId) {
|
|
$stmt = $this->conn->prepare('
|
|
SELECT * FROM ws_webhook_log
|
|
WHERE webhook_id = ?
|
|
ORDER BY created_at DESC
|
|
LIMIT 100
|
|
');
|
|
$stmt->execute([$webhookId]);
|
|
} else {
|
|
$stmt = $this->conn->prepare('
|
|
SELECT wl.*, w.url
|
|
FROM ws_webhook_log wl
|
|
LEFT JOIN ws_webhook w ON wl.webhook_id = w.id
|
|
ORDER BY wl.created_at DESC
|
|
LIMIT 100
|
|
');
|
|
$stmt->execute();
|
|
}
|
|
|
|
$logs = $stmt->fetchAllAssociative();
|
|
|
|
$this->sendResponse([
|
|
'success' => true,
|
|
'data' => $logs
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
$this->sendError('Failed to fetch webhook logs: ' . $e->getMessage(), 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Event-Test
|
|
*/
|
|
public function testWebhook($webhookId)
|
|
{
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
$this->sendError('Method not allowed', 405);
|
|
}
|
|
|
|
try {
|
|
$stmt = $this->conn->prepare('
|
|
SELECT * FROM ws_webhook
|
|
WHERE id = ? AND active = 1
|
|
');
|
|
$stmt->execute([$webhookId]);
|
|
$webhook = $stmt->fetchAssociative();
|
|
|
|
if (!$webhook) {
|
|
$this->sendError('Webhook not found', 404);
|
|
}
|
|
|
|
$testData = [
|
|
'test' => true,
|
|
'message' => 'This is a test webhook',
|
|
'timestamp' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
$this->sendWebhook($webhook, 'test', $testData);
|
|
|
|
$this->sendResponse([
|
|
'success' => true,
|
|
'message' => 'Test webhook sent successfully'
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
$this->sendError('Failed to send test webhook: ' . $e->getMessage(), 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verfügbare Events abrufen
|
|
*/
|
|
public function getAvailableEvents()
|
|
{
|
|
$events = [
|
|
'order.created' => 'Bestellung erstellt',
|
|
'order.updated' => 'Bestellung aktualisiert',
|
|
'order.cancelled' => 'Bestellung storniert',
|
|
'order.shipped' => 'Bestellung versendet',
|
|
'order.delivered' => 'Bestellung geliefert',
|
|
'customer.registered' => 'Kunde registriert',
|
|
'customer.updated' => 'Kunde aktualisiert',
|
|
'product.created' => 'Produkt erstellt',
|
|
'product.updated' => 'Produkt aktualisiert',
|
|
'product.deleted' => 'Produkt gelöscht',
|
|
'review.created' => 'Bewertung erstellt',
|
|
'review.updated' => 'Bewertung aktualisiert',
|
|
'newsletter.sent' => 'Newsletter gesendet',
|
|
'stock.low' => 'Lagerbestand niedrig',
|
|
'stock.out' => 'Lagerbestand aufgebraucht',
|
|
'payment.received' => 'Zahlung erhalten',
|
|
'payment.failed' => 'Zahlung fehlgeschlagen',
|
|
'test' => 'Test-Event'
|
|
];
|
|
|
|
$this->sendResponse([
|
|
'success' => true,
|
|
'data' => $events
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Webhook-Statistiken
|
|
*/
|
|
public function getWebhookStats()
|
|
{
|
|
try {
|
|
// Webhook-Statistiken
|
|
$stmt = $this->conn->prepare('
|
|
SELECT
|
|
COUNT(*) as total_webhooks,
|
|
COUNT(CASE WHEN active = 1 THEN 1 END) as active_webhooks,
|
|
COUNT(CASE WHEN active = 0 THEN 1 END) as inactive_webhooks
|
|
FROM ws_webhook
|
|
');
|
|
$stmt->execute();
|
|
$webhookStats = $stmt->fetchAssociative();
|
|
|
|
// Log-Statistiken
|
|
$stmt = $this->conn->prepare('
|
|
SELECT
|
|
COUNT(*) as total_logs,
|
|
COUNT(CASE WHEN error IS NOT NULL THEN 1 END) as error_logs,
|
|
COUNT(CASE WHEN error IS NULL THEN 1 END) as success_logs
|
|
FROM ws_webhook_log
|
|
');
|
|
$stmt->execute();
|
|
$logStats = $stmt->fetchAssociative();
|
|
|
|
// Event-Statistiken
|
|
$stmt = $this->conn->prepare('
|
|
SELECT event, COUNT(*) as count
|
|
FROM ws_webhook_log
|
|
GROUP BY event
|
|
ORDER BY count DESC
|
|
LIMIT 10
|
|
');
|
|
$stmt->execute();
|
|
$eventStats = $stmt->fetchAllAssociative();
|
|
|
|
$this->sendResponse([
|
|
'success' => true,
|
|
'data' => [
|
|
'webhooks' => $webhookStats,
|
|
'logs' => $logStats,
|
|
'events' => $eventStats
|
|
]
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
$this->sendError('Failed to get webhook stats: ' . $e->getMessage(), 500);
|
|
}
|
|
}
|
|
}
|