Files
2meet-data-optimizer/admin/class-tmdo-export.php
T
wpdev d36bb954d1 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
2026-07-31 05:06:36 +08:00

273 lines
8.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 GETtools.php?page=wp-data-optimizer&wpdo_export=health&format=csv&days=30
* Capability: manage_options + nonce.
*
* Filenameswpdo-{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;
}
}