feat: change log tab with filters, revision links and clear button

ChangeLog gains query()/abilities()/clear() (filters: ability, user,
result, date range - all optional and additive). LogTab/LogListTable
render the "Protokoll" admin tab: WP_List_Table with pagination, links
to the post/attachment/nav_menu edit screen by object_type, a revision
link shown only when the user may edit the revision's parent post, and
a nonce+manage_options-gated "Protokoll leeren" with a JS confirmation
and highlighted 'partial' rows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-26 04:12:13 +02:00
co-authored by Claude Opus 5.5
parent 9104989c43
commit 522f5bbd19
9 changed files with 940 additions and 0 deletions
+74
View File
@@ -96,6 +96,80 @@ class ChangeLog
return (int) $wpdb->query($wpdb->prepare("DELETE FROM {$table} WHERE created_at < %s", $cutoff));
}
/**
* Reads rows for the "Protokoll" admin tab (spec §8.3), newest first. All filters are
* optional and whitelisted by the caller (LogTab); values only ever reach the query
* through wpdb::prepare() placeholders.
*
* @param array{
* ability?: string, user_id?: int, result?: string, date_from?: string, date_to?: string
* } $filters
* @return array{rows: list<array<string, mixed>>, total: int}
*/
public function query(array $filters, int $limit, int $offset): array
{
$wpdb = $this->db();
$table = $this->tableName();
$where = ['1 = %d'];
$args = [1];
if (($filters['ability'] ?? '') !== '') {
$where[] = 'ability = %s';
$args[] = (string) $filters['ability'];
}
if ((int) ($filters['user_id'] ?? 0) > 0) {
$where[] = 'user_id = %d';
$args[] = (int) $filters['user_id'];
}
if (($filters['result'] ?? '') !== '') {
$where[] = 'result = %s';
$args[] = (string) $filters['result'];
}
if (($filters['date_from'] ?? '') !== '') {
$where[] = 'created_at >= %s';
$args[] = $filters['date_from'] . ' 00:00:00';
}
if (($filters['date_to'] ?? '') !== '') {
$where[] = 'created_at <= %s';
$args[] = $filters['date_to'] . ' 23:59:59';
}
$whereSql = implode(' AND ', $where);
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $whereSql holds one %s/%d per filter actually present.
$total = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE {$whereSql}", ...$args));
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$table} WHERE {$whereSql} ORDER BY id DESC LIMIT %d OFFSET %d",
...array_merge($args, [$limit, $offset])
),
'ARRAY_A'
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
return ['rows' => is_array($rows) ? array_values($rows) : [], 'total' => $total];
}
/** @return list<string> */
public function abilities(): array
{
$wpdb = $this->db();
$table = $this->tableName();
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- own table name.
$abilities = (array) $wpdb->get_col("SELECT DISTINCT ability FROM {$table} ORDER BY ability");
return array_values(array_map('strval', $abilities));
}
/** Deletes every row (button "Protokoll leeren", spec §8.3). Irreversible. */
public function clear(): void
{
$wpdb = $this->db();
$table = $this->tableName();
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- own table name.
$wpdb->query("DELETE FROM {$table}");
}
/**
* RequestContext::clientName() is a client-chosen string (app-password name or
* OAuth client_id) – treat it as untrusted input before it lands in the DB.