Primer commit
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* KAPVOE Stripe Checkout helpers
|
||||
* Plain PHP + cURL. No Composer dependency required.
|
||||
*/
|
||||
|
||||
function kapvoe_load_config(): array {
|
||||
$configFile = __DIR__ . '/config.php';
|
||||
if (!file_exists($configFile)) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'error' => 'Falta el fitxer config.php'
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
/** @var array $config */
|
||||
$config = require $configFile;
|
||||
return $config;
|
||||
}
|
||||
|
||||
function kapvoe_json_input(): array {
|
||||
$raw = file_get_contents('php://input') ?: '{}';
|
||||
$decoded = json_decode($raw, true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
function kapvoe_json_response(array $payload, int $status = 200): never {
|
||||
http_response_code($status);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
function kapvoe_require_post(): void {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
kapvoe_json_response(['ok' => false, 'error' => 'Mètode no permès'], 405);
|
||||
}
|
||||
}
|
||||
|
||||
function kapvoe_validate_required(array $data, array $fields): array {
|
||||
$errors = [];
|
||||
foreach ($fields as $field) {
|
||||
if (!isset($data[$field]) || trim((string)$data[$field]) === '') {
|
||||
$errors[$field] = 'Camp obligatori';
|
||||
}
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
|
||||
function kapvoe_orders_csv_path(array $config): string {
|
||||
$dir = rtrim((string)$config['orders_storage_dir'], DIRECTORY_SEPARATOR);
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0775, true);
|
||||
}
|
||||
return $dir . DIRECTORY_SEPARATOR . 'orders.csv';
|
||||
}
|
||||
|
||||
function kapvoe_append_order(array $config, array $order): void {
|
||||
$path = kapvoe_orders_csv_path($config);
|
||||
$exists = file_exists($path);
|
||||
$fh = fopen($path, 'ab');
|
||||
if (!$fh) {
|
||||
throw new RuntimeException('No s\'ha pogut obrir orders.csv');
|
||||
}
|
||||
|
||||
if (!$exists) {
|
||||
fputcsv($fh, array_keys($order), ';');
|
||||
}
|
||||
fputcsv($fh, array_values($order), ';');
|
||||
fclose($fh);
|
||||
}
|
||||
|
||||
function kapvoe_update_order_status(array $config, string $sessionId, string $newStatus, string $paymentIntent = ''): void {
|
||||
$path = kapvoe_orders_csv_path($config);
|
||||
if (!file_exists($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
$fh = fopen($path, 'rb');
|
||||
if (!$fh) {
|
||||
return;
|
||||
}
|
||||
while (($row = fgetcsv($fh, 0, ';')) !== false) {
|
||||
$rows[] = $row;
|
||||
}
|
||||
fclose($fh);
|
||||
|
||||
if (count($rows) < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
$header = $rows[0];
|
||||
$sessionIdx = array_search('stripe_session_id', $header, true);
|
||||
$statusIdx = array_search('payment_status', $header, true);
|
||||
$intentIdx = array_search('payment_intent_id', $header, true);
|
||||
|
||||
if ($sessionIdx === false || $statusIdx === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
for ($i = 1; $i < count($rows); $i++) {
|
||||
while (count($rows[$i]) < count($header)) {
|
||||
$rows[$i][] = '';
|
||||
}
|
||||
|
||||
if (($rows[$i][$sessionIdx] ?? '') === $sessionId) {
|
||||
$rows[$i][$statusIdx] = $newStatus;
|
||||
if ($intentIdx !== false) {
|
||||
$rows[$i][$intentIdx] = $paymentIntent;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$fh = fopen($path, 'wb');
|
||||
if (!$fh) {
|
||||
return;
|
||||
}
|
||||
foreach ($rows as $row) {
|
||||
while (count($row) < count($header)) {
|
||||
$row[] = '';
|
||||
}
|
||||
fputcsv($fh, $row, ';');
|
||||
}
|
||||
fclose($fh);
|
||||
}
|
||||
|
||||
function kapvoe_get_order_by_session_id(array $config, string $sessionId): ?array {
|
||||
$path = kapvoe_orders_csv_path($config);
|
||||
if (!file_exists($path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fh = fopen($path, 'rb');
|
||||
if (!$fh) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$header = fgetcsv($fh, 0, ';');
|
||||
if (!$header) {
|
||||
fclose($fh);
|
||||
return null;
|
||||
}
|
||||
|
||||
while (($row = fgetcsv($fh, 0, ';')) !== false) {
|
||||
while (count($row) < count($header)) {
|
||||
$row[] = '';
|
||||
}
|
||||
$assoc = array_combine($header, $row);
|
||||
if (($assoc['stripe_session_id'] ?? '') === $sessionId) {
|
||||
fclose($fh);
|
||||
return $assoc ?: null;
|
||||
}
|
||||
}
|
||||
|
||||
fclose($fh);
|
||||
return null;
|
||||
}
|
||||
|
||||
function kapvoe_mark_order_stock_updated(array $config, string $sessionId): void {
|
||||
$path = kapvoe_orders_csv_path($config);
|
||||
if (!file_exists($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
$fh = fopen($path, 'rb');
|
||||
if (!$fh) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (($row = fgetcsv($fh, 0, ';')) !== false) {
|
||||
$rows[] = $row;
|
||||
}
|
||||
fclose($fh);
|
||||
|
||||
if (count($rows) < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$header = $rows[0];
|
||||
|
||||
$sessionIdx = array_search('stripe_session_id', $header, true);
|
||||
$stockUpdatedIdx = array_search('stock_updated', $header, true);
|
||||
$stockUpdatedAtIdx = array_search('stock_updated_at', $header, true);
|
||||
$webhookProcessedIdx = array_search('webhook_processed_at', $header, true);
|
||||
|
||||
if ($sessionIdx === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($stockUpdatedIdx === false) {
|
||||
$header[] = 'stock_updated';
|
||||
$stockUpdatedIdx = count($header) - 1;
|
||||
}
|
||||
|
||||
if ($stockUpdatedAtIdx === false) {
|
||||
$header[] = 'stock_updated_at';
|
||||
$stockUpdatedAtIdx = count($header) - 1;
|
||||
}
|
||||
|
||||
if ($webhookProcessedIdx === false) {
|
||||
$header[] = 'webhook_processed_at';
|
||||
$webhookProcessedIdx = count($header) - 1;
|
||||
}
|
||||
|
||||
$rows[0] = $header;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
for ($i = 1; $i < count($rows); $i++) {
|
||||
while (count($rows[$i]) < count($header)) {
|
||||
$rows[$i][] = '';
|
||||
}
|
||||
|
||||
if (($rows[$i][$sessionIdx] ?? '') === $sessionId) {
|
||||
if (($rows[$i][$stockUpdatedIdx] ?? '') === '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
$rows[$i][$stockUpdatedIdx] = '1';
|
||||
$rows[$i][$stockUpdatedAtIdx] = $now;
|
||||
$rows[$i][$webhookProcessedIdx] = $now;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$fh = fopen($path, 'wb');
|
||||
if (!$fh) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
while (count($row) < count($header)) {
|
||||
$row[] = '';
|
||||
}
|
||||
fputcsv($fh, $row, ';');
|
||||
}
|
||||
|
||||
fclose($fh);
|
||||
}
|
||||
|
||||
function kapvoe_create_checkout_session(array $config, array $payload): array {
|
||||
$secretKey = (string)$config['stripe_secret_key'];
|
||||
|
||||
$fields = [
|
||||
'mode' => 'payment',
|
||||
'success_url' => (string)$config['success_url'] . '?session_id={CHECKOUT_SESSION_ID}',
|
||||
'cancel_url' => (string)$config['cancel_url'],
|
||||
'customer_email' => (string)$payload['email'],
|
||||
'client_reference_id' => (string)$payload['order_id'],
|
||||
'locale' => 'es',
|
||||
'payment_method_types[0]' => 'card',
|
||||
'line_items[0][price_data][currency]' => (string)$config['currency'],
|
||||
'line_items[0][price_data][product_data][name]' => (string)$payload['product_name'],
|
||||
'line_items[0][price_data][unit_amount]' => (string)$payload['unit_amount_cents'],
|
||||
'line_items[0][quantity]' => (string)$payload['quantity'],
|
||||
'metadata[order_id]' => (string)$payload['order_id'],
|
||||
'metadata[product_code]' => (string)$payload['product_code'],
|
||||
'metadata[customer_name]' => (string)$payload['customer_name'],
|
||||
'metadata[phone]' => (string)$payload['phone'],
|
||||
'metadata[postal_code]' => (string)$payload['postal_code'],
|
||||
'metadata[city]' => (string)$payload['city'],
|
||||
'metadata[province]' => (string)$payload['province'],
|
||||
];
|
||||
|
||||
$ch = curl_init('https://api.stripe.com/v1/checkout/sessions');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query($fields),
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Authorization: Bearer ' . $secretKey
|
||||
],
|
||||
]);
|
||||
|
||||
$raw = curl_exec($ch);
|
||||
if ($raw === false) {
|
||||
throw new RuntimeException('Error cURL Stripe: ' . curl_error($ch));
|
||||
}
|
||||
|
||||
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$decoded = json_decode($raw, true);
|
||||
if ($httpCode >= 400 || !is_array($decoded)) {
|
||||
throw new RuntimeException('Stripe ha retornat un error: ' . $raw);
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
function kapvoe_verify_stripe_signature(string $payload, string $header, string $secret, int $tolerance = 300): bool {
|
||||
if (!$header || !$secret) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$parts = explode(',', $header);
|
||||
$timestamp = null;
|
||||
$signature = null;
|
||||
|
||||
foreach ($parts as $part) {
|
||||
[$k, $v] = array_pad(explode('=', trim($part), 2), 2, null);
|
||||
if ($k === 't') {
|
||||
$timestamp = $v;
|
||||
}
|
||||
if ($k === 'v1') {
|
||||
$signature = $v;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$timestamp || !$signature) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (abs(time() - (int)$timestamp) > $tolerance) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$signedPayload = $timestamp . '.' . $payload;
|
||||
$expected = hash_hmac('sha256', $signedPayload, $secret);
|
||||
|
||||
return hash_equals($expected, $signature);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
// Clau secreta real de Stripe. NO la posis mai al HTML.
|
||||
'stripe_secret_key' => 'sk_test_51IvGaFHRV8rA2XwsDiGII0rVfot2HabnvF862L8G6rYIzUscZDMkM4WlVAV6GbYSMZsFEDF44PEppdJaLdGo0idB00UyabGh0u',
|
||||
|
||||
// Secret del webhook de Stripe (Signing secret)
|
||||
'stripe_webhook_secret' => 'whsec_wBJZjwPIhPclM1i4bKUtnQqol26caq5L',
|
||||
|
||||
// Moneda
|
||||
'currency' => 'eur',
|
||||
|
||||
// On guardar les comandes provisionals
|
||||
'orders_storage_dir' => __DIR__ . '/data',
|
||||
|
||||
// URLs públiques
|
||||
'success_url' => 'https://kapvoe-portfoli.treblarella.org/checkout/payment-success.php',
|
||||
'cancel_url' => 'https://kapvoe-portfoli.treblarella.org/checkout/payment-cancel.php',
|
||||
];
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/common.php';
|
||||
|
||||
kapvoe_require_post();
|
||||
$config = kapvoe_load_config();
|
||||
$data = kapvoe_json_input();
|
||||
|
||||
$required = [
|
||||
'product_code',
|
||||
'product_name',
|
||||
'price',
|
||||
'quantity',
|
||||
'customer_name',
|
||||
'address',
|
||||
'postal_code',
|
||||
'city',
|
||||
'province',
|
||||
'phone',
|
||||
'email',
|
||||
];
|
||||
$errors = kapvoe_validate_required($data, $required);
|
||||
|
||||
if (!filter_var((string)($data['email'] ?? ''), FILTER_VALIDATE_EMAIL)) {
|
||||
$errors['email'] = 'Correu electrònic invàlid';
|
||||
}
|
||||
if (!preg_match('/^\d{5}$/', (string)($data['postal_code'] ?? ''))) {
|
||||
$errors['postal_code'] = 'Codi postal invàlid';
|
||||
}
|
||||
if (!preg_match('/^[0-9+\s]{8,20}$/', (string)($data['phone'] ?? ''))) {
|
||||
$errors['phone'] = 'Telèfon invàlid';
|
||||
}
|
||||
|
||||
$price = (float)str_replace(',', '.', (string)$data['price']);
|
||||
$quantity = max(1, (int)$data['quantity']);
|
||||
|
||||
if ($price <= 0) {
|
||||
$errors['price'] = 'Preu invàlid';
|
||||
}
|
||||
|
||||
if ($errors) {
|
||||
kapvoe_json_response(['ok' => false, 'errors' => $errors], 422);
|
||||
}
|
||||
|
||||
$orderId = 'ORD-' . date('Ymd-His') . '-' . substr(bin2hex(random_bytes(3)), 0, 6);
|
||||
$unitAmountCents = (int)round($price * 100);
|
||||
|
||||
$payload = [
|
||||
'order_id' => $orderId,
|
||||
'product_code' => trim((string)$data['product_code']),
|
||||
'product_name' => trim((string)$data['product_name']),
|
||||
'unit_amount_cents' => $unitAmountCents,
|
||||
'quantity' => $quantity,
|
||||
'customer_name' => trim((string)$data['customer_name']),
|
||||
'address' => trim((string)$data['address']),
|
||||
'postal_code' => trim((string)$data['postal_code']),
|
||||
'city' => trim((string)$data['city']),
|
||||
'province' => trim((string)$data['province']),
|
||||
'phone' => trim((string)$data['phone']),
|
||||
'email' => trim((string)$data['email']),
|
||||
];
|
||||
|
||||
try {
|
||||
$session = kapvoe_create_checkout_session($config, $payload);
|
||||
|
||||
kapvoe_append_order($config, [
|
||||
'order_id' => $orderId,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'product_code' => $payload['product_code'],
|
||||
'product_name' => $payload['product_name'],
|
||||
'unit_price' => $price,
|
||||
'quantity' => $quantity,
|
||||
'customer_name' => $payload['customer_name'],
|
||||
'address' => $payload['address'],
|
||||
'postal_code' => $payload['postal_code'],
|
||||
'city' => $payload['city'],
|
||||
'province' => $payload['province'],
|
||||
'phone' => $payload['phone'],
|
||||
'email' => $payload['email'],
|
||||
'payment_status' => 'pending',
|
||||
'stripe_session_id' => $session['id'] ?? '',
|
||||
'payment_intent_id' => '',
|
||||
]);
|
||||
|
||||
kapvoe_json_response([
|
||||
'ok' => true,
|
||||
'checkout_url' => $session['url'] ?? null,
|
||||
'order_id' => $orderId,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
kapvoe_json_response([
|
||||
'ok' => false,
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<!doctype html>
|
||||
<html lang="ca">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Pagament cancel·lat</title>
|
||||
<style>
|
||||
body{font-family:Arial,Helvetica,sans-serif;background:#081221;color:#fff;margin:0;padding:40px}
|
||||
.box{max-width:780px;margin:auto;background:#0f1d34;border:1px solid rgba(255,255,255,.12);border-radius:24px;padding:32px}
|
||||
a{color:#7dd3fc}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="box">
|
||||
<h1>Pagament cancel·lat</h1>
|
||||
<p>No s'ha completat el pagament. Pots tornar al catàleg i provar-ho de nou.</p>
|
||||
<p><a href="https://kapvoe-portfoli.treblarella.org/">Torna al catàleg</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
$sessionId = htmlspecialchars($_GET['session_id'] ?? '', ENT_QUOTES, 'UTF-8');
|
||||
?><!doctype html>
|
||||
<html lang="ca">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Pagament correcte</title>
|
||||
<style>
|
||||
body{font-family:Arial,Helvetica,sans-serif;background:#081221;color:#fff;margin:0;padding:40px}
|
||||
.box{max-width:780px;margin:auto;background:#0f1d34;border:1px solid rgba(255,255,255,.12);border-radius:24px;padding:32px}
|
||||
a{color:#7dd3fc}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="box">
|
||||
<h1>Pagament correcte</h1>
|
||||
<p>Hem rebut la teva compra. En breu revisarem la comanda i t'avisarem.</p>
|
||||
<?php if ($sessionId): ?>
|
||||
<p><strong>Stripe session:</strong> <?= $sessionId ?></p>
|
||||
<?php endif; ?>
|
||||
<p><a href="https://kapvoe-portfoli.treblarella.org/">Torna al catàleg</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/common.php';
|
||||
|
||||
$config = kapvoe_load_config();
|
||||
|
||||
$payload = file_get_contents('php://input') ?: '';
|
||||
$sigHeader = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';
|
||||
$secret = (string)($config['stripe_webhook_secret'] ?? '');
|
||||
|
||||
if (!kapvoe_verify_stripe_signature($payload, $sigHeader, $secret)) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'error' => 'Signatura Stripe invàlida'
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
$event = json_decode($payload, true);
|
||||
|
||||
if (!is_array($event)) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'error' => 'Payload JSON invàlid'
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
$type = (string)($event['type'] ?? '');
|
||||
$object = $event['data']['object'] ?? null;
|
||||
|
||||
if (!is_array($object)) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'error' => 'Event sense objecte de dades'
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
switch ($type) {
|
||||
case 'checkout.session.completed':
|
||||
$sessionId = (string)($object['id'] ?? '');
|
||||
$paymentIntentId = (string)($object['payment_intent'] ?? '');
|
||||
$paymentStatus = (string)($object['payment_status'] ?? '');
|
||||
|
||||
if ($sessionId === '') {
|
||||
throw new RuntimeException('Falta session id');
|
||||
}
|
||||
|
||||
kapvoe_update_order_status(
|
||||
$config,
|
||||
$sessionId,
|
||||
$paymentStatus === 'paid' ? 'paid' : 'pending',
|
||||
$paymentIntentId
|
||||
);
|
||||
|
||||
$order = kapvoe_get_order_by_session_id($config, $sessionId);
|
||||
|
||||
if (!$order) {
|
||||
throw new RuntimeException('No s\'ha trobat la comanda al CSV');
|
||||
}
|
||||
|
||||
$alreadyUpdated = (string)($order['stock_updated'] ?? '') === '1';
|
||||
|
||||
if ($paymentStatus === 'paid' && !$alreadyUpdated) {
|
||||
kapvoe_mark_order_stock_updated($config, $sessionId);
|
||||
$alreadyUpdated = true;
|
||||
}
|
||||
|
||||
http_response_code(200);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'handled' => $type,
|
||||
'session_id' => $sessionId,
|
||||
'payment_intent_id' => $paymentIntentId,
|
||||
'already_updated' => $alreadyUpdated
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
|
||||
default:
|
||||
http_response_code(200);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'ignored' => $type
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'error' => $e->getMessage()
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
Reference in New Issue
Block a user