chore: initial snapshot of 2meet-data-optimizer v0.1.0
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
/**
|
||||
* TMDO_Export — Report export endpoints (v2.5.0 M14).
|
||||
*
|
||||
* Three export types:
|
||||
* 1. health — wp_wpdo_audit op='health_check_daily' over last N days
|
||||
* 2. snapshots — wp_wpdo_snapshots metadata (no inline_blob payload)
|
||||
* 3. monthly — wpdo_monthly_summary_history (latest + up to 12 archives)
|
||||
*
|
||||
* Triggered via admin GET:tools.php?page=wp-data-optimizer&wpdo_export=health&format=csv&days=30
|
||||
* Capability: manage_options + nonce.
|
||||
*
|
||||
* Filenames:wpdo-{type}-{site_slug}-{YYYYMMDD}.{csv|json}
|
||||
*
|
||||
* @package WP_Data_Optimizer
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stateless static export handler.
|
||||
*/
|
||||
class TMDO_Export {
|
||||
|
||||
public const NONCE = 'wpdo_export';
|
||||
|
||||
/**
|
||||
* Hook into admin_init to handle export requests.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register(): void {
|
||||
add_action( 'admin_init', array( __CLASS__, 'maybe_handle' ), 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect + serve an export request.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function maybe_handle(): void {
|
||||
if ( ! isset( $_GET['wpdo_export'] ) ) {
|
||||
return;
|
||||
}
|
||||
if ( ! TMDO_Capability::current_user_can_admin() ) {
|
||||
return;
|
||||
}
|
||||
if ( ! check_admin_referer( self::NONCE ) ) {
|
||||
return;
|
||||
}
|
||||
$type = sanitize_key( wp_unslash( (string) $_GET['wpdo_export'] ) );
|
||||
$format = sanitize_key( wp_unslash( (string) ( $_GET['format'] ?? 'csv' ) ) );
|
||||
$days = isset( $_GET['days'] ) ? max( 1, min( 365, (int) $_GET['days'] ) ) : 30;
|
||||
if ( ! in_array( $format, array( 'csv', 'json' ), true ) ) {
|
||||
$format = 'csv';
|
||||
}
|
||||
switch ( $type ) {
|
||||
case 'health':
|
||||
self::send_health( $format, $days );
|
||||
break;
|
||||
case 'snapshots':
|
||||
self::send_snapshots( $format );
|
||||
break;
|
||||
case 'monthly':
|
||||
self::send_monthly( $format );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a download URL with nonce.
|
||||
*
|
||||
* @param string $type health|snapshots|monthly.
|
||||
* @param string $format csv|json.
|
||||
* @param int $days For health only.
|
||||
* @return string
|
||||
*/
|
||||
public static function url( string $type, string $format = 'csv', int $days = 30 ): string {
|
||||
$args = array(
|
||||
'page' => '2meet-data-optimizer',
|
||||
'wpdo_export' => $type,
|
||||
'format' => $format,
|
||||
);
|
||||
if ( 'health' === $type ) {
|
||||
$args['days'] = $days;
|
||||
}
|
||||
return wp_nonce_url( add_query_arg( $args, admin_url( 'tools.php' ) ), self::NONCE );
|
||||
}
|
||||
|
||||
// ─── handlers ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Send health export.
|
||||
*
|
||||
* @param string $format csv|json.
|
||||
* @param int $days Number of days to include.
|
||||
* @return void
|
||||
*/
|
||||
private static function send_health( string $format, int $days ): void {
|
||||
global $wpdb;
|
||||
$audit = $wpdb->prefix . 'wpdo_audit';
|
||||
$exists = (int) $wpdb->get_var(
|
||||
$wpdb->prepare( // phpcs:ignore WordPress.DB
|
||||
'SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s',
|
||||
$audit
|
||||
)
|
||||
);
|
||||
$rows = array();
|
||||
if ( 1 === $exists ) {
|
||||
$raw = (array) $wpdb->get_results(
|
||||
$wpdb->prepare( // phpcs:ignore WordPress.DB
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- {$audit} is a trusted table name via $wpdb->prefix
|
||||
"SELECT ts, op, value_after FROM `{$audit}` WHERE op = 'health_check_daily' AND ts >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY) ORDER BY ts DESC",
|
||||
$days
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
foreach ( $raw as $r ) {
|
||||
$ctx = isset( $r['value_after'] ) ? json_decode( (string) $r['value_after'], true ) : array();
|
||||
if ( ! is_array( $ctx ) ) {
|
||||
$ctx = array();
|
||||
}
|
||||
$rows[] = array(
|
||||
'ts' => (string) $r['ts'],
|
||||
'critical' => (int) ( $ctx['critical'] ?? 0 ),
|
||||
'recommended' => (int) ( $ctx['recommended'] ?? 0 ),
|
||||
'duration_ms' => (int) ( $ctx['duration_ms'] ?? 0 ),
|
||||
'autoload_kb' => (int) ( $ctx['autoload_kb'] ?? 0 ),
|
||||
'conflicts' => (int) ( $ctx['conflicts'] ?? 0 ),
|
||||
'module_suggestions_count' => (int) ( $ctx['module_suggestions_count'] ?? 0 ),
|
||||
);
|
||||
}
|
||||
}
|
||||
$headers = array( 'ts', 'critical', 'recommended', 'duration_ms', 'autoload_kb', 'conflicts', 'module_suggestions_count' );
|
||||
self::respond( $format, 'health', $headers, $rows );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send snapshots export.
|
||||
*
|
||||
* @param string $format csv|json.
|
||||
* @return void
|
||||
*/
|
||||
private static function send_snapshots( string $format ): void {
|
||||
global $wpdb;
|
||||
$snap = $wpdb->prefix . 'wpdo_snapshots';
|
||||
$exists = (int) $wpdb->get_var(
|
||||
$wpdb->prepare( // phpcs:ignore WordPress.DB
|
||||
'SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s',
|
||||
$snap
|
||||
)
|
||||
);
|
||||
$rows = array();
|
||||
if ( 1 === $exists ) {
|
||||
$rows = (array) $wpdb->get_results(
|
||||
"SELECT snapshot_id, trigger_type, size_bytes, row_count, storage, file_path, file_sha256, notes, created_at, expires_at FROM `{$snap}` ORDER BY created_at DESC", // phpcs:ignore WordPress.DB
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
$headers = array( 'snapshot_id', 'trigger_type', 'size_bytes', 'row_count', 'storage', 'file_path', 'file_sha256', 'notes', 'created_at', 'expires_at' );
|
||||
self::respond( $format, 'snapshots', $headers, $rows );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send monthly summary export.
|
||||
*
|
||||
* @param string $format csv|json.
|
||||
* @return void
|
||||
*/
|
||||
private static function send_monthly( string $format ): void {
|
||||
$latest = (array) get_option( 'wpdo_monthly_summary_latest', array() );
|
||||
$history = (array) get_option( 'wpdo_monthly_summary_history', array() );
|
||||
$all = array();
|
||||
if ( ! empty( $latest ) ) {
|
||||
$all[] = $latest;
|
||||
}
|
||||
foreach ( $history as $h ) {
|
||||
if ( is_array( $h ) ) {
|
||||
$all[] = $h;
|
||||
}
|
||||
}
|
||||
if ( 'json' === $format ) {
|
||||
self::respond_json( 'monthly', $all );
|
||||
return;
|
||||
}
|
||||
// Flatten for CSV — top-level metric only.
|
||||
$rows = array();
|
||||
foreach ( $all as $row ) {
|
||||
$health = (array) ( $row['health'] ?? array() );
|
||||
$snap = (array) ( $row['snapshots'] ?? array() );
|
||||
$rows[] = array(
|
||||
'period_start' => (string) ( $row['period_start'] ?? '' ),
|
||||
'period_end' => (string) ( $row['period_end'] ?? '' ),
|
||||
'generated_at' => (string) ( $row['generated_at'] ?? '' ),
|
||||
'health_total' => (int) ( $health['total'] ?? 0 ),
|
||||
'health_success' => (int) ( $health['success'] ?? 0 ),
|
||||
'health_critical' => (int) ( $health['critical'] ?? 0 ),
|
||||
'snapshots_total' => (int) ( $snap['total'] ?? 0 ),
|
||||
'snapshots_bytes' => (int) ( $snap['total_bytes'] ?? 0 ),
|
||||
'autoload_size' => (int) ( $row['autoload_size'] ?? 0 ),
|
||||
);
|
||||
}
|
||||
$headers = array( 'period_start', 'period_end', 'generated_at', 'health_total', 'health_success', 'health_critical', 'snapshots_total', 'snapshots_bytes', 'autoload_size' );
|
||||
self::respond_csv( 'monthly', $headers, $rows );
|
||||
}
|
||||
|
||||
// ─── output helpers ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Dispatch to csv or json responder.
|
||||
*
|
||||
* @param string $format csv|json.
|
||||
* @param string $type Export type slug.
|
||||
* @param array $headers Column headers.
|
||||
* @param array $rows Data rows.
|
||||
* @return void
|
||||
*/
|
||||
private static function respond( string $format, string $type, array $headers, array $rows ): void {
|
||||
if ( 'json' === $format ) {
|
||||
self::respond_json( $type, $rows );
|
||||
return;
|
||||
}
|
||||
self::respond_csv( $type, $headers, $rows );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send CSV response.
|
||||
*
|
||||
* @param string $type Export type slug.
|
||||
* @param array $headers Column headers.
|
||||
* @param array $rows Data rows.
|
||||
* @return void
|
||||
*/
|
||||
private static function respond_csv( string $type, array $headers, array $rows ): void {
|
||||
$body = TMDO_CSV_Writer::build( $headers, $rows );
|
||||
self::send_attachment( $type, 'csv', 'text/csv; charset=UTF-8', $body );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send JSON response.
|
||||
*
|
||||
* @param string $type Export type slug.
|
||||
* @param array $rows Data rows.
|
||||
* @return void
|
||||
*/
|
||||
private static function respond_json( string $type, array $rows ): void {
|
||||
$body = wp_json_encode( $rows, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
|
||||
self::send_attachment( $type, 'json', 'application/json; charset=UTF-8', (string) $body );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send file attachment response.
|
||||
*
|
||||
* @param string $type Export type slug.
|
||||
* @param string $ext File extension.
|
||||
* @param string $mime MIME type.
|
||||
* @param string $body File body content.
|
||||
* @return void
|
||||
*/
|
||||
private static function send_attachment( string $type, string $ext, string $mime, string $body ): void {
|
||||
$site_slug = sanitize_title( (string) get_option( 'blogname', 'site' ) ) ?: 'site';
|
||||
$filename = sprintf( 'wpdo-%s-%s-%s.%s', $type, $site_slug, gmdate( 'Ymd' ), $ext );
|
||||
nocache_headers();
|
||||
header( 'Content-Type: ' . $mime );
|
||||
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
|
||||
header( 'Content-Length: ' . strlen( $body ) );
|
||||
echo $body; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
exit;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user