fix(security, core): Auth-Pflicht für Ingest-APIs, 500er-Ursachen beheben, Agenten-Workflow
Sicherheit
- install_db.php war ohne Authentifizierung erreichbar und setzte bei jedem
Aufruf das Admin-Passwort auf einen fest im Code stehenden Wert zurück.
Jetzt Auth-Pflicht; ein Konto wird nur bei leerer Benutzertabelle angelegt.
- Stored XSS im Bugtracker-Detail-Modal: Titel, Beschreibung, Fehlermeldung,
Stacktrace und Kommentare gingen ungefiltert durch innerHTML.
- report.php, projects.php und das Veröffentlichen von Releases verlangen jetzt
zwingend ein Token. Publish war zuvor völlig ungeschützt.
- CSRF-Token in allen Formularen, Session-Regenerierung nach Login,
Drosselung fehlgeschlagener Anmeldeversuche.
- Zugangsdaten aus der Versionskontrolle entfernt (Serverdaten.txt,
config.php, .htpasswd, deploy_config.json). Historie enthält sie weiterhin,
Rotation erforderlich (siehe docs/UPGRADE.md).
- Token-Validierung nur noch über SHA-256-Hash; expires_at wird ausgewertet.
Behobene 500er
- Audit::log() war in index.php weder eingebunden noch importiert. Jeder
Klick auf "Aktivierung freigeben" endete in einem Fatal Error.
- Derselbe benannte PDO-Platzhalter mehrfach je Statement (:id in
revokeToken/deleteToken, :q siebenfach in der Volltextsuche). Bei
EMULATE_PREPARES=false ist das nicht zulässig und warf HY093.
- Migration 005 nutzte dynamisches SQL, dessen Semikolons in String-Literalen
vom alten explode(';')-Installer als Statement-Ende gelesen wurden. Sie
schlug still fehl, wodurch push_id/target_agent/tags dauerhaft fehlten.
- Monitor-Umbenennung ohne Transaktion, verschachtelte Transaktionen im
RateLimiter.
Funktionale Korrekturen
- Der Watchdog-Evaluator fehlte vollständig: Monitor-Zustände änderten sich nur
beim Eintreffen eines Heartbeats, ein ausgefallenes System blieb dauerhaft
"up". Erster Lauf auf dem Produktivsystem: 7 von 10 Monitoren waren
tatsächlich seit über einem Tag nicht erreichbar.
- Das Feld "os" fehlte im Monitor-Dialog, wurde aber gespeichert und löschte
damit bei jedem Speichern das Betriebssystem.
- Der Resolve-Dialog existierte im HTML nicht; der Button war funktionslos.
- Versionsvergleich erfolgte lexikografisch, wodurch 1.9.0 als neuer galt
als 1.10.0.
- Schreiboperationen meldeten Erfolg auch für nicht existierende IDs.
- Post/Redirect/Get gegen doppelte Einträge beim Neuladen.
Neue Struktur
- src/bootstrap.php mit PSR-4-Autoloader ersetzt die require-Ketten.
- Core: Config, Http, Csrf, ApiAuth, Logger, Migrator, ErrorReporter.
- Migrator mit zeichenweisem SQL-Parser, dc_migrations und Baseline-Verfahren,
damit bestehende Installationen keine Beispieldaten zurückbekommen.
Agenten-Workflow
- Claim/Lease: Items werden exklusiv übernommen, damit nicht zwei Agenten am
selben Problem arbeiten. action=next holt und reserviert in einem Zug.
- Idempotenz über client_ref, Deduplizierung auch für Feature Requests,
Erkennung von Regressionen, automatische Eskalation des Schweregrads.
- Strukturierter Code-Kontext (repo_url, commit_sha, file_path, line_no).
- Delta-Abfragen über updated_since, Pagination, Bulk-Update.
- Beim Veröffentlichen eines Releases schließen sich Items mit passendem
resolved_in_build selbst.
- Ausgehende Webhooks mit HMAC-Signatur, /api/health, /api/openapi.json.
- Unbehandelte Fehler meldet die Plattform in ihren eigenen Bugtracker.
WebUI
- Serverseitige Filterung mit Pagination statt Rendern aller Datensätze.
- Migrations-Schranke, Evaluator-Warnung, Übersicht aktiver Agenten.
Zeitstempel liegen in der Datenbank durchgängig in UTC und werden für die
Anzeige in die App-Zeitzone umgerechnet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a21536f495
commit
e7fbc85db4
@@ -1,11 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Deploymentcenter\Modules\Watchdog;
|
||||
|
||||
use Deploymentcenter\Core\Logger;
|
||||
use PDO;
|
||||
use RuntimeException;
|
||||
|
||||
class MonitorRepo
|
||||
/**
|
||||
* Monitore des Watchdog-Moduls.
|
||||
*
|
||||
* Korrekturen gegenueber der Erstfassung:
|
||||
* - updateMonitor() nimmt jetzt ein Feld-Array entgegen und aktualisiert nur
|
||||
* die tatsaechlich uebergebenen Spalten. Zuvor wurden alle Spalten fest
|
||||
* geschrieben; fehlte ein Formularfeld (z. B. "os", das es im Bearbeiten-
|
||||
* Dialog gar nicht gab), wurde die Spalte bei jedem Speichern auf NULL
|
||||
* gesetzt - und damit auch die automatische Icon-Erkennung zerstoert.
|
||||
* - Das Umbenennen einer Source laeuft in einer Transaktion. Vorher wurden
|
||||
* erst Kinder und Tokens umgehaengt und danach umbenannt; scheiterte das
|
||||
* Umbenennen am Unique-Key, zeigten die Kinder auf einen Parent, den es
|
||||
* nicht mehr gab.
|
||||
* - Beim Umbenennen wird auch das Event-Log mitgezogen, damit die Historie
|
||||
* nicht abreisst.
|
||||
*/
|
||||
final class MonitorRepo
|
||||
{
|
||||
public const STATES = ['up', 'warning', 'down', 'error', 'stopped', 'maintenance', 'unknown'];
|
||||
public const TYPES = ['heartbeat', 'host', 'hypervisor_node', 'guest'];
|
||||
|
||||
/** Spalten, die ueber updateMonitor() gesetzt werden duerfen. */
|
||||
private const UPDATABLE = [
|
||||
'source' => 'source',
|
||||
'type' => 'type',
|
||||
'group_key' => 'group_key',
|
||||
'parent_source' => 'parent_source',
|
||||
'os' => 'os',
|
||||
'notes' => 'notes',
|
||||
'url' => 'url',
|
||||
'icon' => 'icon',
|
||||
'expected_interval_sec' => 'expected_interval_sec',
|
||||
'is_muted' => 'is_muted',
|
||||
'expect_running' => 'expect_running',
|
||||
];
|
||||
|
||||
private PDO $db;
|
||||
|
||||
public function __construct(PDO $db)
|
||||
@@ -13,20 +51,27 @@ class MonitorRepo
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/** @return list<array<string,mixed>> */
|
||||
public function getAllMonitors(): array
|
||||
{
|
||||
$stmt = $this->db->query('SELECT * FROM watchdog_monitors ORDER BY group_key ASC, source ASC');
|
||||
$stmt = $this->db->query('
|
||||
SELECT * FROM watchdog_monitors
|
||||
ORDER BY COALESCE(group_key, "zzz") ASC, source ASC
|
||||
');
|
||||
return $stmt->fetchAll() ?: [];
|
||||
}
|
||||
|
||||
public function getMonitor(string $source, string $instance = 'default'): ?array
|
||||
{
|
||||
$stmt = $this->db->prepare('SELECT * FROM watchdog_monitors WHERE source = :s AND instance = :i');
|
||||
$stmt->execute([':s' => $source, ':i' => $instance]);
|
||||
$stmt = $this->db->prepare('SELECT * FROM watchdog_monitors WHERE source = :source AND instance = :instance');
|
||||
$stmt->execute([':source' => $source, ':instance' => $instance]);
|
||||
$row = $stmt->fetch();
|
||||
return $row ?: null;
|
||||
return is_array($row) ? $row : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nimmt einen Heartbeat entgegen und legt den Monitor bei Bedarf an.
|
||||
*/
|
||||
public function upsertHeartbeat(
|
||||
string $source,
|
||||
string $instance,
|
||||
@@ -38,27 +83,47 @@ class MonitorRepo
|
||||
?string $groupKey = null,
|
||||
?string $os = null
|
||||
): array {
|
||||
$metricsJson = is_array($metrics) || is_object($metrics) ? json_encode($metrics, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) : null;
|
||||
$state = ($status === 'ok') ? 'up' : (($status === 'warning') ? 'warning' : 'down');
|
||||
$metricsJson = (is_array($metrics) || is_object($metrics))
|
||||
? json_encode($metrics, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
|
||||
: null;
|
||||
|
||||
$state = match ($status) {
|
||||
'ok' => 'up',
|
||||
'warning' => 'warning',
|
||||
default => 'down',
|
||||
};
|
||||
|
||||
$previous = $this->getMonitor($source, $instance);
|
||||
$previousState = $previous !== null ? (string)$previous['state'] : null;
|
||||
|
||||
$type = in_array($type, self::TYPES, true) ? $type : 'heartbeat';
|
||||
$intervalSec = max(10, min($intervalSec, 86400));
|
||||
|
||||
$stmt = $this->db->prepare('
|
||||
INSERT INTO watchdog_monitors (
|
||||
source, instance, type, state, expected_interval_sec, last_seen_utc,
|
||||
last_status, last_message, metrics_json, group_key, os, created_utc, updated_utc
|
||||
source, instance, type, state, last_state_change_utc, expected_interval_sec,
|
||||
last_seen_utc, last_status, last_message, metrics_json, group_key, os,
|
||||
created_utc, updated_utc
|
||||
) VALUES (
|
||||
:source, :instance, :type, :state, :interval, NOW(),
|
||||
:last_status, :message, :metrics, :group_key, :os, NOW(), NOW()
|
||||
:source, :instance, :type, :state, UTC_TIMESTAMP(), :interval,
|
||||
UTC_TIMESTAMP(), :last_status, :message, :metrics, :group_key, :os,
|
||||
UTC_TIMESTAMP(), UTC_TIMESTAMP()
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
state = VALUES(state),
|
||||
-- Reihenfolge ist relevant: MySQL wertet die Zuweisungen von
|
||||
-- links nach rechts aus. last_state_change_utc muss den alten
|
||||
-- Wert von state sehen, also vor dessen Zuweisung stehen.
|
||||
last_state_change_utc = IF(state <> VALUES(state), UTC_TIMESTAMP(), last_state_change_utc),
|
||||
state = VALUES(state),
|
||||
down_since_utc = IF(VALUES(state) = "up", NULL, down_since_utc),
|
||||
expected_interval_sec = VALUES(expected_interval_sec),
|
||||
last_seen_utc = VALUES(last_seen_utc),
|
||||
last_status = VALUES(last_status),
|
||||
last_message = VALUES(last_message),
|
||||
metrics_json = VALUES(metrics_json),
|
||||
group_key = COALESCE(VALUES(group_key), group_key),
|
||||
os = COALESCE(VALUES(os), os),
|
||||
updated_utc = VALUES(updated_utc)
|
||||
last_seen_utc = VALUES(last_seen_utc),
|
||||
last_status = VALUES(last_status),
|
||||
last_message = VALUES(last_message),
|
||||
metrics_json = VALUES(metrics_json),
|
||||
group_key = COALESCE(VALUES(group_key), group_key),
|
||||
os = COALESCE(VALUES(os), os),
|
||||
updated_utc = VALUES(updated_utc)
|
||||
');
|
||||
|
||||
$stmt->execute([
|
||||
@@ -67,101 +132,305 @@ class MonitorRepo
|
||||
':type' => $type,
|
||||
':state' => $state,
|
||||
':interval' => $intervalSec,
|
||||
':last_status' => $status,
|
||||
':last_status' => in_array($status, ['ok', 'warning', 'error'], true) ? $status : 'error',
|
||||
':message' => $message,
|
||||
':metrics' => $metricsJson,
|
||||
':group_key' => $groupKey,
|
||||
':os' => $os,
|
||||
]);
|
||||
|
||||
return $this->getMonitor($source, $instance);
|
||||
}
|
||||
|
||||
public function updateMonitor(
|
||||
string $oldSource,
|
||||
string $newSource,
|
||||
string $instance,
|
||||
?string $type,
|
||||
?string $groupKey,
|
||||
?string $parentSource,
|
||||
?string $os,
|
||||
?string $notes,
|
||||
?string $url,
|
||||
?int $intervalSec,
|
||||
?string $icon = null,
|
||||
bool $isMuted = false
|
||||
): bool {
|
||||
// Cascade source renaming to children & agent tokens
|
||||
if ($oldSource !== $newSource) {
|
||||
$this->db->prepare('UPDATE watchdog_monitors SET parent_source = :new WHERE parent_source = :old')
|
||||
->execute([':new' => $newSource, ':old' => $oldSource]);
|
||||
$this->db->prepare('UPDATE watchdog_agent_tokens SET monitor_source = :new WHERE monitor_source = :old')
|
||||
->execute([':new' => $newSource, ':old' => $oldSource]);
|
||||
$monitor = $this->getMonitor($source, $instance);
|
||||
if ($monitor === null) {
|
||||
throw new RuntimeException('Monitor konnte nicht gespeichert werden: ' . $source);
|
||||
}
|
||||
|
||||
$monitor['_previous_state'] = $previousState;
|
||||
$monitor['_state_changed'] = $previousState !== null && $previousState !== $state;
|
||||
|
||||
return $monitor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legt einen Monitor manuell an (ohne Heartbeat).
|
||||
*
|
||||
* @param array<string,mixed> $fields
|
||||
*/
|
||||
public function createMonitor(string $source, array $fields = []): array
|
||||
{
|
||||
$stmt = $this->db->prepare('
|
||||
UPDATE watchdog_monitors SET
|
||||
source = :new_source,
|
||||
type = COALESCE(:type, type),
|
||||
group_key = :group_key,
|
||||
parent_source = :parent_source,
|
||||
os = :os,
|
||||
notes = :notes,
|
||||
url = :url,
|
||||
expected_interval_sec = COALESCE(:interval, expected_interval_sec),
|
||||
icon = COALESCE(:icon, icon),
|
||||
is_muted = :is_muted,
|
||||
updated_utc = NOW()
|
||||
WHERE source = :old_source AND instance = :instance
|
||||
INSERT INTO watchdog_monitors (
|
||||
source, instance, type, state, expected_interval_sec,
|
||||
group_key, parent_source, os, created_utc, updated_utc
|
||||
) VALUES (
|
||||
:source, "default", :type, "unknown", :interval,
|
||||
:group_key, :parent_source, :os, UTC_TIMESTAMP(), UTC_TIMESTAMP()
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
expected_interval_sec = VALUES(expected_interval_sec),
|
||||
group_key = VALUES(group_key),
|
||||
parent_source = VALUES(parent_source),
|
||||
os = COALESCE(VALUES(os), os),
|
||||
updated_utc = UTC_TIMESTAMP()
|
||||
');
|
||||
|
||||
return $stmt->execute([
|
||||
':new_source' => $newSource,
|
||||
':type' => !empty($type) ? $type : null,
|
||||
':group_key' => !empty($groupKey) ? $groupKey : null,
|
||||
':parent_source' => !empty($parentSource) ? $parentSource : null,
|
||||
':os' => !empty($os) ? $os : null,
|
||||
':notes' => !empty($notes) ? $notes : null,
|
||||
':url' => !empty($url) ? $url : null,
|
||||
':interval' => $intervalSec,
|
||||
':icon' => !empty($icon) ? $icon : null,
|
||||
':is_muted' => $isMuted ? 1 : 0,
|
||||
':old_source' => $oldSource,
|
||||
':instance' => $instance,
|
||||
$type = (string)($fields['type'] ?? 'heartbeat');
|
||||
|
||||
$stmt->execute([
|
||||
':source' => $source,
|
||||
':type' => in_array($type, self::TYPES, true) ? $type : 'heartbeat',
|
||||
':interval' => max(10, min((int)($fields['expected_interval_sec'] ?? 60), 86400)),
|
||||
':group_key' => self::nullIfEmpty($fields['group_key'] ?? null),
|
||||
':parent_source' => self::nullIfEmpty($fields['parent_source'] ?? null),
|
||||
':os' => self::nullIfEmpty($fields['os'] ?? null),
|
||||
]);
|
||||
|
||||
return $this->getMonitor($source) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert einen Monitor. Es werden ausschliesslich die in $fields
|
||||
* enthaltenen Spalten geschrieben - alles andere bleibt unangetastet.
|
||||
*
|
||||
* @param array<string,mixed> $fields
|
||||
*/
|
||||
public function updateMonitor(string $oldSource, string $instance, array $fields): bool
|
||||
{
|
||||
$monitor = $this->getMonitor($oldSource, $instance);
|
||||
if ($monitor === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$newSource = isset($fields['source']) ? trim((string)$fields['source']) : $oldSource;
|
||||
if ($newSource === '') {
|
||||
$newSource = $oldSource;
|
||||
}
|
||||
|
||||
$renaming = $newSource !== $oldSource;
|
||||
|
||||
if ($renaming) {
|
||||
$conflict = $this->getMonitor($newSource, $instance);
|
||||
if ($conflict !== null) {
|
||||
throw new RuntimeException(
|
||||
sprintf('Ein Monitor namens "%s" existiert bereits.', $newSource)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$set = [];
|
||||
$params = [':old_source' => $oldSource, ':instance' => $instance];
|
||||
|
||||
foreach (self::UPDATABLE as $key => $column) {
|
||||
if (!array_key_exists($key, $fields)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $fields[$key];
|
||||
|
||||
if ($column === 'expected_interval_sec') {
|
||||
$value = max(10, min((int)$value, 86400));
|
||||
} elseif ($column === 'is_muted' || $column === 'expect_running') {
|
||||
$value = !empty($value) ? 1 : 0;
|
||||
} elseif ($column === 'type') {
|
||||
if (!in_array((string)$value, self::TYPES, true)) {
|
||||
continue;
|
||||
}
|
||||
} elseif ($column === 'source') {
|
||||
$value = $newSource;
|
||||
} else {
|
||||
$value = self::nullIfEmpty($value);
|
||||
}
|
||||
|
||||
$set[] = $column . ' = :f_' . $key;
|
||||
$params[':f_' . $key] = $value;
|
||||
}
|
||||
|
||||
if ($set === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$set[] = 'updated_utc = UTC_TIMESTAMP()';
|
||||
|
||||
// Umbenennung und alle abhaengigen Aktualisierungen als eine Einheit.
|
||||
$ownTransaction = !$this->db->inTransaction();
|
||||
if ($ownTransaction) {
|
||||
$this->db->beginTransaction();
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $this->db->prepare(
|
||||
'UPDATE watchdog_monitors SET ' . implode(', ', $set)
|
||||
. ' WHERE source = :old_source AND instance = :instance'
|
||||
);
|
||||
$stmt->execute($params);
|
||||
|
||||
if ($renaming) {
|
||||
$this->cascadeRename($oldSource, $newSource);
|
||||
}
|
||||
|
||||
if ($ownTransaction) {
|
||||
$this->db->commit();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
if ($ownTransaction && $this->db->inTransaction()) {
|
||||
$this->db->rollBack();
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Zieht Kinder, Agent-Tokens und Event-Log auf den neuen Namen um. */
|
||||
private function cascadeRename(string $oldSource, string $newSource): void
|
||||
{
|
||||
$updates = [
|
||||
'UPDATE watchdog_monitors SET parent_source = :new WHERE parent_source = :old',
|
||||
'UPDATE watchdog_agent_tokens SET monitor_source = :new WHERE monitor_source = :old',
|
||||
'UPDATE watchdog_event_log SET source = :new WHERE source = :old',
|
||||
];
|
||||
|
||||
foreach ($updates as $sql) {
|
||||
$this->db->prepare($sql)->execute([':new' => $newSource, ':old' => $oldSource]);
|
||||
}
|
||||
|
||||
Logger::info('Monitor umbenannt', ['from' => $oldSource, 'to' => $newSource]);
|
||||
}
|
||||
|
||||
public function setParentSource(string $source, ?string $parentSource, string $instance = 'default'): bool
|
||||
{
|
||||
$stmt = $this->db->prepare('UPDATE watchdog_monitors SET parent_source = :parent, updated_utc = NOW() WHERE source = :s AND instance = :i');
|
||||
return $stmt->execute([':parent' => !empty($parentSource) ? $parentSource : null, ':s' => $source, ':i' => $instance]);
|
||||
// Ein Monitor darf nicht sein eigener Parent sein.
|
||||
if ($parentSource !== null && trim($parentSource) === $source) {
|
||||
$parentSource = null;
|
||||
}
|
||||
|
||||
$stmt = $this->db->prepare('
|
||||
UPDATE watchdog_monitors
|
||||
SET parent_source = :parent, updated_utc = UTC_TIMESTAMP()
|
||||
WHERE source = :source AND instance = :instance
|
||||
');
|
||||
$stmt->execute([
|
||||
':parent' => self::nullIfEmpty($parentSource),
|
||||
':source' => $source,
|
||||
':instance' => $instance,
|
||||
]);
|
||||
|
||||
return $stmt->rowCount() > 0;
|
||||
}
|
||||
|
||||
public function updateState(int $id, string $state, ?string $reason = null): bool
|
||||
/**
|
||||
* Setzt den Zustand eines Monitors und vermerkt den Wechselzeitpunkt.
|
||||
*/
|
||||
public function setState(int $id, string $state, ?string $reason = null): bool
|
||||
{
|
||||
$stmt = $this->db->prepare('UPDATE watchdog_monitors SET state = :state, metric_state_reason = :reason, updated_utc = NOW() WHERE id = :id');
|
||||
return $stmt->execute([':state' => $state, ':reason' => $reason, ':id' => $id]);
|
||||
if (!in_array($state, self::STATES, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$isDown = in_array($state, ['down', 'error'], true);
|
||||
$isUp = $state === 'up';
|
||||
|
||||
// Reihenfolge beachten: last_state_change_utc und down_since_utc muessen
|
||||
// den alten Wert von state sehen, stehen daher vor dessen Zuweisung.
|
||||
$stmt = $this->db->prepare('
|
||||
UPDATE watchdog_monitors
|
||||
SET last_state_change_utc = IF(state <> :state_cmp, UTC_TIMESTAMP(), last_state_change_utc),
|
||||
down_since_utc = CASE
|
||||
WHEN :is_down = 1 AND down_since_utc IS NULL THEN UTC_TIMESTAMP()
|
||||
WHEN :is_up = 1 THEN NULL
|
||||
ELSE down_since_utc
|
||||
END,
|
||||
state = :state,
|
||||
metric_state_reason = :reason,
|
||||
updated_utc = UTC_TIMESTAMP()
|
||||
WHERE id = :id
|
||||
');
|
||||
|
||||
$stmt->execute([
|
||||
':state_cmp' => $state,
|
||||
':is_down' => $isDown ? 1 : 0,
|
||||
':is_up' => $isUp ? 1 : 0,
|
||||
':state' => $state,
|
||||
':reason' => $reason,
|
||||
':id' => $id,
|
||||
]);
|
||||
|
||||
return $stmt->rowCount() > 0;
|
||||
}
|
||||
|
||||
public function setMaintenance(string $source, string $instance, ?string $untilUtc): bool
|
||||
{
|
||||
$state = $untilUtc ? 'maintenance' : 'up';
|
||||
$stmt = $this->db->prepare('UPDATE watchdog_monitors SET state = :state, suppress_until_utc = :until, updated_utc = NOW() WHERE source = :s AND instance = :i');
|
||||
return $stmt->execute([':state' => $state, ':until' => $untilUtc, ':s' => $source, ':i' => $instance]);
|
||||
$stmt = $this->db->prepare('
|
||||
UPDATE watchdog_monitors
|
||||
SET state = :state,
|
||||
suppress_until_utc = :until,
|
||||
updated_utc = UTC_TIMESTAMP()
|
||||
WHERE source = :source AND instance = :instance
|
||||
');
|
||||
$stmt->execute([
|
||||
':state' => $untilUtc !== null ? 'maintenance' : 'unknown',
|
||||
':until' => $untilUtc,
|
||||
':source' => $source,
|
||||
':instance' => $instance,
|
||||
]);
|
||||
|
||||
return $stmt->rowCount() > 0;
|
||||
}
|
||||
|
||||
public function deleteMonitor(string $source, string $instance = 'default'): bool
|
||||
{
|
||||
// 1. Unlink any children
|
||||
$this->db->prepare('UPDATE watchdog_monitors SET parent_source = NULL WHERE parent_source = :s')
|
||||
->execute([':s' => $source]);
|
||||
$ownTransaction = !$this->db->inTransaction();
|
||||
if ($ownTransaction) {
|
||||
$this->db->beginTransaction();
|
||||
}
|
||||
|
||||
// 2. Revoke agent tokens for this monitor
|
||||
$this->db->prepare('UPDATE watchdog_agent_tokens SET revoked = 1 WHERE monitor_source = :s')
|
||||
->execute([':s' => $source]);
|
||||
try {
|
||||
// Kinder auf Top-Level heben, damit keine Waisen entstehen.
|
||||
$this->db->prepare('UPDATE watchdog_monitors SET parent_source = NULL WHERE parent_source = :source')
|
||||
->execute([':source' => $source]);
|
||||
|
||||
// 3. Delete monitor
|
||||
$stmt = $this->db->prepare('DELETE FROM watchdog_monitors WHERE source = :s AND instance = :i');
|
||||
return $stmt->execute([':s' => $source, ':i' => $instance]);
|
||||
// Zugehoerige Agent-Tokens entwerten.
|
||||
$this->db->prepare('UPDATE watchdog_agent_tokens SET revoked = 1 WHERE monitor_source = :source')
|
||||
->execute([':source' => $source]);
|
||||
|
||||
$stmt = $this->db->prepare('DELETE FROM watchdog_monitors WHERE source = :source AND instance = :instance');
|
||||
$stmt->execute([':source' => $source, ':instance' => $instance]);
|
||||
$deleted = $stmt->rowCount() > 0;
|
||||
|
||||
if ($ownTransaction) {
|
||||
$this->db->commit();
|
||||
}
|
||||
|
||||
return $deleted;
|
||||
} catch (\Throwable $e) {
|
||||
if ($ownTransaction && $this->db->inTransaction()) {
|
||||
$this->db->rollBack();
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitore, die der Evaluator pruefen muss.
|
||||
*
|
||||
* @return list<array<string,mixed>>
|
||||
*/
|
||||
public function getEvaluationCandidates(): array
|
||||
{
|
||||
$stmt = $this->db->query('
|
||||
SELECT * FROM watchdog_monitors
|
||||
WHERE state <> "maintenance"
|
||||
AND (suppress_until_utc IS NULL OR suppress_until_utc < UTC_TIMESTAMP())
|
||||
');
|
||||
return $stmt->fetchAll() ?: [];
|
||||
}
|
||||
|
||||
/** @param mixed $value */
|
||||
private static function nullIfEmpty($value): ?string
|
||||
{
|
||||
if ($value === null || is_array($value) || is_object($value)) {
|
||||
return null;
|
||||
}
|
||||
$value = trim((string)$value);
|
||||
return $value === '' ? null : $value;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user