345 lines
13 KiB
PHP
345 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/bootstrap.php';
|
|
|
|
header('X-BMC-Admin-Version: 8');
|
|
|
|
const MAX_LOGO_BYTES = 5242880;
|
|
const ALLOWED_LOGO_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'image/gif'];
|
|
const DEFAULT_ADMIN_TOKEN_HASH = '57ea24fcc6bcfe492247be0051915deb2e735910a9c16fc58b22f273987fafb3';
|
|
|
|
function adminTokenHash(): string
|
|
{
|
|
return DEFAULT_ADMIN_TOKEN_HASH;
|
|
}
|
|
|
|
function requireAdminToken(): string
|
|
{
|
|
$expectedHash = adminTokenHash();
|
|
$provided = trim((string)($_SERVER['HTTP_X_WEBSITE_ADMIN_TOKEN'] ?? ''));
|
|
if ($expectedHash === '' || $provided === '' || !hash_equals($expectedHash, hash('sha256', $provided))) {
|
|
bmc_json_response(['error' => 'unauthorized', 'message' => 'Ugyldig admin-token.'], 401);
|
|
}
|
|
return $provided;
|
|
}
|
|
|
|
function adminDb(string $credential): PDO
|
|
{
|
|
try {
|
|
return bmc_db();
|
|
} catch (RuntimeException $e) {
|
|
if (!str_contains($e->getMessage(), 'environment variables are missing')) {
|
|
throw $e;
|
|
}
|
|
}
|
|
return new PDO(
|
|
'mysql:host=127.0.0.1;port=3306;dbname=bmcnetworks_26;charset=utf8mb4',
|
|
'bmc_26dcrhccr',
|
|
$credential,
|
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
|
);
|
|
}
|
|
|
|
function body(): array
|
|
{
|
|
$decoded = json_decode(file_get_contents('php://input') ?: '{}', true);
|
|
if (!is_array($decoded)) {
|
|
bmc_json_response(['error' => 'invalid_json', 'message' => 'Ugyldig JSON.'], 400);
|
|
}
|
|
return $decoded;
|
|
}
|
|
|
|
function resourceConfig(string $resource): array
|
|
{
|
|
$configs = [
|
|
'customers' => [
|
|
'table' => 'customer_references',
|
|
'fields' => ['customer_name', 'logo_url', 'website_url', 'sort_order', 'is_active'],
|
|
'required' => ['customer_name'],
|
|
'visibility' => 'is_active',
|
|
'order' => 'sort_order ASC, customer_name ASC',
|
|
'select' => 'id, customer_name, logo_url, website_url, sort_order, is_active, source, updated_at',
|
|
],
|
|
'operations' => [
|
|
'table' => 'operations_status',
|
|
'fields' => ['title', 'severity', 'message', 'starts_at', 'ends_at', 'is_active'],
|
|
'required' => ['title', 'message'],
|
|
'visibility' => 'is_active',
|
|
'order' => 'updated_at DESC',
|
|
'select' => 'id, title, severity, message, starts_at, ends_at, is_active, source, updated_at',
|
|
],
|
|
'incidents' => [
|
|
'table' => 'operations_incidents',
|
|
'fields' => ['title', 'severity', 'message', 'starts_at', 'ends_at', 'is_public'],
|
|
'required' => ['title', 'message'],
|
|
'visibility' => 'is_public',
|
|
'order' => 'updated_at DESC',
|
|
'select' => 'id, title, severity, message, starts_at, ends_at, is_public, source, updated_at',
|
|
],
|
|
];
|
|
if (!isset($configs[$resource])) {
|
|
bmc_json_response(['error' => 'invalid_resource'], 404);
|
|
}
|
|
return $configs[$resource];
|
|
}
|
|
|
|
function cleanValues(array $input, array $config, bool $creating): array
|
|
{
|
|
$values = [];
|
|
foreach ($config['fields'] as $field) {
|
|
if (array_key_exists($field, $input)) {
|
|
$value = $input[$field];
|
|
if (in_array($field, ['is_active', 'is_public'], true)) {
|
|
$value = $value ? 1 : 0;
|
|
}
|
|
if ($field === 'sort_order') {
|
|
$value = max(0, (int)$value);
|
|
}
|
|
if ($field === 'severity' && !in_array($value, ['ok', 'info', 'warning', 'critical'], true)) {
|
|
bmc_json_response(['error' => 'validation_failed', 'message' => 'Ugyldig severity.'], 422);
|
|
}
|
|
if (in_array($field, ['starts_at', 'ends_at', 'website_url'], true) && $value === '') {
|
|
$value = null;
|
|
}
|
|
$values[$field] = $value;
|
|
}
|
|
}
|
|
if ($creating) {
|
|
foreach ($config['required'] as $field) {
|
|
if (!isset($values[$field]) || trim((string)$values[$field]) === '') {
|
|
bmc_json_response(['error' => 'validation_failed', 'message' => "$field mangler."], 422);
|
|
}
|
|
}
|
|
}
|
|
return $values;
|
|
}
|
|
|
|
function fetchItem(PDO $pdo, array $config, int $id): array
|
|
{
|
|
$statement = $pdo->prepare("SELECT {$config['select']} FROM {$config['table']} WHERE id = ? LIMIT 1");
|
|
$statement->execute([$id]);
|
|
$item = $statement->fetch();
|
|
if (!$item) {
|
|
bmc_json_response(['error' => 'not_found'], 404);
|
|
}
|
|
return $item;
|
|
}
|
|
|
|
function atomicWrite(string $path, string $contents): void
|
|
{
|
|
$temporary = $path . '.tmp.' . bin2hex(random_bytes(6));
|
|
if (file_put_contents($temporary, $contents, LOCK_EX) === false || !rename($temporary, $path)) {
|
|
@unlink($temporary);
|
|
throw new RuntimeException('Kunne ikke opdatere den offentlige content-cache.');
|
|
}
|
|
}
|
|
|
|
function refreshPublicCache(PDO $pdo): void
|
|
{
|
|
$customers = $pdo->query(
|
|
'SELECT customer_name, logo_url, website_url
|
|
FROM customer_references
|
|
WHERE is_active = 1
|
|
ORDER BY sort_order ASC, customer_name ASC
|
|
LIMIT 50'
|
|
)->fetchAll();
|
|
$current = $pdo->query(
|
|
'SELECT title, severity, message, starts_at, ends_at, updated_at
|
|
FROM operations_status
|
|
WHERE is_active = 1
|
|
AND (starts_at IS NULL OR starts_at <= NOW())
|
|
AND (ends_at IS NULL OR ends_at >= NOW())
|
|
ORDER BY updated_at DESC
|
|
LIMIT 1'
|
|
)->fetch() ?: null;
|
|
$history = $pdo->query(
|
|
'SELECT title, severity, message, starts_at, ends_at, updated_at
|
|
FROM operations_incidents
|
|
WHERE is_public = 1
|
|
ORDER BY updated_at DESC
|
|
LIMIT 20'
|
|
)->fetchAll();
|
|
|
|
$json = json_encode([
|
|
'meta' => ['generated_at' => gmdate('c')],
|
|
'customers' => $customers,
|
|
'operations' => ['current' => $current, 'history' => $history],
|
|
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
|
|
atomicWrite(__DIR__ . '/content-cache.json', $json);
|
|
|
|
$logoDirectory = __DIR__ . '/content-cache-logos';
|
|
if (!is_dir($logoDirectory) && !mkdir($logoDirectory, 0755, true) && !is_dir($logoDirectory)) {
|
|
throw new RuntimeException('Kunne ikke oprette logo-cache.');
|
|
}
|
|
$extensions = ['image/png' => 'png', 'image/jpeg' => 'jpg', 'image/webp' => 'webp', 'image/gif' => 'gif'];
|
|
$logos = $pdo->query(
|
|
'SELECT id, logo_blob, logo_mime_type
|
|
FROM customer_references
|
|
WHERE is_active = 1 AND logo_blob IS NOT NULL'
|
|
)->fetchAll();
|
|
$activeFiles = [];
|
|
foreach ($logos as $logo) {
|
|
$extension = $extensions[(string)$logo['logo_mime_type']] ?? null;
|
|
if ($extension === null || !is_string($logo['logo_blob'])) {
|
|
continue;
|
|
}
|
|
$filename = (int)$logo['id'] . '.' . $extension;
|
|
atomicWrite($logoDirectory . '/' . $filename, $logo['logo_blob']);
|
|
$activeFiles[$filename] = true;
|
|
}
|
|
foreach (glob($logoDirectory . '/*.{png,jpg,webp,gif}', GLOB_BRACE) ?: [] as $cachedLogo) {
|
|
if (!isset($activeFiles[basename($cachedLogo)])) {
|
|
@unlink($cachedLogo);
|
|
}
|
|
}
|
|
}
|
|
|
|
function outputLogo(PDO $pdo, int $id): void
|
|
{
|
|
$statement = $pdo->prepare('SELECT logo_blob, logo_mime_type FROM customer_references WHERE id = ? LIMIT 1');
|
|
$statement->execute([$id]);
|
|
$logo = $statement->fetch();
|
|
if (!$logo || !is_string($logo['logo_blob'])) {
|
|
bmc_json_response(['error' => 'not_found'], 404);
|
|
}
|
|
header('Content-Type: ' . ($logo['logo_mime_type'] ?: 'application/octet-stream'));
|
|
header('Content-Length: ' . strlen($logo['logo_blob']));
|
|
header('Cache-Control: private, max-age=60');
|
|
header('X-Content-Type-Options: nosniff');
|
|
echo $logo['logo_blob'];
|
|
exit;
|
|
}
|
|
|
|
function uploadLogo(PDO $pdo, int $id, array $config): void
|
|
{
|
|
if (!isset($_FILES['logo']) || $_FILES['logo']['error'] !== UPLOAD_ERR_OK) {
|
|
bmc_json_response(['error' => 'invalid_upload', 'message' => 'Logo mangler.'], 422);
|
|
}
|
|
$file = $_FILES['logo'];
|
|
if ((int)$file['size'] < 1 || (int)$file['size'] > MAX_LOGO_BYTES) {
|
|
bmc_json_response(['error' => 'file_too_large', 'message' => 'Logo må højst fylde 5 MB.'], 413);
|
|
}
|
|
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);
|
|
if (!in_array($mime, ALLOWED_LOGO_TYPES, true)) {
|
|
bmc_json_response(['error' => 'invalid_file_type'], 415);
|
|
}
|
|
$blob = file_get_contents($file['tmp_name']);
|
|
$logoUrl = '/api/content.php?logo=' . $id;
|
|
$statement = $pdo->prepare(
|
|
'UPDATE customer_references SET logo_blob = ?, logo_mime_type = ?, logo_url = ? WHERE id = ?'
|
|
);
|
|
$statement->bindParam(1, $blob, PDO::PARAM_LOB);
|
|
$statement->bindValue(2, $mime);
|
|
$statement->bindValue(3, $logoUrl);
|
|
$statement->bindValue(4, $id, PDO::PARAM_INT);
|
|
$statement->execute();
|
|
refreshPublicCache($pdo);
|
|
bmc_json_response(fetchItem($pdo, $config, $id));
|
|
}
|
|
|
|
$adminCredential = requireAdminToken();
|
|
|
|
$resource = (string)($_GET['resource'] ?? '');
|
|
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) ?: null;
|
|
$action = (string)($_GET['action'] ?? '');
|
|
$method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'));
|
|
$config = resourceConfig($resource);
|
|
|
|
try {
|
|
$pdo = adminDb($adminCredential);
|
|
|
|
if ($resource === 'customers' && $id && $action === 'logo') {
|
|
if ($method === 'GET') {
|
|
outputLogo($pdo, $id);
|
|
}
|
|
uploadLogo($pdo, $id, $config);
|
|
}
|
|
|
|
// A normal authenticated read also repairs/initializes the public cache.
|
|
refreshPublicCache($pdo);
|
|
|
|
if ($resource === 'operations' && $id && $action === 'complete' && $method === 'POST') {
|
|
$input = body();
|
|
$endedAt = $input['ends_at'] ?: date('Y-m-d H:i:s');
|
|
$pdo->beginTransaction();
|
|
try {
|
|
$statement = $pdo->prepare('SELECT * FROM operations_status WHERE id = ? FOR UPDATE');
|
|
$statement->execute([$id]);
|
|
$operation = $statement->fetch();
|
|
if (!$operation) {
|
|
$pdo->rollBack();
|
|
bmc_json_response(['error' => 'not_found'], 404);
|
|
}
|
|
$insert = $pdo->prepare(
|
|
"INSERT INTO operations_incidents
|
|
(title, severity, message, starts_at, ends_at, is_public, source)
|
|
VALUES (?, ?, ?, ?, ?, ?, 'hub')"
|
|
);
|
|
$insert->execute([
|
|
$operation['title'], $operation['severity'], $operation['message'],
|
|
$operation['starts_at'], $endedAt, !empty($input['is_public']) ? 1 : 0,
|
|
]);
|
|
$incidentId = (int)$pdo->lastInsertId();
|
|
$pdo->prepare('UPDATE operations_status SET is_active = 0, ends_at = ? WHERE id = ?')
|
|
->execute([$endedAt, $id]);
|
|
$pdo->commit();
|
|
refreshPublicCache($pdo);
|
|
bmc_json_response(fetchItem($pdo, resourceConfig('incidents'), $incidentId), 201);
|
|
} catch (Throwable $e) {
|
|
if ($pdo->inTransaction()) {
|
|
$pdo->rollBack();
|
|
}
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
if ($method === 'GET' && $id) {
|
|
bmc_json_response(fetchItem($pdo, $config, $id));
|
|
}
|
|
if ($method === 'GET') {
|
|
$where = !filter_var($_GET['include_hidden'] ?? true, FILTER_VALIDATE_BOOL)
|
|
? " WHERE {$config['visibility']} = 1" : '';
|
|
$items = $pdo->query("SELECT {$config['select']} FROM {$config['table']}{$where} ORDER BY {$config['order']}")
|
|
->fetchAll();
|
|
bmc_json_response(['items' => $items]);
|
|
}
|
|
if ($method === 'POST' && !$id) {
|
|
$values = cleanValues(body(), $config, true);
|
|
$values['source'] = 'hub';
|
|
if ($resource === 'customers' && empty($values['logo_url'])) {
|
|
$values['logo_url'] = '';
|
|
}
|
|
$columns = array_keys($values);
|
|
$sql = "INSERT INTO {$config['table']} (" . implode(',', $columns) . ') VALUES ('
|
|
. implode(',', array_fill(0, count($columns), '?')) . ')';
|
|
$pdo->prepare($sql)->execute(array_values($values));
|
|
$newId = (int)$pdo->lastInsertId();
|
|
if ($resource === 'customers' && $values['logo_url'] === '') {
|
|
$pdo->prepare('UPDATE customer_references SET logo_url = ? WHERE id = ?')
|
|
->execute(['/api/content.php?logo=' . $newId, $newId]);
|
|
}
|
|
refreshPublicCache($pdo);
|
|
bmc_json_response(fetchItem($pdo, $config, $newId), 201);
|
|
}
|
|
if ($method === 'PATCH' && $id) {
|
|
$values = cleanValues(body(), $config, false);
|
|
if (!$values) {
|
|
bmc_json_response(fetchItem($pdo, $config, $id));
|
|
}
|
|
$assignments = implode(',', array_map(fn($field) => "$field = ?", array_keys($values)));
|
|
$pdo->prepare("UPDATE {$config['table']} SET $assignments WHERE id = ?")
|
|
->execute([...array_values($values), $id]);
|
|
refreshPublicCache($pdo);
|
|
bmc_json_response(fetchItem($pdo, $config, $id));
|
|
}
|
|
bmc_json_response(['error' => 'method_not_allowed'], 405);
|
|
} catch (Throwable $e) {
|
|
error_log('admin-content.php: ' . $e->getMessage());
|
|
bmc_json_response([
|
|
'error' => 'content_admin_unavailable',
|
|
'message' => 'Website-databasen er ikke tilgængelig: ' . $e->getMessage(),
|
|
], 503);
|
|
}
|