| 1 |
<?php |
| 2 |
defined('ABSPATH') or die('Unauthorized Access'); |
| 3 |
|
| 4 |
class Phpinfo_WP_Snapshots { |
| 5 |
|
| 6 |
private static function _pro(): bool { return Phpinfo_WP_License::is_valid(); } |
| 7 |
|
| 8 |
// Only track directives that actually matter — full ini_get_all() is hundreds of noisy keys |
| 9 |
const TRACKED = [ |
| 10 |
'memory_limit', 'upload_max_filesize', 'post_max_size', 'max_execution_time', |
| 11 |
'max_input_time', 'max_input_vars', 'max_file_uploads', 'display_errors', |
| 12 |
'display_startup_errors', 'error_reporting', 'error_log', 'log_errors', |
| 13 |
'expose_php', 'allow_url_fopen', 'allow_url_include', 'disable_functions', |
| 14 |
'file_uploads', 'default_socket_timeout', 'date.timezone', |
| 15 |
'session.gc_maxlifetime', 'session.use_strict_mode', |
| 16 |
'session.cookie_httponly', 'session.cookie_secure', |
| 17 |
'opcache.enable', 'opcache.memory_consumption', 'opcache.max_accelerated_files', |
| 18 |
'opcache.validate_timestamps', 'opcache.revalidate_freq', |
| 19 |
'zlib.output_compression', 'default_charset', 'short_open_tag', |
| 20 |
'mbstring.internal_encoding', 'precision', 'serialize_precision', |
| 21 |
]; |
| 22 |
|
| 23 |
private static function table(): string { |
| 24 |
global $wpdb; |
| 25 |
return $wpdb->prefix . 'phpinfowp_snapshots'; |
| 26 |
} |
| 27 |
|
| 28 |
public static function install_table(): void { |
| 29 |
global $wpdb; |
| 30 |
$charset = $wpdb->get_charset_collate(); |
| 31 |
$sql = "CREATE TABLE IF NOT EXISTS " . self::table() . " ( |
| 32 |
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, |
| 33 |
label VARCHAR(255) NOT NULL DEFAULT '', |
| 34 |
created_by BIGINT UNSIGNED NOT NULL DEFAULT 0, |
| 35 |
created_at DATETIME NOT NULL, |
| 36 |
snapshot_data LONGTEXT NOT NULL, |
| 37 |
PRIMARY KEY (id), |
| 38 |
KEY created_at (created_at) |
| 39 |
) {$charset};"; |
| 40 |
require_once ABSPATH . 'wp-admin/includes/upgrade.php'; |
| 41 |
dbDelta($sql); |
| 42 |
} |
| 43 |
|
| 44 |
public static function capture(): array { |
| 45 |
if (!self::_pro()) return []; |
| 46 |
$data = []; |
| 47 |
foreach (self::TRACKED as $key) { |
| 48 |
$val = ini_get($key); |
| 49 |
$data[$key] = ($val === false) ? null : $val; |
| 50 |
} |
| 51 |
// Also record PHP version itself |
| 52 |
$data['__php_version'] = PHP_VERSION; |
| 53 |
return $data; |
| 54 |
} |
| 55 |
|
| 56 |
public static function take(string $label = '', int $user_id = 0): int { |
| 57 |
if (!self::_pro()) return 0; |
| 58 |
global $wpdb; |
| 59 |
$data = self::capture(); |
| 60 |
$wpdb->insert(self::table(), [ |
| 61 |
'label' => $label ?: 'Manual snapshot', |
| 62 |
'created_by' => $user_id ?: get_current_user_id(), |
| 63 |
'created_at' => current_time('mysql', true), |
| 64 |
'snapshot_data' => wp_json_encode($data), |
| 65 |
]); |
| 66 |
return (int) $wpdb->insert_id; |
| 67 |
} |
| 68 |
|
| 69 |
public static function list(int $limit = 50): array { |
| 70 |
if (!self::_pro()) return []; |
| 71 |
global $wpdb; |
| 72 |
return $wpdb->get_results($wpdb->prepare( |
| 73 |
"SELECT id, label, created_by, created_at FROM " . self::table() . " ORDER BY created_at DESC LIMIT %d", |
| 74 |
$limit |
| 75 |
)); |
| 76 |
} |
| 77 |
|
| 78 |
public static function get(int $id): ?object { |
| 79 |
if (!self::_pro()) return null; |
| 80 |
global $wpdb; |
| 81 |
$row = $wpdb->get_row($wpdb->prepare( |
| 82 |
"SELECT * FROM " . self::table() . " WHERE id = %d LIMIT 1", $id |
| 83 |
)); |
| 84 |
if (!$row) return null; |
| 85 |
$row->snapshot_data = json_decode($row->snapshot_data, true) ?? []; |
| 86 |
return $row; |
| 87 |
} |
| 88 |
|
| 89 |
public static function delete(int $id): void { |
| 90 |
if (!self::_pro()) return; |
| 91 |
global $wpdb; |
| 92 |
$wpdb->delete(self::table(), ['id' => $id], ['%d']); |
| 93 |
} |
| 94 |
|
| 95 |
public static function get_latest(): ?object { |
| 96 |
if (!self::_pro()) return null; |
| 97 |
global $wpdb; |
| 98 |
$row = $wpdb->get_row( |
| 99 |
"SELECT * FROM " . self::table() . " ORDER BY created_at DESC LIMIT 1" |
| 100 |
); |
| 101 |
if (!$row) return null; |
| 102 |
$row->snapshot_data = json_decode($row->snapshot_data, true) ?? []; |
| 103 |
return $row; |
| 104 |
} |
| 105 |
|
| 106 |
public static function get_previous_to(int $id): ?object { |
| 107 |
if (!self::_pro()) return null; |
| 108 |
global $wpdb; |
| 109 |
$row = $wpdb->get_row($wpdb->prepare( |
| 110 |
"SELECT * FROM " . self::table() . " WHERE id < %d ORDER BY id DESC LIMIT 1", $id |
| 111 |
)); |
| 112 |
if (!$row) return null; |
| 113 |
$row->snapshot_data = json_decode($row->snapshot_data, true) ?? []; |
| 114 |
return $row; |
| 115 |
} |
| 116 |
|
| 117 |
// Returns array of ['type'=>added|removed|changed, 'key'=>..., 'old'=>..., 'new'=>...] |
| 118 |
public static function diff(array $old_data, array $new_data): array { |
| 119 |
if (!self::_pro()) return []; |
| 120 |
$changes = []; |
| 121 |
|
| 122 |
foreach ($old_data as $key => $old_val) { |
| 123 |
if (!array_key_exists($key, $new_data)) { |
| 124 |
$changes[] = ['type' => 'removed', 'key' => $key, 'old' => $old_val, 'new' => null]; |
| 125 |
} elseif ((string) $old_val !== (string) $new_data[$key]) { |
| 126 |
$changes[] = ['type' => 'changed', 'key' => $key, 'old' => $old_val, 'new' => $new_data[$key]]; |
| 127 |
} |
| 128 |
} |
| 129 |
foreach ($new_data as $key => $new_val) { |
| 130 |
if (!array_key_exists($key, $old_data)) { |
| 131 |
$changes[] = ['type' => 'added', 'key' => $key, 'old' => null, 'new' => $new_val]; |
| 132 |
} |
| 133 |
} |
| 134 |
|
| 135 |
usort($changes, function ($a, $b) { |
| 136 |
return strcmp($a['key'], $b['key']); |
| 137 |
}); |
| 138 |
return $changes; |
| 139 |
} |
| 140 |
|
| 141 |
// Auto-snapshot called by cron. Returns diff vs. previous if changes detected. |
| 142 |
public static function auto_snapshot(): array { |
| 143 |
if (!self::_pro()) return []; |
| 144 |
$new_id = self::take('Auto (weekly)', 0); |
| 145 |
$new = self::get($new_id); |
| 146 |
$prev = self::get_previous_to($new_id); |
| 147 |
|
| 148 |
if (!$prev) return []; |
| 149 |
return self::diff($prev->snapshot_data, $new->snapshot_data); |
| 150 |
} |
| 151 |
|
| 152 |
public static function prune(int $keep = 30): void { |
| 153 |
if (!self::_pro()) return; |
| 154 |
if (!Phpinfo_WP_License::is_unlimited()) { |
| 155 |
$keep = 3; |
| 156 |
} |
| 157 |
global $wpdb; |
| 158 |
$ids = $wpdb->get_col($wpdb->prepare( |
| 159 |
"SELECT id FROM " . self::table() . " ORDER BY created_at DESC LIMIT %d, 9999", $keep |
| 160 |
)); |
| 161 |
if ($ids) { |
| 162 |
$placeholders = implode(',', array_fill(0, count($ids), '%d')); |
| 163 |
$wpdb->query($wpdb->prepare("DELETE FROM " . self::table() . " WHERE id IN ($placeholders)", ...$ids)); |
| 164 |
} |
| 165 |
} |
| 166 |
} |
| 167 |
|