PluginProbe
WP-Memory-Usage / 2.0.0
WP-Memory-Usage v2.0.0
2.3.1 trunk 1.2.10 1.2.11 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 2.0.0 2.0.1 2.0.2 2.1.0 2.1.1 2.2.0 2.2.1 2.3.0
wp-memory-usage / includes / threshold-alerts.php

threshold-alerts.php in WP-Memory-Usage 2.0.0, at includes/threshold-alerts.php

2,250 lines 101.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Threshold alerts for WP-Memory-Usage
4 * - This does NOT attribute memory usage to a specific plugin. It records the request context where the peak happened.
5 */
6 if ( ! defined( 'ABSPATH' ) ) { exit; }
7 if ( ! class_exists( 'WPMU_Threshold_Alerts' ) ) :
8 final class WPMU_Threshold_Alerts {
9 const OPTION_KEY = 'wpmu_threshold_alerts';
10 const CAP = 'manage_options';
11 const DIGEST_HOOK = 'wpmu_daily_digest';
12 #private static $nolog_admin = FALSE;
13 private static $event_anz_display = 10;
14 private static $event_anz_store = 10;
15 const SETTING_COLOR_BACK = "#e7f3ff";
16 const SETTING_COLOR_FONT = "#0c5d9e";
17 const SETTING_COLOR_BORDER = "#3498db";
18
19
20 const WPMU_LOG_FILE = "wpmu-log.cgi";
21 const WPMU_LOG_PATH = ABSPATH. '../logs/wpmu/';
22 private static $log_path = "";
23
24
25 public static function init($event_anz_display, $event_anz_store, $log_path) {
26 if (is_admin()) {
27 add_action( 'admin_menu', array( __CLASS__, 'add_admin_menu' ) );
28 add_action( 'admin_init', array( __CLASS__, 'register_settings' ) );
29 add_action( 'admin_bar_menu', array( __CLASS__, 'admin_bar_node' ), 100 );
30 self::$event_anz_display = $event_anz_display;
31 }
32 self::$log_path = $log_path;
33 self::$event_anz_store = $event_anz_store;
34 add_action('plugins_loaded', function () {
35 register_shutdown_function(array( __CLASS__, 'check_thresholds_on_shutdown' ));
36 });
37 # add_action( self::DIGEST_HOOK, array( __CLASS__, 'send_daily_digest' ) );
38 add_filter( 'cron_schedules', array( __CLASS__, 'add_cron_intervals' ) );
39 add_action( 'wpmu_cleanup_hook', array( __CLASS__, 'cron_cleanup' ) );
40 // Cron planen – Intervall kommt aus Settings
41 self::reschedule_cron();
42
43 register_activation_hook( dirname( __FILE__ ) . '/../wp-memory-usage.php', array( __CLASS__, 'activate' ) );
44 register_deactivation_hook( dirname( __FILE__ ) . '/../wp-memory-usage.php', array( __CLASS__, 'deactivate' ) );
45 }
46
47 public static function reschedule_cron() {
48 $opts = self::get_settings();
49 $interval_min = isset( $opts['rotate_log'] ) ? (int) $opts['rotate_log'] : 60;
50 #$interval_sec = $interval_min * 60;
51 $hook = 'wpmu_cleanup_hook';
52 $next = wp_next_scheduled( $hook );
53 $schedule = wp_get_schedule( $hook );
54 $expected = 'wpmu_every_' . $interval_min . '_min';
55 if ( ! $next || $schedule !== $expected ) {
56 if ( $next ) {
57 wp_unschedule_event( $next, $hook );
58 }
59 wp_schedule_event( time(), $expected, $hook );
60 }
61 }
62
63 public static function add_cron_intervals( $schedules ) {
64 $opts = self::get_settings();
65 $interval_min = isset( $opts['rotate_log'] ) ? (int) $opts['rotate_log'] : 60;
66 $schedules[ 'wpmu_every_' . $interval_min . '_min' ] = array(
67 'interval' => $interval_min * 60,
68 #'display' => sprintf( 'Every %d minutes (WPMU)', $interval_min ),
69 /* translators: %d: number of minutes */
70 'display' => sprintf( __( 'Every %d minutes (WPMU)', 'wp-memory-usage' ), $interval_min ),
71 );
72 return $schedules;
73 }
74
75 public static function activate() {}
76
77 public static function deactivate() {
78 $ts = wp_next_scheduled( 'wpmu_cleanup_hook' );
79 if ( $ts ) {
80 wp_unschedule_event( $ts, 'wpmu_cleanup_hook' );
81 }
82 }
83
84 public static function cron_cleanup() {
85 $dir = self::WPMU_LOG_PATH;
86 $file = $dir . self::WPMU_LOG_FILE;
87
88 $ts = wp_date("Y-m-d-H-i-s");
89 $backup = $dir . 'wpmu-log_' . $ts . '.cgibak';
90
91 global $wp_filesystem;
92 if ( ! function_exists( 'WP_Filesystem' ) ) {
93 require_once ABSPATH . 'wp-admin/includes/file.php';
94 }
95 WP_Filesystem();
96 $wp_filesystem->move( $file, $backup, true ); // true = overwrite falls Ziel existiert
97 #rename( $file, $backup ); # first rename, close this file
98
99 #error_log("cron_cleanup: $file");
100 #if ( file_exists( $file )) {
101 if ( file_exists( $backup )) {
102 $opts = self::get_complete_file( basename( $backup ) );
103
104 $aggregation = array(
105 'status' => array( 'ok' => 0, 'warn' => 0, 'danger' => 0, 'critical' => 0 ),
106 'type' => array(),
107 'uri' => array(),
108 'interval' => array(
109 'first' => PHP_INT_MAX,
110 'last' => 0,
111 'per_minute' => array(),
112 ),
113 );
114
115 foreach ( $opts as $e1 ) {
116 $s = (string) ( $e1['s'] ?? 'ok' );
117 $t = (int) ( $e1['t'] ?? 0 );
118 $u = (int) ( $e1['u'] ?? 0 );
119 $c = is_array( $e1['c'] ) ? $e1['c'] : array();
120 $type = (string) ( $c['type'] ?? '' );
121 $uri = (string) ( $c['uri'] ?? '' );
122
123 // 1) Status-Häufigkeit
124 $aggregation['status'][ $s ] = ( $aggregation['status'][ $s ] ?? 0 ) + 1;
125
126 // 2) Request-Typ
127 if ( $type ) {
128 $aggregation['type'][ $type ] = ( $aggregation['type'][ $type ] ?? 0 ) + 1;
129 }
130
131 // 3) URI-Häufigkeit + avg/max
132 if ( $uri ) {
133 if ( ! isset( $aggregation['uri'][ $uri ] ) ) {
134 $aggregation['uri'][ $uri ] = array(
135 'total' => 0,
136 'warn' => 0,
137 'danger' => 0,
138 'critical' => 0,
139 'sum_usage' => 0,
140 'max_usage' => 0,
141 );
142 }
143 $aggregation['uri'][ $uri ]['total']++;
144 $aggregation['uri'][ $uri ][ $s ] = ( $aggregation['uri'][ $uri ][ $s ] ?? 0 ) + 1;
145 $aggregation['uri'][ $uri ]['sum_usage'] += $u;
146 if ( $u > $aggregation['uri'][ $uri ]['max_usage'] ) {
147 $aggregation['uri'][ $uri ]['max_usage'] = $u;
148 }
149 }
150
151 // 4) Zeitintervall
152 if ( $t < $aggregation['interval']['first'] ) { $aggregation['interval']['first'] = $t; }
153 if ( $t > $aggregation['interval']['last'] ) { $aggregation['interval']['last'] = $t; }
154
155 // 5) Ereignisse pro Minute
156 $minute_key = wp_date( get_option('date_format') . ', ' . get_option('time_format'), (int) $t ); #date( 'Y-m-d H:i', $t );
157 $aggregation['interval']['per_minute'][ $minute_key ] =
158 ( $aggregation['interval']['per_minute'][ $minute_key ] ?? 0 ) + 1;
159 }
160
161 // Zeitspanne berechnen
162 $aggregation['interval']['duration_sec'] =
163 $aggregation['interval']['last'] - $aggregation['interval']['first'];
164
165 // Mittelwert pro URI berechnen, sum_usage entfernen
166 foreach ( $aggregation['uri'] as $uri_key => &$udata ) {
167 $udata['avg_usage'] = $udata['total'] > 0
168 ? (int) round( $udata['sum_usage'] / $udata['total'] )
169 : 0;
170 unset( $udata['sum_usage'] );
171 }
172 unset( $udata );
173
174 $filenamedigest = 'digest_' . $ts . '.cgibak';
175 #error_log("filenamedigest $filenamedigest");
176 self::update_file( $aggregation, $filenamedigest );
177
178 // ── Digest-E-Mail senden, wenn send_email aktiv und Alerts vorhanden ──
179 $settings_for_mail = self::get_settings();
180 if (
181 ! empty( $settings_for_mail['send_email'] ) &&
182 is_email( $settings_for_mail['email_to'] ) &&
183 (
184 ( $aggregation['status']['warn'] ?? 0 ) > 0 ||
185 ( $aggregation['status']['danger'] ?? 0 ) > 0 ||
186 ( $aggregation['status']['critical'] ?? 0 ) > 0
187 )
188 ) {
189 self::send_digest_email( $settings_for_mail['email_to'], $aggregation, $filenamedigest );
190 }
191
192 } // end if ( file_exists( $backup ) )
193
194 // Alte Backups löschen
195 $opts = self::get_settings();
196 $del_min = isset( $opts['del_frequency_logfiles'] ) ? (int) $opts['del_frequency_logfiles'] : 1440;
197 #error_log("del_min: $del_min");
198
199 if ( $del_min > 0 ) {
200 #error_log("do del");
201 $max_age_sec = $del_min * 60;
202 $now = time();
203 foreach ( glob( $dir . '*.cgibak' ) as $bak ) {
204 // Nur rotierte Log-Backups (wpmu-log_*.cgibak) löschen, keine Digest-Dateien
205 #error_log("del: ".$bak);
206 if ( strpos( basename( $bak ), 'wpmu-log_' ) === 0 ) {
207 if ( filemtime( $bak ) < ( $now - $max_age_sec ) ) {
208 #error_log("do del: ".$bak);
209 wp_delete_file( $bak );
210 }
211 }
212 }
213 }
214 }
215
216 public static function add_admin_menu() {
217 add_options_page(
218 esc_html__( 'Memory Threshold Alerts', 'wp-memory-usage' ),
219 esc_html__( 'Memory Alerts', 'wp-memory-usage' ),
220 self::CAP,
221 'wpmu-memory-alerts',
222 array( __CLASS__, 'render_settings_page' )
223 );
224 }
225
226 #### LOGGING: SAVE and LOAD : BEGIN ####
227 private static function get_from_file($filename) {
228 if (empty($filename)) { return NULL; }
229 $dir = self::WPMU_LOG_PATH;
230
231 global $wp_filesystem;
232 if ( ! function_exists( 'WP_Filesystem' ) ) {
233 require_once ABSPATH . 'wp-admin/includes/file.php';
234 }
235 WP_Filesystem();
236 if ( ! $wp_filesystem->is_dir( $dir ) ) {
237 $wp_filesystem->mkdir( $dir, 0755 );
238 }
239 #if (!is_dir($dir)) { @mkdir($dir, 0755, true); }
240
241 $file = $dir . $filename;
242 if (!file_exists($file)) {
243 return [];
244 }
245 // phpcs:disable WordPress.WP.AlternativeFunctions
246 $fh = @fopen($file, 'rb');
247 if (!$fh) return [];
248 if (!flock($fh, LOCK_SH)) { fclose($fh); return []; }
249 $content = fgets($fh);
250 $content = trim($content);
251 flock($fh, LOCK_UN);
252 fclose($fh);
253 // phpcs:enable WordPress.WP.AlternativeFunctions
254 return json_decode($content, TRUE);
255 }
256
257 private static function get_complete_file($filename) {
258 if (empty($filename)) { return NULL; }
259 $dir = self::WPMU_LOG_PATH;
260
261 global $wp_filesystem;
262 if ( ! function_exists( 'WP_Filesystem' ) ) {
263 require_once ABSPATH . 'wp-admin/includes/file.php';
264 }
265 WP_Filesystem();
266 if ( ! $wp_filesystem->is_dir( $dir ) ) {
267 $wp_filesystem->mkdir( $dir, 0755 );
268 }
269 #if (!is_dir($dir)) { @mkdir($dir, 0755, true); }
270
271 $file = $dir . $filename;
272 // phpcs:disable WordPress.WP.AlternativeFunctions
273 $fh = @fopen($file, 'rb');
274 if (!$fh) return [];
275 if (!flock($fh, LOCK_SH)) { fclose($fh); return []; }
276 $content = stream_get_contents($fh);
277 flock($fh, LOCK_UN);
278 fclose($fh);
279 // phpcs:enable WordPress.WP.AlternativeFunctions
280 $lines = array_filter(explode("\n", trim($content)));
281 $result = [];
282 foreach ($lines as $line) {
283 $decoded = json_decode(trim($line), true);
284 if (is_array($decoded)) {
285 $result[] = $decoded;
286 }
287 }
288 return $result;
289 }
290
291 private static function get_log() {
292 return self::get_complete_file(self::WPMU_LOG_FILE);
293 }
294
295 private static function add_log($opts) {
296 return self::append_file( $opts, self::WPMU_LOG_FILE);
297 }
298
299 private static function get_options_file() {
300 $opts = self::get_log();
301 if ( ! is_array( $opts ) ) { $opts = array(); }
302 $merged = array_merge( self::defaults(), $opts );
303 if ( isset( $merged['last_context'] ) && ! is_array( $merged['last_context'] ) ) {
304 $merged['last_context'] = array( 'summary' => (string) $merged['last_context'] );
305 }
306 if ( ! isset( $merged['ev'] ) || ! is_array( $merged['ev'] ) ) {
307 $merged['ev'] = array();
308 }
309 if ( ! isset( $merged['fingerprint_sent'] ) || ! is_array( $merged['fingerprint_sent'] ) ) {
310 $merged['fingerprint_sent'] = array();
311 }
312 return $merged;
313 }
314
315 private static function append_file( $opts, $filename ) {
316 if (empty($filename)) { return NULL; }
317 $dir = self::WPMU_LOG_PATH;
318
319 global $wp_filesystem;
320 if ( ! function_exists( 'WP_Filesystem' ) ) {
321 require_once ABSPATH . 'wp-admin/includes/file.php';
322 }
323 WP_Filesystem();
324 if ( ! $wp_filesystem->is_dir( $dir ) ) {
325 $wp_filesystem->mkdir( $dir, 0755 );
326 }
327 #if (!is_dir($dir)) { @mkdir($dir, 0755, true); }
328 $file = $dir . $filename;
329 $optsStr = json_encode($opts) . "\n";
330 // phpcs:disable WordPress.WP.AlternativeFunctions
331 $fh = fopen($file, 'ab');
332 if ( $fh ) {
333 flock($fh, LOCK_EX);
334 fwrite($fh, $optsStr);
335 fflush($fh);
336 flock($fh, LOCK_UN);
337 fclose($fh);
338 }
339 #file_put_contents($file, $optsStr, FILE_APPEND | LOCK_EX);
340 // phpcs:enable WordPress.WP.AlternativeFunctions
341 }
342
343 private static function update_file( $opts, $filename ) {
344 if (empty($filename)) { return NULL; }
345 $dir = self::WPMU_LOG_PATH;
346 #error_log("DIR: ".$dir);
347 global $wp_filesystem;
348 if ( ! function_exists( 'WP_Filesystem' ) ) {
349 require_once ABSPATH . 'wp-admin/includes/file.php';
350 }
351 WP_Filesystem();
352 if ( ! $wp_filesystem->is_dir( $dir ) ) {
353 $wp_filesystem->mkdir( $dir, 0755 );
354 }
355 #if (!is_dir($dir)) { @mkdir($dir, 0755, true); }
356 $file = $dir . $filename;
357 $optsStr = json_encode($opts);
358 // phpcs:disable WordPress.WP.AlternativeFunctions
359 $fh = fopen($file, 'c+');
360 if (!$fh) return;
361 flock($fh, LOCK_EX);
362 ftruncate($fh, 0);
363 rewind($fh);
364 fwrite($fh, $optsStr);
365 fflush($fh);
366 flock($fh, LOCK_UN);
367 fclose($fh);
368 // phpcs:enable WordPress.WP.AlternativeFunctions
369 }
370
371 private static function update_options_file( $opts ) {
372 self::update_file($opts, self::WPMU_LOG_FILE);
373 }
374
375 private static function get_settings() {
376 $settings = self::get_from_file("settings.cgi");
377 #error_log("settings: ".print_r($settings, true));
378 if ($settings === []) {
379 #error_log("settings: DEF");
380 $settings = self::defaults();
381 }
382 #error_log("Settings: ".print_r($settings, true));
383 return $settings;
384 }
385
386 private static function set_settings($opts) {
387 return self::update_file($opts, "settings.cgi");
388 }
389 #### LOGGING: SAVE and LOAD : END ####
390
391 ###############################################################
392 private static function defaults() {
393 return array(
394 'warn_pct' => 70,
395 'danger_pct' => 85,
396 'critical_pct' => 95,
397 'logop_warn_pct' => 0,
398 'logop_danger_pct' => 0,
399 'logop_critical_pct' => 0,
400 'email_to' => get_option( 'admin_email' ),
401 'send_email' => 0,
402 'track_peak' => 1,
403 'log_ajax' => 1,
404 'log_cron' => 1,
405 'log_rest' => 1,
406 'log_admin' => 1,
407 'log_favicon' => 1,
408 'log_ok' => 0,
409 'digest_enabled' => 1,
410 'rotate_log' => 30,
411 'del_frequency_logfiles' => 1440, # 1day=24*60
412 );
413 }
414
415 /** ---------------- Settings UI ---------------- */
416 #public static function error_logging($errortxt) {
417 # error_log($errortxt);
418 # return TRUE;
419 #}
420
421 public static function section_intro_measure() {
422 echo esc_html__( 'Choose how memory usage should be measured. Peak memory catches short spikes during the request.', 'wp-memory-usage' );
423 }
424 public static function section_intro_log() {
425 echo esc_html__( 'Configure which request types and memory states should be logged.', 'wp-memory-usage' );
426 }
427 public static function section_intro_howalert() {
428 echo esc_html__( 'Configure when and how email alerts are sent.', 'wp-memory-usage' );
429 }
430 public static function section_intro_thresholds() {
431 echo esc_html__('Thresholds are evaluated as percentage of the effective memory limit (min of PHP and WP limits where applicable).', 'wp-memory-usage' );
432 }
433
434 public static function sanitize_options_file( $input ) {
435 $opts_settings = self::get_settings();
436 $in = is_array( $input ) ? $input : array();
437 $opts_settings['warn_pct'] = self::clamp_int( isset( $in['warn_pct'] ) ? $in['warn_pct'] : $opts_settings['warn_pct'], 1, 99 );
438 $opts_settings['danger_pct'] = self::clamp_int( isset( $in['danger_pct'] ) ? $in['danger_pct'] : $opts_settings['danger_pct'], 1, 100 );
439 $opts_settings['critical_pct'] = self::clamp_int( isset( $in['critical_pct'] ) ? $in['critical_pct'] : $opts_settings['critical_pct'], 1, 100 );
440 $opts_settings['logop_warn_pct'] = ! empty( $in['logop_warn_pct'] ) ? 1 : 0;
441 $opts_settings['logop_danger_pct'] = ! empty( $in['logop_danger_pct'] ) ? 1 : 0;
442 $opts_settings['logop_critical_pct'] = ! empty( $in['logop_critical_pct'] ) ? 1 : 0;
443 if ( $opts_settings['danger_pct'] <= $opts_settings['warn_pct'] ) {
444 $opts_settings['danger_pct'] = min( 100, $opts_settings['warn_pct'] + 5 );
445 }
446 if ( $opts_settings['critical_pct'] <= $opts_settings['danger_pct'] ) {
447 $opts_settings['critical_pct'] = min( 100, $opts_settings['danger_pct'] + 5 );
448 }
449 $opts_settings['email_to'] = sanitize_email( isset( $in['email_to'] ) ? $in['email_to'] : $opts_settings['email_to'] );
450 $opts_settings['send_email'] = ! empty( $in['send_email'] ) ? 1 : 0;
451 $opts_settings['track_peak'] = ! empty( $in['track_peak'] ) ? 1 : 0;
452 $opts_settings['del_frequency_logfiles'] = self::clamp_int( isset( $in['del_frequency_logfiles'] ) ? $in['del_frequency_logfiles'] : $opts_settings['del_frequency_logfiles'], 0, 43200 );
453 $opts_settings['digest_enabled'] = ! empty( $in['digest_enabled'] ) ? 1 : 0;
454 $opts_settings['rotate_log'] = self::clamp_int( isset( $in['rotate_log'] ) ? $in['rotate_log'] : $opts_settings['rotate_log'], 1, 600 );
455 $opts_settings['log_admin'] = ! empty( $in['log_admin'] ) ? 1 : 0;
456 $opts_settings['log_ajax'] = ! empty( $in['log_ajax'] ) ? 1 : 0;
457 $opts_settings['log_rest'] = ! empty( $in['log_rest'] ) ? 1 : 0;
458 $opts_settings['log_cron'] = ! empty( $in['log_cron'] ) ? 1 : 0;
459 $opts_settings['log_favicon'] = ! empty( $in['log_favicon'] ) ? 1 : 0;
460 $opts_settings['log_ok'] = ! empty( $in['log_ok'] ) ? 1 : 0;
461 self::set_settings( $opts_settings );
462 self::reschedule_cron();
463 return $opts_settings;
464 }
465
466 private static function clamp_int( $value, $min, $max ) {
467 $v = (int) $value;
468 if ( $v < (int) $min ) { $v = (int) $min; }
469 if ( $v > (int) $max ) { $v = (int) $max; }
470 return $v;
471 }
472
473 public static function render_field( $args ) {
474 $opts = self::get_settings();
475 $key = isset( $args['key'] ) ? (string) $args['key'] : '';
476 $val = isset( $opts[ $key ] ) ? $opts[ $key ] : '';
477 $type = 'number';
478 if ( 'email_to' === $key ) { $type = 'email'; }
479 $style = '';
480 if ( 'number' === $key ) { $style = ' style="width: 200px;"'; }
481 $attrs = '';
482 if ( 'number' === $type ) {
483 $attrs = 'step="1"';
484 if ( in_array( $key, array( 'warn_pct', 'danger_pct', 'critical_pct' ), true ) ) {
485 $attrs .= ' min="1" max="100" size="4"';
486 } elseif ( in_array( $key, array( 'del_frequency_logfiles' ), true ) ) {
487 $attrs .= ' min="0" max="43200"'; #max 30 days
488 } elseif ( in_array( $key, array( 'rotate_log' ), true ) ) {
489 $attrs .= ' min="1" max="600"';
490 }
491 }
492 printf(
493 '<input type="%s" name="%s[%s]" value="%s" %s %s />',
494 esc_attr( $type ),
495 esc_attr( self::OPTION_KEY ),
496 esc_attr( $key ),
497 esc_attr( (string) $val ),
498 esc_attr( (string) $style ),
499 wp_kses_data( $attrs )
500 );
501 $desc = '';
502 switch ( $key ) {
503 case 'email_to':
504 $desc = __( 'Email address that should receive alerts. Tip: use a shared mailbox or a ticket system address so alerts don\'t get lost. You can also use your agency\'s address if they maintain the site.', 'wp-memory-usage' );
505 break;
506 case 'warn_pct':
507 $desc = __( 'Warning threshold (in percent). Use this as an early heads‑up. Example: 70 means "warn me when a request uses 70% of the available memory". This is usually not an emergency, but it tells you where to look.', 'wp-memory-usage' );
508 break;
509 case 'danger_pct':
510 $desc = __( 'Danger threshold (in percent). Use this for "take action soon". Example: 85 means "this request consumed 85% of the available memory". Repeated hits here often lead to errors on heavy pages or imports.', 'wp-memory-usage' );
511 break;
512 case 'critical_pct':
513 $desc = __( 'Critical threshold (in percent). This is "high risk of out-of-memory errors". Example: 95 means "the request almost hit the limit". If you see this on the frontend, visitors can get 500 errors.', 'wp-memory-usage' );
514 break;
515 case 'del_frequency_logfiles':
516 $desc = __( 'Delete rotated Logfiles older than X Minutes', 'wp-memory-usage' );
517 break;
518 case 'rotate_log':
519 $desc = __( 'Rotate every X Minutes the logfile and create a digest', 'wp-memory-usage' );
520 break;
521 default:
522 break;
523 }
524 if ( $desc ) {
525 echo '<br>' . esc_html( $desc ) . '';
526 }
527 }
528
529 public static function render_checkbox( $args ) {
530 $opts = self::get_settings();
531 $key = isset( $args['key'] ) ? (string) $args['key'] : '';
532 $val = ! empty( $opts[ $key ] ) ? 1 : 0;
533 printf(
534 '<label><input type="checkbox" name="%s[%s]" value="1" %s /></label>',
535 esc_attr( self::OPTION_KEY ),
536 esc_attr( $key ),
537 checked( 1, $val, false )
538 );
539 $desc = '';
540 switch ( $key ) {
541 case 'send_email':
542 $desc = __( 'Enable email alerts. If disabled, the plugin still shows the status in the admin bar and stores results, but it will not send any emails.', 'wp-memory-usage' );
543 break;
544 case 'track_peak':
545 $desc = __( 'Use peak memory (recommended). The plugin measures the highest memory usage during the request. This catches short spikes (for example during image processing) that might be missed when only looking at the current value.', 'wp-memory-usage' );
546 break;
547 #case 'digest_enabled':
548 # $desc = __( 'Enable daily digest (Smart mode). You will receive one summary email per day with the most important alerts and where they happened. Useful if you don\'t want immediate emails for every event.', 'wp-memory-usage' );
549 # break;
550 default:
551 break;
552 }
553 if ( $desc ) {
554 echo '<p class="description">' . esc_html( $desc ) . '</p>';
555 }
556 }
557
558 public static function render_select( $args ) {
559 $opts = self::get_settings();
560 $key = isset( $args['key'] ) ? (string) $args['key'] : '';
561 $val = isset( $opts[ $key ] ) ? (string) $opts[ $key ] : '';
562 $options = isset( $args['options'] ) && is_array( $args['options'] ) ? $args['options'] : array();
563 printf( '<select name="%s[%s]">', esc_attr( self::OPTION_KEY ), esc_attr( $key ) );
564 foreach ( $options as $k => $label ) {
565 printf(
566 '<option value="%s" %s>%s</option>',
567 esc_attr( (string) $k ),
568 selected( $val, (string) $k, false ),
569 esc_html( (string) $label )
570 );
571 }
572 echo '</select>';
573 }
574
575 /** ---------------- Admin Bar ---------------- */
576 public static function admin_bar_node( $bar ) {
577 if ( ! is_user_logged_in() || ! current_user_can( self::CAP ) ) { return; }
578 $opts = self::get_settings();
579 #error_log("admin_bar_node: ".print_r($opts, true));
580 $limit = self::get_effective_memory_limit_bytes();
581 $usage = self::get_current_memory_bytes( ! empty( $opts['track_peak'] ) );
582 $state = self::state_for_usage( $usage, $limit, $opts );
583 $label = sprintf(
584 '%s %s / %s',
585 strtoupper( $state ),
586 self::format_bytes( $usage ),
587 $limit > 0 ? self::format_bytes( $limit ) : __( 'unlimited', 'wp-memory-usage' )
588 );
589 $bar->add_node( array(
590 'id' => 'wpmu_memory_alerts',
591 'title' => esc_html( $label ),
592 'href' => admin_url( 'options-general.php?page=wpmu-memory-alerts' ),
593 'meta' => array( 'title' => 'WP Memory Threshold Alerts' ),
594 ) );
595 }
596
597 /** ---------------- Core logic ---------------- */
598 public static function check_thresholds_on_shutdown() {
599 $opts_settings = self::get_settings();
600 $opts = [];
601 $limit = self::get_effective_memory_limit_bytes();
602 $usage = self::get_current_memory_bytes( ! empty( $opts_settings['track_peak'] ) );
603 $context = self::collect_context();
604 $now = time();
605 $state = self::state_for_usage( $usage, $limit, $opts_settings );
606 $opts['st'] = $state;
607 $opts['pe'] = (int) $usage;
608 $opts['li'] = (int) $limit;
609 self::log_event( $opts, $opts_settings, $context );
610 }
611
612 #private static function is_favicon_wpjson_call() {
613 # $request_uri = $_SERVER['REQUEST_URI'];
614 # return !(strpos($request_uri, '/wp-json/') !== false && strpos($request_uri, 'favicon.ico') !== false);
615 #}
616
617 private static function send_email_alert( $to, $state, $usage, $limit, $context ) {
618 $site = wp_parse_url( home_url(), PHP_URL_HOST );
619 $subject = sprintf(
620 '[%s] Memory %s (%s)',
621 $site ? $site : 'WordPress',
622 strtoupper( $state ),
623 self::format_pct( $usage, $limit )
624 );
625 $lines = array();
626 $lines[] = __('WP Memory Threshold Alert', 'wp-memory-usage' );
627 $lines[] = '';
628 $lines[] = __('State: ', 'wp-memory-usage' ) . strtoupper( $state );
629 $lines[] = __('Usage: ', 'wp-memory-usage' ) . self::format_bytes( $usage );
630 $lines[] = __('Limit: ', 'wp-memory-usage' ) . ( $limit > 0 ? self::format_bytes( $limit ) : __('unlimited', 'wp-memory-usage' ) );
631 $lines[] = __('Ratio: ', 'wp-memory-usage' ) . self::format_pct( $usage, $limit );
632 $lines[] = '';
633 $lines[] = __('Context:', 'wp-memory-usage' );
634 $lines[] = __('- Type:', 'wp-memory-usage' ) .' ' . (string) ( isset( $context['type'] ) ? $context['type'] : '' );
635 $lines[] = __('- Method:', 'wp-memory-usage' ).' ' . (string) ( isset( $context['method'] ) ? $context['method'] : '' );
636 $lines[] = __('- URI:', 'wp-memory-usage' ).' '. (string) ( isset( $context['uri'] ) ? $context['uri'] : '' );
637 if ( ! empty( $context['admin_screen'] ) ) { $lines[] = __('- Admin screen:', 'wp-memory-usage' ).' '. (string) $context['admin_screen']; }
638 if ( ! empty( $context['post_id'] ) ) { $lines[] = __('- Post ID:', 'wp-memory-usage' ) .' ' . (string) $context['post_id']; }
639 if ( ! empty( $context['ajax_action'] ) ) { $lines[] = __('- AJAX action:', 'wp-memory-usage' ) .' ' . (string) $context['ajax_action']; }
640 if ( ! empty( $context['rest_route'] ) ) { $lines[] = __('- REST route:', 'wp-memory-usage' ).' ' . (string) $context['rest_route']; }
641 if ( ! empty( $context['user'] ) ) { $lines[] = __('- User:', 'wp-memory-usage' ) .' ' . (string) $context['user']; }
642 $lines[] = '';
643 $lines[] = __('Suggested actions:', 'wp-memory-usage' );
644 $lines[] = __('- Reproduce this URL/action and temporarily disable recently added/updated plugins', 'wp-memory-usage' );
645 $lines[] = __('- If it is an import/editor screen, try with fewer plugins active', 'wp-memory-usage' );
646 $lines[] = __('- Consider raising PHP memory_limit / WP_MEMORY_LIMIT if appropriate', 'wp-memory-usage' );
647 $lines[] = '';
648 $lines[] = __('Settings: ', 'wp-memory-usage' ) . admin_url( 'options-general.php?page=wpmu-memory-alerts' );
649 $headers = array( 'Content-Type: text/plain; charset=UTF-8' );
650 return (bool) wp_mail( $to, $subject, implode( "\n", $lines ), $headers );
651 }
652
653 /** ---------------- Digest email ---------------- */
654 public static function send_digest_email( $to, $aggregation, $filename ) {
655 $site = wp_parse_url( home_url(), PHP_URL_HOST );
656 $site = $site ? $site : 'WordPress';
657 $warn = (int) ( $aggregation['status']['warn'] ?? 0 );
658 $danger = (int) ( $aggregation['status']['danger'] ?? 0 );
659 $critical = (int) ( $aggregation['status']['critical'] ?? 0 );
660 $total = $warn + $danger + $critical + (int) ( $aggregation['status']['ok'] ?? 0 );
661
662 // Subject: höchste Severity hervorheben
663 $severity = $critical > 0 ? __('CRITICAL','wp-memory-usage') : ( $danger > 0 ? __('DANGER','wp-memory-usage') : __('WARN','wp-memory-usage') );
664 $subject = sprintf( '[%s] Memory Digest %s – warn:%d danger:%d critical:%d', $site, $severity, $warn, $danger, $critical );
665
666 $lines = array();
667 $lines[] = __('WP Memory Usage – Digest Report', 'wp-memory-usage' );
668 $lines[] = str_repeat( '-', 50 );
669 $lines[] = __('File : ', 'wp-memory-usage' ) . $filename;
670 $lines[] = __('Site : ', 'wp-memory-usage' ) . home_url();
671 $lines[] = '';
672 $lines[] = __('STATUS SUMMARY', 'wp-memory-usage' );
673 $lines[] = ' '.__('OK : ', 'wp-memory-usage' ) . (int) ( $aggregation['status']['ok'] ?? 0 );
674 $lines[] = ' '.__('WARN : ', 'wp-memory-usage' ) . $warn;
675 $lines[] = ' '.__('DANGER : ', 'wp-memory-usage' ) . $danger;
676 $lines[] = ' '.__('CRITICAL : ', 'wp-memory-usage' ) . $critical;
677 $lines[] = ' '.__('Total : ', 'wp-memory-usage' ) . $total;
678 $lines[] = '';
679
680 // Zeitintervall
681 $first = $aggregation['interval']['first'] ?? 0;
682 $last = $aggregation['interval']['last'] ?? 0;
683 if ( $first && $last ) {
684 $lines[] = __('TIME RANGE', 'wp-memory-usage' );
685 $lines[] = ' '.__('From : ', 'wp-memory-usage' ) . wp_date( 'Y-m-d H:i:s', $first );
686 $lines[] = ' '.__('To : ', 'wp-memory-usage' ) . wp_date( 'Y-m-d H:i:s', $last );
687 $dur = $last - $first;
688 $lines[] = ' '.__('Dur : ', 'wp-memory-usage' ) . floor( $dur / 60 ) . ' min ' . ( $dur % 60 ) . ' sec';
689 $lines[] = '';
690 }
691
692 // Request-Typen
693 if ( ! empty( $aggregation['type'] ) ) {
694 $lines[] = __('REQUEST TYPES', 'wp-memory-usage' );
695 foreach ( $aggregation['type'] as $type => $cnt ) {
696 $lines[] = sprintf( ' %-12s: %d', strtoupper( $type ), $cnt );
697 }
698 $lines[] = '';
699 }
700
701 // Top-URIs mit Alerts (max. 20, sortiert nach warn+danger+critical)
702 if ( ! empty( $aggregation['uri'] ) ) {
703 $uris_with_alerts = array_filter( $aggregation['uri'], function ( $u ) {
704 return ( ( $u['warn'] ?? 0 ) + ( $u['danger'] ?? 0 ) + ( $u['critical'] ?? 0 ) ) > 0;
705 } );
706 uasort( $uris_with_alerts, function ( $a, $b ) {
707 $sa = ( $a['warn'] ?? 0 ) + ( $a['danger'] ?? 0 ) * 2 + ( $a['critical'] ?? 0 ) * 3;
708 $sb = ( $b['warn'] ?? 0 ) + ( $b['danger'] ?? 0 ) * 2 + ( $b['critical'] ?? 0 ) * 3;
709 return $sb <=> $sa;
710 } );
711 $lines[] = __('TOP URIs WITH ALERTS (warn/danger/critical | avg | max)', 'wp-memory-usage' );
712 $count = 0;
713 foreach ( $uris_with_alerts as $uri => $ud ) {
714 if ( ++$count > 20 ) { break; }
715 $lines[] = sprintf(
716 ' %s',
717 $uri
718 );
719 $lines[] = sprintf(
720 ' W:%d D:%d C:%d | total:%d | avg:%s | max:%s',
721 $ud['warn'] ?? 0,
722 $ud['danger'] ?? 0,
723 $ud['critical'] ?? 0,
724 $ud['total'] ?? 0,
725 self::format_bytes( $ud['avg_usage'] ?? 0 ),
726 self::format_bytes( $ud['max_usage'] ?? 0 )
727 );
728 }
729 $lines[] = '';
730 }
731
732 $lines[] = __( 'Settings:', 'wp-memory-usage' ) . admin_url( 'options-general.php?page=wpmu-memory-alerts&tab=digest' );
733 $headers = array( 'Content-Type: text/plain; charset=UTF-8' );
734 return (bool) wp_mail( $to, $subject, implode( "\n", $lines ), $headers );
735 }
736
737 /** ---------------- Digest ---------------- */
738 private static function ensure_digest_schedule( $hour ) {
739 $hour = (int) $hour;
740 if ( $hour < 0 || $hour > 23 ) { $hour = 8; }
741 if ( wp_next_scheduled( self::DIGEST_HOOK ) ) { return; }
742 $now = current_time( 'timestamp' );
743 $run = strtotime( gmdate( 'Y-m-d', $now ) . sprintf( ' %02d:00:00', $hour ) );
744 if ( $run <= $now ) { $run = strtotime( '+1 day', $run ); }
745 wp_schedule_event( $run, 'daily', self::DIGEST_HOOK );
746 }
747
748 /** ---------------- Context capture ---------------- */
749 private static function collect_context() {
750 $uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
751 #$method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( (string) $_SERVER['REQUEST_METHOD'] ) : '';
752 $method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : '';
753 $is_ajax = function_exists( 'wp_doing_ajax' ) && wp_doing_ajax();
754 $is_cron = function_exists( 'wp_doing_cron' ) && wp_doing_cron();
755 $is_rest = ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || ( '' !== $uri && false !== strpos( $uri, '/wp-json/' ) );
756 $is_admin = is_admin();
757 $type = $is_cron ? 'cron' : ( $is_ajax ? 'ajax' : ( $is_rest ? 'rest' : ( $is_admin ? 'admin' : 'front' ) ) );
758 $ajax_action = '';
759 // phpcs:disable WordPress.Security.NonceVerification.Recommended
760 if ( $is_ajax && isset( $_REQUEST['action'] ) ) {
761 $ajax_action = sanitize_key( wp_unslash( $_REQUEST['action'] ) );
762 }
763 // phpcs:enable WordPress.Security.NonceVerification.Recommended
764 $rest_route = '';
765 if ( $is_rest ) {
766 if ( isset( $GLOBALS['wp'] ) && is_object( $GLOBALS['wp'] ) && isset( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
767 $rest_route = (string) $GLOBALS['wp']->query_vars['rest_route'];
768 } else {
769 $rest_route = $uri;
770 }
771 }
772 $admin_screen = '';
773 if ( $is_admin && function_exists( 'get_current_screen' ) ) {
774 $screen = get_current_screen();
775 if ( $screen && ! empty( $screen->id ) ) {
776 $admin_screen = (string) $screen->id;
777 }
778 }
779 global $post;
780 $post_id = isset( $post->ID ) ? (string) $post->ID : 'unknown';
781 $user_str = '';
782 $user = wp_get_current_user();
783 if ( $user && $user->exists() ) {
784 $roles = is_array( $user->roles ) ? implode( ',', $user->roles ) : '';
785 $user_str = sprintf( '%s (#%d)%s', $user->user_login, (int) $user->ID, $roles ? ' roles:' . $roles : '' );
786 }
787 return array(
788 'type' => $type,
789 'method' => $method,
790 'uri' => $uri,
791 'admin_screen' => $admin_screen,
792 'post_id' => $post_id ? $post_id : '',
793 'ajax_action' => $ajax_action,
794 'rest_route' => $rest_route,
795 'user' => $user_str,
796 'is_admin' => $is_admin ? 1 : 0,
797 'is_ajax' => $is_ajax ? 1 : 0,
798 'is_cron' => $is_cron ? 1 : 0,
799 'is_rest' => $is_rest ? 1 : 0,
800 );
801 }
802
803 /** ---------------- set limits ---------------- */
804 private static function log_event( &$opts, $opts_settings, $ctx ) {
805 if ( ( ! $opts_settings["log_admin"] ) && ( $ctx['is_admin'] ) ) {
806 #error_log("NO LOG admin");
807 return NULL;
808 }
809 if ( ( ! $opts_settings["log_ok"] ) && ( $opts['st'] === "ok" ) ) {
810 return NULL;
811 }
812 if ( ( ! $opts_settings["logop_warn_pct"] ) && ( $opts['st'] === "warn" ) ) {
813 #error_log("NO LOG warn");
814 return NULL;
815 }
816 if ( ( ! $opts_settings["logop_danger_pct"] ) && ( $opts['st'] === "danger" ) ) {
817 #error_log("NO LOG danger");
818 return NULL;
819 }
820 if ( ( ! $opts_settings["logop_critical_pct"] ) && ( $opts['st'] === "critical" ) ) {
821 #error_log("NO LOG critical");
822 return NULL;
823 }
824 if ( ( ! $opts_settings["log_rest"] ) && ( $ctx['is_rest'] ) ) { return NULL; }
825 if ( ( ! $opts_settings["log_ajax"] ) && ( $ctx['is_ajax'] ) ) { return NULL; }
826 if ( ( ! $opts_settings["log_cron"] ) && ( $ctx['is_cron'] ) ) { return NULL; }
827 if ( preg_match("/favicon.ico/", $ctx['uri']) ) {
828 #error_log("NO LOG favicon");
829 return TRUE;
830 }
831 $optsout = array(
832 't' => time(),
833 's' => (string) $opts['st'],
834 'u' => (int) $opts['pe'],
835 'l' => (int) $opts['li'],
836 'c' => array(
837 'type' => isset( $ctx['type'] ) ? (string) $ctx['type'] : '',
838 'uri' => isset( $ctx['uri'] ) ? (string) $ctx['uri'] : '',
839 'admin_screen' => isset( $ctx['admin_screen'] ) ? (string) $ctx['admin_screen'] : '',
840 'ajax_action' => isset( $ctx['ajax_action'] ) ? (string) $ctx['ajax_action'] : '',
841 'rest_route' => isset( $ctx['rest_route'] ) ? (string) $ctx['rest_route'] : '',
842 'user' => isset( $ctx['user'] ) ? (string) $ctx['user'] : '',
843 'is_admin' => ! empty( $ctx['is_admin'] ) ? 1 : 0,
844 ),
845 );
846 self::add_log( $optsout );
847 }
848
849 #############################################################
850 # DONE
851 #############################################################
852
853 ### UI
854 public static function register_settings() {
855 register_setting(
856 'wpmu_threshold_settings',
857 self::OPTION_KEY,
858 array( __CLASS__, 'sanitize_options_file' )
859 );
860 // Thresholds
861 add_settings_section(
862 'wpmu_main',
863 '<h2 style="background-color: '.self::SETTING_COLOR_BACK.'; padding: 10px; color: '.self::SETTING_COLOR_FONT.'; border: 2px solid '.self::SETTING_COLOR_BORDER.';">'
864 . esc_html__( 'Thresholds', 'wp-memory-usage' ) . "</h2>",
865 array( __CLASS__, 'section_intro_thresholds' ),
866 'wpmu-memory-alerts'
867 );
868 #$fields = array(
869 # 'warn_pct' => esc_html__( 'Warning threshold', 'wp-memory-usage' ),
870 # 'danger_pct' => esc_html__( 'Danger threshold', 'wp-memory-usage' ),
871 # 'critical_pct' => esc_html__( 'Critical threshold', 'wp-memory-usage' ),
872 #);
873 $fields = array(
874 'warn_pct' => array(
875 'label' => esc_html__( 'Warning threshold', 'wp-memory-usage' ),
876 'log_label' => esc_html__( 'Log Warning', 'wp-memory-usage' ),
877 ),
878 'danger_pct' => array(
879 'label' => esc_html__( 'Danger threshold', 'wp-memory-usage' ),
880 'log_label' => esc_html__( 'Log Danger', 'wp-memory-usage' ),
881 ),
882 'critical_pct' => array(
883 'label' => esc_html__( 'Critical threshold', 'wp-memory-usage' ),
884 'log_label' => esc_html__( 'Log Critical', 'wp-memory-usage' ),
885 ),
886 );
887 #foreach ( $fields as $key => $label ) {
888 foreach ( $fields as $key => $field ) {
889 add_settings_field(
890 $key,
891 #esc_html( $label . " %" ),
892 esc_html( $field['label'] . ' %' ),
893 array( __CLASS__, 'render_field' ),
894 'wpmu-memory-alerts',
895 'wpmu_main',
896 array( 'key' => $key )
897 );
898 add_settings_field(
899 ( "logop_" . $key ),
900 $field['log_label'],
901 #esc_html__( 'Log ' . $fields[ $key ], 'wp-memory-usage' ),
902 array( __CLASS__, 'render_checkbox' ),
903 'wpmu-memory-alerts',
904 'wpmu_main',
905 array( 'key' => ( "logop_" . $key ) )
906 );
907 }
908 // How to measure
909 add_settings_section(
910 'wpmu_measure',
911 '<h2 style="background-color: '.self::SETTING_COLOR_BACK.'; padding: 10px; color: '.self::SETTING_COLOR_FONT.'; border: 2px solid '.self::SETTING_COLOR_BORDER.';">'
912 . esc_html__( 'How to measure', 'wp-memory-usage' ) . "</h2>",
913 array( __CLASS__, 'section_intro_measure' ),
914 'wpmu-memory-alerts'
915 );
916 add_settings_field(
917 'track_peak',
918 esc_html__( 'Use peak memory (recommended)', 'wp-memory-usage' ),
919 array( __CLASS__, 'render_checkbox' ),
920 'wpmu-memory-alerts',
921 'wpmu_measure',
922 array( 'key' => 'track_peak' )
923 );
924 // Logging
925 add_settings_section(
926 'wpmu_logging',
927 '<h2 style="background-color: '.self::SETTING_COLOR_BACK.'; padding: 10px; color: '.self::SETTING_COLOR_FONT.'; border: 2px solid '.self::SETTING_COLOR_BORDER.';">'
928 . esc_html__( 'Logging', 'wp-memory-usage' ) . "</h2>",
929 array( __CLASS__, 'section_intro_log' ),
930 'wpmu-memory-alerts'
931 );
932
933
934 $log_fields = array(
935 'log_ajax' => esc_html__( 'Log Ajax', 'wp-memory-usage' ),
936 'log_rest' => esc_html__( 'Log Rest', 'wp-memory-usage' ),
937 'log_admin' => esc_html__( 'Log Admin', 'wp-memory-usage' ),
938 'log_cron' => esc_html__( 'Log Cron', 'wp-memory-usage' ),
939 'log_favicon' => esc_html__( 'Log favicon.ico', 'wp-memory-usage' ),
940 'log_ok' => esc_html__( 'Log OK', 'wp-memory-usage' ),
941 );
942
943 foreach ( $log_fields as $logkey => $label ) {
944
945
946
947 #foreach ( array( 'log_ajax', 'log_rest', 'log_admin', 'log_cron', 'log_favicon', 'log_ok' ) as $logkey ) {
948 #$labels = array(
949 # 'log_ajax' => 'Log Ajax',
950 # 'log_rest' => 'Log Rest',
951 # 'log_admin' => 'Log Admin',
952 # 'log_cron' => 'Log Cron',
953 # 'log_favicon' => 'Log favicon.ico',
954 # 'log_ok' => 'Log OK',
955 #);
956 add_settings_field(
957 $logkey,
958 #esc_html__( $labels[ $logkey ], 'wp-memory-usage' ),
959 $label,
960 array( __CLASS__, 'render_checkbox' ),
961 'wpmu-memory-alerts',
962 'wpmu_logging',
963 array( 'key' => $logkey )
964 );
965 }
966 // Alert settings
967 add_settings_section(
968 'wpmu_alertsettings',
969 '<h2 style="background-color: '.self::SETTING_COLOR_BACK.'; padding: 10px; color: '.self::SETTING_COLOR_FONT.'; border: 2px solid '.self::SETTING_COLOR_BORDER.';">'
970 . esc_html__( 'How to alert', 'wp-memory-usage' ) . "</h2>",
971 array( __CLASS__, 'section_intro_howalert' ),
972 'wpmu-memory-alerts'
973 );
974 add_settings_field( 'email_to', esc_html__('Alert email recipient','wp-memory-usage' ), array( __CLASS__, 'render_field' ), 'wpmu-memory-alerts', 'wpmu_alertsettings', array( 'key' => 'email_to' ) );
975 add_settings_field( 'send_email', esc_html__('Send email alerts','wp-memory-usage' ), array( __CLASS__, 'render_checkbox' ), 'wpmu-memory-alerts', 'wpmu_alertsettings', array( 'key' => 'send_email' ) );
976 add_settings_field( 'rotate_log', esc_html__('Digest interval','wp-memory-usage' ), array( __CLASS__, 'render_field' ), 'wpmu-memory-alerts', 'wpmu_alertsettings', array( 'key' => 'rotate_log' ) );
977 add_settings_field( 'del_frequency_logfiles', esc_html__('Delete rotated Logfiles','wp-memory-usage' ), array( __CLASS__, 'render_field' ), 'wpmu-memory-alerts', 'wpmu_alertsettings', array( 'key' => 'del_frequency_logfiles' ) );
978 }
979
980 public static function render_settings_page() {
981 if ( ! current_user_can( self::CAP ) ) {
982 wp_die( esc_html__( 'Insufficient permissions.', 'wp-memory-usage' ) );
983 }
984 $opts = self::get_settings();
985 $tab = 'history';
986 if ( isset( $_GET['tab'], $_GET['_wpmu_tab_nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpmu_tab_nonce'] ) ), 'wpmu_tab_nav' ) ) {
987 $tab = sanitize_key( wp_unslash( $_GET['tab'] ) );
988 }
989 if ( ! in_array( $tab, array( 'settings', 'current', 'actions', 'digest', 'history', 'check_installation' ), true ) ) {
990 $tab = 'history';
991 }
992 $page_url = admin_url( 'options-general.php?page=wpmu-memory-alerts' );
993 $tab_nonce = wp_create_nonce( 'wpmu_tab_nav' );
994 ?>
995 <div class="wrap">
996 <h1><?php echo esc_html__( 'WP Memory Usage', 'wp-memory-usage' ); ?> <?php echo esc_html__( 'Threshold Alerts', 'wp-memory-usage' ); ?></h1>
997 <h2 class="nav-tab-wrapper" style="margin-top: 12px;">
998 <?php
999 $tabs = array(
1000 'settings' => esc_html__( '⚙️ Settings', 'wp-memory-usage' ),
1001 'history' => esc_html__( '📋 History', 'wp-memory-usage' ),
1002 'digest' => esc_html__( '📊 Digest', 'wp-memory-usage' ),
1003 'actions' => esc_html__( '🛠️ Actions', 'wp-memory-usage' ),
1004 'current' => esc_html__( '📏 Memory Thresholds', 'wp-memory-usage' ),
1005 'check_installation' => esc_html__( '🔍 Check Installation', 'wp-memory-usage' ),
1006 );
1007 # );
1008 foreach ( $tabs as $t => $label ) :
1009 ?>
1010 <a href="<?php echo esc_url( add_query_arg( array( 'tab' => $t, '_wpmu_tab_nonce' => $tab_nonce ), $page_url ) ); ?>"
1011 class="nav-tab <?php echo ( $t === $tab ) ? 'nav-tab-active' : ''; ?>">
1012 <?php echo esc_html($label); ?>
1013 </a>
1014 <?php endforeach; ?>
1015 </h2>
1016
1017 <?php if ( 'settings' === $tab ) : ?>
1018 <form method="post" action="options.php">
1019 <?php
1020 settings_fields( 'wpmu_threshold_settings' );
1021 do_settings_sections( 'wpmu-memory-alerts' );
1022 submit_button();
1023 ?>
1024 </form>
1025
1026 <?php elseif ( 'current' === $tab ) : ?>
1027 <?php
1028 $php_limit_raw = ini_get( 'memory_limit' );
1029 $wp_limit_raw = defined( 'WP_MEMORY_LIMIT' ) ? WP_MEMORY_LIMIT : '';
1030 $wpmax_raw = defined( 'WP_MAX_MEMORY_LIMIT' ) ? WP_MAX_MEMORY_LIMIT : '';
1031 $php_limit_b = self::parse_size_to_bytes( is_string( $php_limit_raw ) ? $php_limit_raw : '' );
1032 $wp_limit_b = self::parse_size_to_bytes( is_string( $wp_limit_raw ) ? $wp_limit_raw : '' );
1033 $wpmax_b = self::parse_size_to_bytes( is_string( $wpmax_raw ) ? $wpmax_raw : '' );
1034 $effective_b = self::get_effective_memory_limit_bytes();
1035 $effective_mb = $effective_b > 0 ? (int) round( $effective_b / 1048576 ) : 0;
1036
1037 // Threshold settings
1038 $s_opts = self::get_settings();
1039 $warn_pct = (int) ( $s_opts['warn_pct'] ?? 70 );
1040 $danger_pct = (int) ( $s_opts['danger_pct'] ?? 85 );
1041 $critical_pct = (int) ( $s_opts['critical_pct'] ?? 95 );
1042 $warn_mb = $effective_mb > 0 ? round( $effective_mb * $warn_pct / 100, 1 ) : null;
1043 $danger_mb = $effective_mb > 0 ? round( $effective_mb * $danger_pct / 100, 1 ) : null;
1044 $critical_mb = $effective_mb > 0 ? round( $effective_mb * $critical_pct / 100, 1 ) : null;
1045
1046 // ── Bewertungen ────────────────────────────────────────────────
1047 $recs = array(); // ['icon','color','bg','text']
1048
1049 // Effektives Limit
1050 if ( $effective_mb <= 0 ) {
1051 $recs[] = array( 'icon' => '🆘', 'color' => '#8B0000', 'bg' => '#fdf2f2',
1052 'text' => __('Could not determine the effective memory limit. Check that WP_MEMORY_LIMIT or PHP memory_limit are set.', 'wp-memory-usage') );
1053 } elseif ( $effective_mb < 64 ) {
1054 $recs[] = array( 'icon' => '🆘', 'color' => '#8B0000', 'bg' => '#fdf2f2',
1055 'text' => __('Effective limit is very low:', 'wp-memory-usage').$effective_mb.' MB.'.__('Most WordPress sites need at least 128 MB; WooCommerce, page builders or heavy plugins often need 256 MB or more. You will likely see out-of-memory errors.', 'wp-memory-usage') );
1056 } elseif ( $effective_mb < 128 ) {
1057 $recs[] = array( 'icon' => '⚠️', 'color' => '#7a5200', 'bg' => '#fef9ec',
1058 'text' => __('Effective limit is', 'wp-memory-usage').' {'.($effective_mb).' MB.'.__('Tight for a typical WordPress site. Consider raising to at least 128 MB (256 MB recommended). Add to wp-config.php: define(\'WP_MEMORY_LIMIT\', \'256M\');', 'wp-memory-usage') );
1059 } elseif ( $effective_mb < 256 ) {
1060 $recs[] = array( 'icon' => '�
1061 ', 'color' => '#2d6a2d', 'bg' => '#f2faf2',
1062 'text' => __('Effective limit is', 'wp-memory-usage').' {'.($effective_mb).' MB.'.__('Acceptable for most sites. For WooCommerce, LMS or heavy builders, 256 MB+ is better.', 'wp-memory-usage') );
1063 } else {
1064 $recs[] = array( 'icon' => '�
1065 ', 'color' => '#2d6a2d', 'bg' => '#f2faf2',
1066 'text' => __('Effective limit is', 'wp-memory-usage').' ('.($effective_mb).' MB.'.__('Good.', 'wp-memory-usage') );
1067 }
1068
1069 // PHP vs WP Limit-Verhältnis
1070 if ( $php_limit_b > 0 && $wp_limit_b > 0 && $wp_limit_b > $php_limit_b ) {
1071 $recs[] = array( 'icon' => '⚠️', 'color' => '#7a5200', 'bg' => '#fef9ec',
1072 'text' => 'WP_MEMORY_LIMIT (' . ( (string) $wp_limit_raw ) . ') '.__('is higher than PHP memory_limit', 'wp-memory-usage').' (' . ( (string) $php_limit_raw ) . '). '.__('WordPress cannot exceed the PHP ceiling – the PHP limit wins. Raise memory_limit in php.ini / .htaccess / php_value.', 'wp-memory-usage') );
1073 }
1074 if ( $wp_limit_b > 0 && $wpmax_b > 0 && $wp_limit_b > $wpmax_b ) {
1075 $recs[] = array( 'icon' => '⚠️', 'color' => '#7a5200', 'bg' => '#fef9ec',
1076 'text' => 'WP_MEMORY_LIMIT (' . ( (string) $wp_limit_raw ) . ') '.__('is higher than WP_MAX_MEMORY_LIMIT', 'wp-memory-usage').' (' . ( (string) $wpmax_raw ) . '). '.__('WP_MAX_MEMORY_LIMIT should be ≥ WP_MEMORY_LIMIT.', 'wp-memory-usage') );
1077 }
1078
1079 // Threshold-Abstände
1080 $gap_warn_danger = $danger_pct - $warn_pct;
1081 $gap_danger_crit = $critical_pct - $danger_pct;
1082 if ( $gap_warn_danger < 5 ) {
1083 $recs[] = array( 'icon' => '⚠️', 'color' => '#7a5200', 'bg' => '#fef9ec',
1084 'text' => __( 'Warning ({', 'wp-memory-usage').($warn_pct).'}%) '.__('and Danger', 'wp-memory-usage').' ({'.($danger_pct).'}%) '.__('thresholds are very close together (gap:', 'wp-memory-usage').' {'.($gap_warn_danger).'}%). '.__('Consider a gap of at least 10% so you have time to react between levels.', 'wp-memory-usage') );
1085 }
1086 if ( $gap_danger_crit < 5 ) {
1087 $recs[] = array( 'icon' => '⚠️', 'color' => '#7a5200', 'bg' => '#fef9ec',
1088 'text' => __('Danger', 'wp-memory-usage').' ({'.($danger_pct).'}%) '.__('and Critical', 'wp-memory-usage').' ({'.($critical_pct).'}%) '.__('thresholds are very close together (gap', 'wp-memory-usage' ).': {'.($gap_danger_crit).'}%). '.__('Consider a gap of at least 5%.', 'wp-memory-usage' ));
1089 }
1090 if ( $warn_pct < 50 ) {
1091 $recs[] = array( 'icon' => '⚠️', 'color' => '#7a5200', 'bg' => '#fef9ec',
1092 'text' => __('Warning threshold is very low', 'wp-memory-usage').' ({'.($warn_pct).'}%). '.__('You will receive many false-positive alerts on normal pages. A value of 65–75% is typical.', 'wp-memory-usage') );
1093 }
1094 if ( $critical_pct < 90 ) {
1095 $recs[] = array( 'icon' => '⚠️', 'color' => '#7a5200', 'bg' => '#fef9ec',
1096 'text' => __('Critical threshold is set to', 'wp-memory-usage').' {'.($critical_pct).'}% – '.__('that leaves little headroom before actual out-of-memory errors. Consider raising to 92–95%.', 'wp-memory-usage') );
1097 }
1098 if ( $critical_pct > 98 ) {
1099 $recs[] = array( 'icon' => '⚠️', 'color' => '#7a5200', 'bg' => '#fef9ec',
1100 'text' => __('Critical threshold is very high', 'wp-memory-usage').' ({'.($critical_pct).'}%). '.__('At this level you may already be getting OOM errors before the alert fires. 95% is a safer upper bound.', 'wp-memory-usage') );
1101 }
1102 if ( count( $recs ) === 1 && $recs[0]['icon'] === '�
1103 ' ) {
1104 $recs[] = array( 'icon' => '�
1105 ', 'color' => '#2d6a2d', 'bg' => '#f2faf2',
1106 'text' => __('Threshold settings look good: Warn', 'wp-memory-usage').' {'.($warn_pct).'}% / '.__('Danger', 'wp-memory-usage').' {'.($danger_pct).'}% / '.__('Critical', 'wp-memory-usage').' {'.($critical_pct).'}%.' );
1107 }
1108 ?>
1109 <p><?php echo esc_html__( 'This tab shows the memory limits that actually apply to your site. Memory issues are often caused by a limit that is lower than expected.', 'wp-memory-usage' ); ?></p>
1110 <table class="widefat striped" style="max-width: 1000px;">
1111 <thead><tr>
1112 <th><?php echo esc_html__( 'Setting', 'wp-memory-usage' ); ?></th>
1113 <th><?php echo esc_html__( 'Value', 'wp-memory-usage' ); ?></th>
1114 <th><?php echo esc_html__( 'Meaning', 'wp-memory-usage' ); ?></th>
1115 </tr></thead>
1116 <tbody>
1117 <tr>
1118 <td><code>WP_MEMORY_LIMIT</code></td>
1119 <td><?php echo $wp_limit_raw ? esc_html( (string) $wp_limit_raw ) : '<em>' . esc_html__( 'not defined', 'wp-memory-usage' ) . '</em>'; ?></td>
1120 <td><?php echo esc_html__( 'Memory limit for the regular (frontend) WordPress runtime. WordPress may increase the PHP limit up to this value if possible.', 'wp-memory-usage' ); ?></td>
1121 </tr>
1122 <tr>
1123 <td><code>WP_MAX_MEMORY_LIMIT</code></td>
1124 <td><?php echo $wpmax_raw ? esc_html( (string) $wpmax_raw ) : '<em>' . esc_html__( 'not defined', 'wp-memory-usage' ) . '</em>'; ?></td>
1125 <td><?php echo esc_html__( 'Memory limit for admin-area tasks that can be heavier (updates, editor, imports). This is often higher than WP_MEMORY_LIMIT.', 'wp-memory-usage' ); ?></td>
1126 </tr>
1127 <tr>
1128 <td><code>PHP memory_limit</code></td>
1129 <td><?php echo $php_limit_raw ? esc_html( (string) $php_limit_raw ) : '<em>' . esc_html__( 'unknown', 'wp-memory-usage' ) . '</em>'; ?></td>
1130 <td><?php echo esc_html__( 'The PHP-level memory limit set by your server / hosting. This is the hard ceiling unless you can raise it in PHP configuration.', 'wp-memory-usage' ); ?></td>
1131 </tr>
1132 <tr>
1133 <td><strong><?php echo esc_html__( 'Effective limit used for alerts', 'wp-memory-usage' ); ?></strong></td>
1134 <td><strong><?php echo $effective_b > 0 ? esc_html( self::format_bytes( (int) $effective_b ) ) : esc_html__( 'unlimited/unknown', 'wp-memory-usage' ); ?></strong></td>
1135 <td><?php echo esc_html__( 'For safety, this plugin uses the lower of the WordPress and PHP limits (when both are set). That reflects what will actually break first.', 'wp-memory-usage' ); ?></td>
1136 </tr>
1137 </tbody>
1138 </table>
1139
1140 <?php if ( $effective_mb > 0 ) : ?>
1141 <h3 style="margin-top:20px;"><?php echo esc_html__( 'Alert thresholds in absolute values', 'wp-memory-usage' ); ?></h3>
1142 <table class="widefat striped" style="max-width:600px;">
1143 <thead><tr>
1144 <th><?php echo esc_html__( 'Level', 'wp-memory-usage' ); ?></th>
1145 <th><?php echo esc_html__( '%', 'wp-memory-usage' ); ?></th>
1146 <th><?php echo esc_html__( '≈ MB', 'wp-memory-usage' ); ?></th>
1147 </tr></thead>
1148 <tbody>
1149 <tr><td><strong style="color:#f0ad4e;"> <?php echo esc_html__( 'Warn', 'wp-memory-usage' ); ?></strong></td>
1150 <td><?php echo esc_html($warn_pct); ?>%</td>
1151 <td><?php echo esc_html($warn_mb !== null ? $warn_mb . ' MB' : ''); ?></td></tr>
1152 <tr><td><strong style="color:#d9534f;">🔴 <?php echo esc_html__( 'Danger', 'wp-memory-usage' ); ?></strong></td>
1153 <td><?php echo esc_html($danger_pct); ?>%</td>
1154 <td><?php echo esc_html($danger_mb !== null ? $danger_mb . ' MB' : ''); ?></td></tr>
1155 <tr><td><strong style="color:#8B0000;">🆘 <?php echo esc_html__( 'Critical', 'wp-memory-usage' ); ?></strong></td>
1156 <td><?php echo esc_html($critical_pct); ?>%</td>
1157 <td><?php echo esc_html($critical_mb !== null ? $critical_mb . ' MB' : ''); ?></td></tr>
1158 </tbody>
1159 </table>
1160 <?php endif; ?>
1161
1162 <h3 style="margin-top:20px;"><?php echo esc_html__( 'Assessment & Recommendations', 'wp-memory-usage' ); ?></h3>
1163 <?php foreach ( $recs as $rec ) : ?>
1164 <div style="display:flex;align-items:flex-start;gap:10px;padding:10px 14px;margin-bottom:8px;border-radius:5px;border-left:4px solid <?php echo esc_attr( $rec['color'] ); ?>;background:<?php echo esc_attr( $rec['bg'] ); ?>;">
1165 <span style="font-size:18px;line-height:1.3;"><?php echo esc_html($rec['icon']); ?></span>
1166 <span style="color:<?php echo esc_attr( $rec['color'] ); ?>;font-size:13px;"><?php echo esc_html( $rec['text'] ); ?></span>
1167 </div>
1168 <?php endforeach; ?>
1169
1170 <?php elseif ( 'actions' === $tab ) : ?>
1171 <h2><?php echo esc_html__( 'What you can do', 'wp-memory-usage' ); ?></h2>
1172 <p><?php echo esc_html__( 'This tab explains practical next steps when you receive a memory alert. You do not need to be a developer to apply most of these actions.', 'wp-memory-usage' ); ?></p>
1173 <h3><?php echo esc_html__( 'Step 1: Understand the severity', 'wp-memory-usage' ); ?></h3>
1174 <ul style="list-style: disc; padding-left: 20px;">
1175 <li><strong><?php echo esc_html__( 'Warning', 'wp-memory-usage' ); ?></strong>: <?php echo esc_html__( 'Close to the limit. Usually no immediate outage, but you should watch it.', 'wp-memory-usage' ); ?></li>
1176 <li><strong><?php echo esc_html__( 'Danger', 'wp-memory-usage' ); ?></strong>: <?php echo esc_html__( 'High risk. Actions like editing, imports, backups or WooCommerce tasks may fail.', 'wp-memory-usage' ); ?></li>
1177 <li><strong><?php echo esc_html__( 'Critical', 'wp-memory-usage' ); ?></strong>: <?php echo esc_html__( 'Very likely to trigger "Allowed memory size exhausted". Act now to avoid outages.', 'wp-memory-usage' ); ?></li>
1178 </ul>
1179 <h3><?php echo esc_html__( 'Step 2: Identify what triggered it', 'wp-memory-usage' ); ?></h3>
1180 <p><?php echo esc_html__( 'Open the "Digest" or "History" tab and look at the context (URL, admin screen, AJAX action, REST route, cron). This tells you which page is involved.', 'wp-memory-usage' ); ?></p>
1181 <h3><?php echo esc_html__( 'Common actions', 'wp-memory-usage' ); ?></h3>
1182
1183
1184 <ol style="list-style: decimal; padding-left: 20px;">
1185 <li><?php echo esc_html__( 'If it happens during imports/backups/crawlers: schedule these jobs at low traffic times.', 'wp-memory-usage' ); ?></li>
1186 <li>
1187 <?php echo esc_html__( 'Increase the PHP memory limit if your hosting allows it (often the quickest fix).', 'wp-memory-usage' ); ?><br>
1188 &bull; <?php echo esc_html__( 'In wp-config.php you can define WP_MEMORY_LIMIT and WP_MAX_MEMORY_LIMIT (example: define(\'WP_MEMORY_LIMIT\', "256M");). This affects WordPress, but cannot exceed the server PHP limit.', 'wp-memory-usage' ); ?>
1189 <br>
1190 &bull; <?php echo esc_html__( 'The PHP memory_limit is controlled by your hosting environment (php.ini, user.ini, .htaccess, or a hosting control panel). If it is lower than your WordPress settings, PHP wins.', 'wp-memory-usage' ); ?>
1191 <br>
1192 &bull; <?php echo esc_html__( 'After changing values, clear caches (object cache, page cache) and re-test the action that caused the peak.', 'wp-memory-usage' ); ?>
1193 </li>
1194 <li><?php echo esc_html__( 'Update WordPress core, themes and plugins. Memory leaks and inefficiencies are often fixed in updates.', 'wp-memory-usage' ); ?></li>
1195 <li><?php echo esc_html__( 'If it happens on frontend pages: check the page and its plugins (builder, gallery, search, related posts, cache). Disable suspects temporarily to confirm.', 'wp-memory-usage' ); ?></li>
1196 </ol>
1197 <h3><?php echo esc_html__( 'When to adjust thresholds', 'wp-memory-usage' ); ?></h3>
1198 <p><?php echo esc_html__( 'If you receive alerts during expected heavy tasks (e.g. backups), increase the Warning/Danger thresholds slightly. Keep Critical high (e. g. 95%) so you still get a real emergency signal.', 'wp-memory-usage' ); ?></p>
1199
1200 <?php elseif ( 'digest' === $tab ) :
1201 // ── Digest-Datei löschen (POST-Action) ───────────────────────────
1202 $delete_notice = '';
1203 if (
1204 isset( $_POST['wpmu_digest_delete'], $_POST['wpmu_digest_delete_nonce'], $_POST['wpmu_digest_delete_file'] ) &&
1205 wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['wpmu_digest_delete_nonce'] ) ), 'wpmu_digest_delete' ) &&
1206 current_user_can( self::CAP )
1207 ) {
1208 $del_fn = sanitize_file_name( wp_unslash( $_POST['wpmu_digest_delete_file'] ) );
1209 // Nur digest_*.cgibak erlaubt
1210 if ( preg_match( '/^digest_[\d\-]+\.cgibak$/', $del_fn ) ) {
1211 $del_path = self::WPMU_LOG_PATH . $del_fn;
1212 if ( file_exists( $del_path ) && wp_delete_file( $del_path ) ) {
1213 $delete_notice = '<div class="notice notice-success is-dismissible"><p>' .
1214 sprintf(
1215 /* translators: %s: filename that was deleted */
1216 esc_html__( 'Deleted: %s', 'wp-memory-usage' ),
1217 esc_html( $del_fn ) ) .
1218 '</p></div>';
1219 } else {
1220 $delete_notice = '<div class="notice notice-error is-dismissible"><p>' .
1221 sprintf(
1222 /* translators: %s: filename that could not deleted */
1223 esc_html__( 'Could not delete: %s', 'wp-memory-usage' ),
1224 esc_html( $del_fn ) ) .
1225 '</p></div>';
1226 }
1227 } elseif ("all"==$del_fn) {
1228 $del_path = self::WPMU_LOG_PATH . $del_fn;
1229 #error_log("del: ".$del_path);
1230 $del_ok = 0;
1231 $del_notok = 0;
1232 $del_no = 0;
1233 foreach ( glob( self::WPMU_LOG_PATH . 'digest_*.cgibak' ) as $bak ) {
1234 #error_log("do del: ".$bak);
1235 $del_no++;
1236 if ( file_exists( $bak ) && wp_delete_file( $bak ) ) {
1237 $del_ok++;
1238 } else {
1239 $del_notok++;
1240 }
1241 }
1242 if ( $del_ok==$del_no ) {
1243 $delete_notice = '<div class="notice notice-success is-dismissible"><p>' .
1244 sprintf(
1245 /* translators: %s: filename that could not deleted */
1246 esc_html__( 'Deleted: %s files', 'wp-memory-usage' ),
1247 esc_html( $del_ok ) ) .
1248 '</p></div>';
1249 } else {
1250 $delete_notice = '<div class="notice notice-error is-dismissible"><p>' .
1251 sprintf(
1252 /* translators: %s: filename that could not deleted */
1253 esc_html__( 'Could not delete: %s files', 'wp-memory-usage' ),
1254 esc_html( $del_notok ) ) .
1255 '</p></div>';
1256 }
1257 }
1258 }
1259 echo wp_kses_post( $delete_notice );
1260
1261 // ── Alle Digest-Dateien einlesen ──────────────────────────────────
1262 $digest_files = array();
1263 foreach ( glob( self::WPMU_LOG_PATH . 'digest_*.cgibak' ) as $bak ) {
1264 $fn = basename( $bak );
1265 $raw = str_replace( array( 'digest_', '.cgibak' ), '', $fn );
1266 $dt = DateTime::createFromFormat( 'Y-m-d-H-i-s', $raw );
1267 $label = $dt
1268 ? wp_date( get_option('date_format') . ', ' . get_option('time_format'), $dt->getTimestamp() )
1269 : $fn; // Fallback: Rohdateiname, falls Format nicht passt
1270 $digest_files[ $fn ] = $label;
1271 }
1272 krsort( $digest_files ); // neueste zuerst
1273
1274 // ── Auswahl auslesen (Nonce-gesichert) ────────────────────────────
1275 $selected_file = '';
1276 $merge_all = false;
1277 if (
1278 isset( $_GET['wpmu_digest_nonce'], $_GET['wpmu_digest_sel'] ) &&
1279 wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['wpmu_digest_nonce'] ) ), 'wpmu_digest_sel' )
1280 ) {
1281 $raw = sanitize_text_field( wp_unslash( $_GET['wpmu_digest_sel'] ) );
1282 if ( $raw === '__all__' ) {
1283 $merge_all = true;
1284 } elseif ( isset( $digest_files[ $raw ] ) ) {
1285 $selected_file = $raw;
1286 }
1287 }
1288
1289 // ── Daten laden ───────────────────────────────────────────────────
1290 $data = array();
1291 if ( $merge_all && ! empty( $digest_files ) ) {
1292 foreach ( array_keys( $digest_files ) as $fn ) {
1293 $d = self::get_from_file( $fn );
1294 if ( ! is_array( $d ) || empty( $d ) ) { continue; }
1295 if ( empty( $data ) ) {
1296 $data = $d;
1297 continue;
1298 }
1299 foreach ( $d['status'] as $s => $cnt ) {
1300 $data['status'][ $s ] = ( $data['status'][ $s ] ?? 0 ) + $cnt;
1301 }
1302 foreach ( ( $d['type'] ?? array() ) as $t => $cnt ) {
1303 $data['type'][ $t ] = ( $data['type'][ $t ] ?? 0 ) + $cnt;
1304 }
1305 foreach ( ( $d['uri'] ?? array() ) as $uri => $ud ) {
1306 if ( ! isset( $data['uri'][ $uri ] ) ) {
1307 $data['uri'][ $uri ] = array( 'total' => 0, 'warn' => 0, 'danger' => 0, 'critical' => 0, 'avg_usage' => 0, 'max_usage' => 0 );
1308 }
1309 $existing_total = $data['uri'][ $uri ]['total'];
1310 $new_total = $ud['total'] ?? 0;
1311 $merged_total = $existing_total + $new_total;
1312 $data['uri'][ $uri ]['avg_usage'] = $merged_total > 0
1313 ? (int) round( ( $data['uri'][ $uri ]['avg_usage'] * $existing_total + ( $ud['avg_usage'] ?? 0 ) * $new_total ) / $merged_total )
1314 : 0;
1315 $data['uri'][ $uri ]['total'] = $merged_total;
1316 $data['uri'][ $uri ]['warn'] = ( $data['uri'][ $uri ]['warn'] ?? 0 ) + ( $ud['warn'] ?? 0 );
1317 $data['uri'][ $uri ]['danger'] = ( $data['uri'][ $uri ]['danger'] ?? 0 ) + ( $ud['danger'] ?? 0 );
1318 $data['uri'][ $uri ]['critical'] = ( $data['uri'][ $uri ]['critical'] ?? 0 ) + ( $ud['critical'] ?? 0 );
1319 if ( ( $ud['max_usage'] ?? 0 ) > $data['uri'][ $uri ]['max_usage'] ) {
1320 $data['uri'][ $uri ]['max_usage'] = $ud['max_usage'];
1321 }
1322 }
1323 if ( isset( $d['interval']['first'] ) && $d['interval']['first'] < $data['interval']['first'] ) {
1324 $data['interval']['first'] = $d['interval']['first'];
1325 }
1326 if ( isset( $d['interval']['last'] ) && $d['interval']['last'] > $data['interval']['last'] ) {
1327 $data['interval']['last'] = $d['interval']['last'];
1328 }
1329 foreach ( ( $d['interval']['per_minute'] ?? array() ) as $min_key => $cnt ) {
1330 $data['interval']['per_minute'][ $min_key ] = ( $data['interval']['per_minute'][ $min_key ] ?? 0 ) + $cnt;
1331 }
1332 }
1333 if ( isset( $data['interval']['first'], $data['interval']['last'] ) ) {
1334 $data['interval']['duration_sec'] = $data['interval']['last'] - $data['interval']['first'];
1335 }
1336 } elseif ( $selected_file ) {
1337 $data = self::get_from_file( $selected_file );
1338 if ( ! is_array( $data ) ) { $data = array(); }
1339 } elseif ( ! empty( $digest_files ) ) {
1340 $selected_file = array_key_first( $digest_files );
1341 $data = self::get_from_file( $selected_file );
1342 if ( ! is_array( $data ) ) { $data = array(); }
1343 }
1344
1345 // ── Auswahl-Formular ──────────────────────────────────────────────
1346 $digest_nonce = wp_create_nonce( 'wpmu_digest_sel' );
1347 ?>
1348 <div style="margin:16px 0;display:flex;align-items:center;gap:12px;flex-wrap:wrap;background:#f9f9f9;padding:12px 16px;border:1px solid #ddd;border-radius:6px;">
1349 <form method="get" style="display:contents;">
1350 <input type="hidden" name="page" value="wpmu-memory-alerts">
1351 <input type="hidden" name="tab" value="digest">
1352 <input type="hidden" name="_wpmu_tab_nonce" value="<?php echo esc_attr( $tab_nonce ); ?>">
1353 <input type="hidden" name="wpmu_digest_nonce" value="<?php echo esc_attr( $digest_nonce ); ?>">
1354 <label for="wpmu_digest_sel" style="font-weight:600;white-space:nowrap;">
1355 <?php echo esc_html__( 'Show Digest:', 'wp-memory-usage' ); ?>
1356 </label>
1357 <select name="wpmu_digest_sel" id="wpmu_digest_sel" onchange="this.form.submit()"
1358 style="min-width:280px;padding:5px 8px;">
1359 <?php if ( empty( $digest_files ) ) : ?>
1360 <option value=""><?php echo esc_html__( '— no digest files found —', 'wp-memory-usage' ); ?></option>
1361 <?php else : ?>
1362 <?php if ( count( $digest_files ) > 1 ) : ?>
1363 <option value="__all__" <?php selected( $merge_all, true ); ?>>
1364 <?php printf(
1365 /* translators: %d: number of digest files */
1366 esc_html__( '⊕ Merge all %d digest files', 'wp-memory-usage' ), count( $digest_files ) ); ?>
1367 </option>
1368 <option disabled>──────────────────────────────</option>
1369 <?php endif; ?>
1370 <?php foreach ( $digest_files as $fn => $label ) : ?>
1371 <option value="<?php echo esc_attr( $fn ); ?>"
1372 <?php selected( ! $merge_all && $selected_file === $fn ); ?>>
1373 <?php
1374 echo esc_html( $label ); ?>
1375 </option>
1376 <?php endforeach; ?>
1377 <?php endif; ?>
1378 </select>
1379 <noscript><button type="submit" class="button"><?php echo esc_html__( 'Show', 'wp-memory-usage' ); ?></button></noscript>
1380 </form>
1381 <?php if ( $selected_file && ! $merge_all ) : ?>
1382 <form method="post" style="display:inline;"
1383 onsubmit="return confirm('<?php echo esc_js( __( 'Delete this digest file? This cannot be undone.', 'wp-memory-usage' ) ); ?>');">
1384 <input type="hidden" name="page" value="wpmu-memory-alerts">
1385 <input type="hidden" name="tab" value="digest">
1386 <input type="hidden" name="_wpmu_tab_nonce" value="<?php echo esc_attr( $tab_nonce ); ?>">
1387 <input type="hidden" name="wpmu_digest_delete" value="1">
1388 <input type="hidden" name="wpmu_digest_delete_file" value="<?php echo esc_attr( $selected_file ); ?>">
1389 <input type="hidden" name="wpmu_digest_delete_nonce"
1390 value="<?php echo esc_attr( wp_create_nonce( 'wpmu_digest_delete' ) ); ?>">
1391 <button type="submit" class="button button-small"
1392 style="border-color:#d9534f;color:#d9534f;background:#fff;">
1393 🗑 <?php echo esc_html__( 'Delete this file', 'wp-memory-usage' ); ?>
1394 </button>
1395 </form>
1396 <form method="post" style="display:inline;"
1397 onsubmit="return confirm('<?php echo esc_js( __( 'Delete all digest files? This cannot be undone.', 'wp-memory-usage' ) ); ?>');">
1398 <input type="hidden" name="page" value="wpmu-memory-alerts">
1399 <input type="hidden" name="tab" value="digest">
1400 <input type="hidden" name="_wpmu_tab_nonce" value="<?php echo esc_attr( $tab_nonce ); ?>">
1401 <input type="hidden" name="wpmu_digest_delete" value="1">
1402 <input type="hidden" name="wpmu_digest_delete_file" value="<?php echo esc_attr( "all" ); ?>">
1403 <input type="hidden" name="wpmu_digest_delete_nonce"
1404 value="<?php echo esc_attr( wp_create_nonce( 'wpmu_digest_delete' ) ); ?>">
1405 <button type="submit" class="button button-small"
1406 style="border-color:#d9534f;color:#d9534f;background:#fff;">
1407 🗑 <?php echo esc_html__( 'Delete all files', 'wp-memory-usage' ); ?>
1408 </button>
1409 </form>
1410 <?php endif; ?>
1411 <?php if ( $merge_all ) : ?>
1412 <span style="background:#e7f3ff;border:1px solid #3498db;border-radius:4px;padding:4px 10px;font-size:12px;color:#0c5d9e;font-weight:600;">
1413 <?php printf(
1414 /* translators: %d: number of merged digest files */
1415 esc_html__( 'Merged: %d files', 'wp-memory-usage' ), count( $digest_files ) ); ?>
1416 </span>
1417 <?php endif;
1418 $currenttime = wp_date( get_option('date_format') . ', ' . get_option('time_format') );
1419 echo esc_html__( 'Servertime', 'wp-memory-usage' ). ": ".esc_html($currenttime);
1420 ?>
1421 </div>
1422 <?php
1423 if (count($data)>0) {
1424 ?>
1425 <table>
1426 <tr><td valign="top">
1427 <!-- STATUS -->
1428 <table class="widefat striped" style="max-width:400px;">
1429 <thead><tr><th colspan="2"><h3><?php echo esc_html__( 'Status Summary', 'wp-memory-usage' ); ?></h3></th></tr></thead>
1430 <thead><tr><th><?php echo esc_html__( 'Status', 'wp-memory-usage' ); ?></th><th><?php echo esc_html__( 'Occurrences', 'wp-memory-usage' ); ?></th></tr></thead>
1431 <tbody>
1432 <?php foreach ( $data['status'] as $s => $count ) :
1433 $color = match( $s ) {
1434 'warn' => '#f0ad4e',
1435 'danger' => '#d9534f',
1436 'critical' => '#8B0000',
1437 default => '#5cb85c',
1438 };
1439 $outtxt = match( $s ) {
1440 'warn' => __('warn', 'wp-memory-usage' ),
1441 'danger' => __('danger', 'wp-memory-usage' ),
1442 'critical' => __('critical', 'wp-memory-usage' ),
1443 default => __('ok', 'wp-memory-usage' ),
1444 };
1445 ?>
1446 <tr>
1447 <td><strong style="color:<?php echo esc_html($color); ?>"><?php echo esc_html(strtoupper( $outtxt )); ?></strong></td>
1448 <td><?php echo esc_html($count); ?></td>
1449 </tr>
1450 <?php endforeach; ?>
1451 </tbody>
1452 </table>
1453 </td><td valign="top">
1454 <!-- TYP -->
1455 <table class="widefat striped" style="max-width:400px;">
1456 <thead><tr><th colspan="2"><h3><?php echo esc_html__( 'Request types', 'wp-memory-usage' ); ?></h3></th></tr></thead>
1457 <thead><tr><th><?php echo esc_html__( 'Type', 'wp-memory-usage' ); ?></th><th><?php echo esc_html__( 'Occurrences', 'wp-memory-usage' ); ?></th></tr></thead>
1458 <tbody>
1459 <?php foreach ( $data['type'] as $type => $count ) : ?>
1460 <tr>
1461 <td><?php echo esc_html( $type ); ?></td>
1462 <td><?php echo esc_html($count); ?></td>
1463 </tr>
1464 <?php endforeach; ?>
1465 </tbody>
1466 </table>
1467 </td><td valign="top">
1468 <!-- ZEITINTERVALL -->
1469 <?php
1470 $dur = $data['interval']['duration_sec'];
1471 $mins = floor( $dur / 60 );
1472 $secs = $dur % 60;
1473 ?>
1474 <!-- PRO INTERVALL -->
1475 <?php
1476 $interval_min = 60 * 3;
1477 $per_interval = array();
1478 foreach ( $data['interval']['per_minute'] as $minute => $count ) {
1479 $ts_slot = strtotime( $minute );
1480 $slot_ts = floor( $ts_slot / ( $interval_min * 60 ) ) * ( $interval_min * 60 );
1481 $slot_key = wp_date( get_option('date_format') . ', ' . get_option('time_format'), $slot_ts );
1482 $per_interval[ $slot_key ] = ( $per_interval[ $slot_key ] ?? 0 ) + $count;
1483 }
1484 $max_count = $per_interval ? max( $per_interval ) : 1;
1485 ?>
1486 <table class="widefat striped" style="max-width:600px;">
1487 <thead><tr><th colspan="3"><h3><?php echo esc_html__( 'Events each', 'wp-memory-usage' ) . " ". esc_html($interval_min) . " " . esc_html__( 'Minutes', 'wp-memory-usage' ); ?></h3></th></tr></thead>
1488 <tr><th colspan="3">
1489 <?php echo esc_html__( 'From', 'wp-memory-usage' ); ?>: <strong><?php echo esc_html(wp_date( get_option('date_format') . ', ' . get_option('time_format'), $data['interval']['first'] )); ?></strong> &nbsp;|&nbsp;
1490 <?php echo esc_html__( 'To', 'wp-memory-usage' ); ?>: <strong><?php echo esc_html(wp_date( get_option('date_format') . ', ' . get_option('time_format'), $data['interval']['last'] )); ?></strong> &nbsp;|&nbsp;
1491 <?php echo esc_html__( 'Duration', 'wp-memory-usage' ); ?>: <strong><?php echo esc_html($mins) . " " . esc_html__( 'min.', 'wp-memory-usage' ) . " ". esc_html($secs) . " " . esc_html__( 'sec.', 'wp-memory-usage' ); ?> </strong>
1492 </th></tr>
1493 <thead><tr><th><?php echo esc_html__( 'Time window', 'wp-memory-usage' ); ?> </th><th><?php echo esc_html__( 'Occurrences', 'wp-memory-usage' ); ?> </th><th><?php echo esc_html__( 'Event Distribution', 'wp-memory-usage' ); ?> </th></tr></thead>
1494 <tbody>
1495 <?php foreach ( $per_interval as $slot => $count ) :
1496 $bar_width = round( ( $count / $max_count ) * 200 );
1497 $bar_color = $count >= 10 ? '#d9534f' : ( $count >= 5 ? '#f0ad4e' : '#5cb85c' );
1498 ?>
1499 <tr>
1500 <td><?php echo esc_html( $slot ); ?> <?php echo esc_html(
1501 wp_date( 'H:i', strtotime( $slot ) + $interval_min * 60 )
1502 );
1503 ?></td>
1504 <td><strong><?php echo esc_html($count); ?></strong></td>
1505 <td><div style="width:<?php echo esc_attr($bar_width); ?>px;height:14px;background:<?php echo esc_attr($bar_color); ?>;border-radius:3px;"></div></td>
1506 </tr>
1507 <?php endforeach; ?>
1508 </tbody>
1509 </table>
1510 </td></tr></table>
1511
1512 <!-- ======================================================
1513 URI-TABELLE: sortierbar, Spalten ausblendbar, avg + max
1514 ====================================================== -->
1515 <h3>URIs</h3>
1516 <?php
1517 uasort( $data['uri'], fn( $a, $b ) => $b['total'] <=> $a['total'] );
1518 $uri_json = json_encode( $data['uri'], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP );
1519 $home_url = json_encode( trailingslashit( home_url() ) );
1520 ?>
1521 <style>
1522 .wpmu-uri-wrap { max-width: 1150px; margin-top: 8px; }
1523 .wpmu-uri-controls {
1524 display: flex; align-items: center; gap: 10px;
1525 flex-wrap: wrap; margin-bottom: 10px;
1526 }
1527 .wpmu-toggle-btn {
1528 display: inline-flex; align-items: center; gap: 4px;
1529 padding: 4px 11px; border-radius: 4px; border: 2px solid transparent;
1530 font-size: 12px; font-weight: 600; cursor: pointer;
1531 background: #fff; transition: opacity .15s, box-shadow .15s;
1532 user-select: none; line-height: 1.5;
1533 }
1534 .wpmu-toggle-btn[data-col="warn"] { border-color: #f0ad4e; color: #7a5200; }
1535 .wpmu-toggle-btn[data-col="danger"] { border-color: #d9534f; color: #a02020; }
1536 .wpmu-toggle-btn[data-col="critical"] { border-color: #8B0000; color: #8B0000; }
1537 .wpmu-toggle-btn.is-hidden { opacity: .35; }
1538 .wpmu-toggle-btn .btn-icon { font-size: 10px; }
1539 #wpmu-uri-tbl { width: 100%; border-collapse: collapse; font-size: 13px; }
1540 #wpmu-uri-tbl th {
1541 background: #f5f5f5; padding: 7px 10px; text-align: left;
1542 border-bottom: 2px solid #ddd; white-space: nowrap;
1543 cursor: pointer; user-select: none;
1544 }
1545 #wpmu-uri-tbl th:hover { background: #eaeaea; }
1546 #wpmu-uri-tbl th .si { font-size: 10px; margin-left: 3px; color: #aaa; }
1547 #wpmu-uri-tbl th.sa .si::after { content: "▲"; color: #0073aa; }
1548 #wpmu-uri-tbl th.sd .si::after { content: "▼"; color: #0073aa; }
1549 #wpmu-uri-tbl th:not(.sa):not(.sd) .si::after { content: "�
1550 "; }
1551 #wpmu-uri-tbl td { padding: 6px 10px; border-bottom: 1px solid #eee; vertical-align: middle; }
1552 #wpmu-uri-tbl tbody tr:hover td { background: #f9f9f9; }
1553 #wpmu-uri-tbl td.nr { text-align: right; font-variant-numeric: tabular-nums; }
1554 .wpmu-badge {
1555 display: inline-block; padding: 2px 7px; border-radius: 10px;
1556 font-size: 11px; font-weight: 700;
1557 }
1558 .wpmu-bw { background: #f0ad4e; color: #3d2b00; }
1559 .wpmu-bd { background: #d9534f; color: #fff; }
1560 .wpmu-bc { background: #8B0000; color: #fff; }
1561 .wpmu-avg { color: #666; font-size: 12px; }
1562 .wpmu-max { font-weight: 700; }
1563 </style>
1564
1565 <div class="wpmu-uri-wrap">
1566 <div class="wpmu-uri-controls">
1567 <strong style="font-size:13px;"><?php echo esc_html__( 'Toggle Columns', 'wp-memory-usage' ); ?>:</strong>
1568 <button type="button" class="wpmu-toggle-btn" data-col="warn">
1569 <span class="btn-icon">✓</span> <?php echo esc_html__( '⚠ warn', 'wp-memory-usage' ); ?>
1570 </button>
1571 <button type="button" class="wpmu-toggle-btn" data-col="danger">
1572 <span class="btn-icon">✓</span> <?php echo esc_html__( '🔴 danger', 'wp-memory-usage' ); ?>
1573 </button>
1574 <button type="button" class="wpmu-toggle-btn" data-col="critical">
1575 <span class="btn-icon">✓</span> <?php echo esc_html__( '🆘 critical', 'wp-memory-usage' ); ?>
1576 </button>
1577 </div>
1578 <table id="wpmu-uri-tbl" class="widefat striped">
1579 <thead>
1580 <tr>
1581 <th data-col="uri">URI <span class="si"></span></th>
1582 <th data-col="total" class="nr sd"><?php echo esc_html__( 'total', 'wp-memory-usage' ); ?> <span class="si"></span></th>
1583 <th data-col="warn" class="nr col-warn" style="text-align:center;"><?php echo esc_html__( '⚠ warn', 'wp-memory-usage' ); ?><span class="si"></span></th>
1584 <th data-col="danger" class="nr col-danger" style="text-align:center;"><?php echo esc_html__( '🔴 danger', 'wp-memory-usage' ); ?><span class="si"></span></th>
1585 <th data-col="critical" class="nr col-critical" style="text-align:center;"><?php echo esc_html__( '🆘 critical', 'wp-memory-usage' ); ?><span class="si"></span></th>
1586 <th data-col="avg" class="nr"><?php echo esc_html__( 'Ø average', 'wp-memory-usage' ); ?><span class="si"></span></th>
1587 <th data-col="max" class="nr"><?php echo esc_html__( 'max', 'wp-memory-usage' ); ?> <span class="si"></span></th>
1588 </tr>
1589 </thead>
1590 <tbody id="wpmu-uri-tbody"></tbody>
1591 </table>
1592 </div>
1593 <?php
1594 $wpmu_no_data = __( 'No data', 'wp-memory-usage' );
1595 ?>
1596 <script>
1597 var WPMU_NO_DATA = <?php echo wp_json_encode( $wpmu_no_data ); ?>;
1598 (function () {
1599 var RAW = <?php echo wp_json_encode( json_decode($uri_json) ); ?>;
1600 var HOMEURL = <?php echo wp_json_encode( json_decode($home_url) ); ?>;
1601
1602 // Build flat row array
1603 var rows = Object.entries( RAW ).map( function ( e ) {
1604 var uri = e[0], d = e[1];
1605 return {
1606 uri: uri,
1607 total: d.total || 0,
1608 warn: d.warn || 0,
1609 danger: d.danger || 0,
1610 critical: d.critical || 0,
1611 avg: d.avg_usage || 0,
1612 max: d.max_usage || 0,
1613 };
1614 });
1615
1616 var sortCol = 'total';
1617 var sortDir = 'desc';
1618 var hidden = {}; // col -> bool
1619
1620 function fmtB( b ) {
1621 b = parseInt( b, 10 ) || 0;
1622 if ( ! b ) return '–';
1623 var u = ['B','KB','MB','GB'], i = 0, v = b;
1624 while ( v >= 1024 && i < u.length - 1 ) { v /= 1024; i++; }
1625 return ( i === 0 ? v.toFixed(0) : v.toFixed(1) ) + '\u202f' + u[i];
1626 }
1627
1628 function esc( s ) {
1629 return String( s )
1630 .replace(/&/g,'&amp;').replace(/</g,'&lt;')
1631 .replace(/>/g,'&gt;').replace(/"/g,'&quot;');
1632 }
1633
1634 function render() {
1635 // Sort
1636 var sorted = rows.slice().sort( function ( a, b ) {
1637 var av = a[ sortCol ], bv = b[ sortCol ];
1638 if ( typeof av === 'string' ) {
1639 av = av.toLowerCase(); bv = bv.toLowerCase();
1640 return sortDir === 'asc' ? ( av < bv ? -1 : av > bv ? 1 : 0 )
1641 : ( av > bv ? -1 : av < bv ? 1 : 0 );
1642 }
1643 return sortDir === 'asc' ? av - bv : bv - av;
1644 });
1645
1646 // Render rows
1647 var html = '';
1648 sorted.forEach( function ( r ) {
1649 var uriDisplay = r.uri.replace( /\?doing_wp_cron=[\d.]+/, '?doing_wp_cron=\u2026' );
1650 var fullUrl = HOMEURL + r.uri.replace( /^\//, '' );
1651
1652 var warnCell = hidden['warn'] ? '' :
1653 '<td class="nr col-warn" style="text-align:center;">' +
1654 ( r.warn ? '<span class="wpmu-badge wpmu-bw">' + r.warn + '</span>' : '<span style="color:#ccc">–</span>' ) +
1655 '</td>';
1656 var dangerCell = hidden['danger'] ? '' :
1657 '<td class="nr col-danger" style="text-align:center;">' +
1658 ( r.danger ? '<span class="wpmu-badge wpmu-bd">' + r.danger + '</span>' : '<span style="color:#ccc">–</span>' ) +
1659 '</td>';
1660 var criticalCell = hidden['critical'] ? '' :
1661 '<td class="nr col-critical" style="text-align:center;">' +
1662 ( r.critical ? '<span class="wpmu-badge wpmu-bc">' + r.critical + '</span>' : '<span style="color:#ccc">–</span>' ) +
1663 '</td>';
1664
1665 html += '<tr>'
1666 + '<td><a href="' + esc( fullUrl ) + '" target="_blank" rel="noopener" style="word-break:break-all;">' + esc( uriDisplay ) + '</a></td>'
1667 + '<td class="nr"><strong>' + r.total + '</strong></td>'
1668 + warnCell
1669 + dangerCell
1670 + criticalCell
1671 + '<td class="nr wpmu-avg">' + fmtB( r.avg ) + '</td>'
1672 + '<td class="nr wpmu-max">' + fmtB( r.max ) + '</td>'
1673 + '</tr>';
1674 });
1675 document.getElementById('wpmu-uri-tbody').innerHTML =
1676 html || '<tr><td colspan="7" style="color:#999;text-align:center;padding:12px;">' + WPMU_NO_DATA + '</td></tr>';
1677
1678 // Update header classes + visibility
1679 document.querySelectorAll('#wpmu-uri-tbl thead th').forEach( function ( th ) {
1680 th.classList.remove('sa','sd');
1681 if ( th.dataset.col === sortCol ) {
1682 th.classList.add( sortDir === 'asc' ? 'sa' : 'sd' );
1683 }
1684 var col = th.dataset.col;
1685 if ( col && ( col === 'warn' || col === 'danger' || col === 'critical' ) ) {
1686 th.style.display = hidden[ col ] ? 'none' : '';
1687 }
1688 });
1689 }
1690
1691 // Sort on header click
1692 document.querySelectorAll('#wpmu-uri-tbl thead th').forEach( function ( th ) {
1693 th.addEventListener('click', function () {
1694 var col = this.dataset.col;
1695 if ( ! col ) return;
1696 if ( sortCol === col ) {
1697 sortDir = sortDir === 'asc' ? 'desc' : 'asc';
1698 } else {
1699 sortCol = col;
1700 sortDir = col === 'uri' ? 'asc' : 'desc';
1701 }
1702 render();
1703 });
1704 });
1705
1706 // Toggle buttons
1707 document.querySelectorAll('.wpmu-toggle-btn').forEach( function ( btn ) {
1708 btn.addEventListener('click', function () {
1709 var col = this.dataset.col;
1710 hidden[ col ] = ! hidden[ col ];
1711 this.classList.toggle('is-hidden', !! hidden[ col ] );
1712 this.querySelector('.btn-icon').textContent = hidden[ col ] ? '' : '';
1713 render();
1714 });
1715 });
1716
1717 render();
1718 })();
1719 </script>
1720 <?PHP
1721 } else {
1722 echo "<h2>".esc_html__( 'No digest created yet. See "Digest interval" in the settings.', 'wp-memory-usage' )."</h2>";
1723 }
1724 ?>
1725
1726 <?php elseif ( 'history' === $tab ) :
1727 $opts = self::get_log();
1728 $totalanz = count($opts);
1729 $log = array_slice( $opts, -1 * self::$event_anz_display );
1730 $log = array_reverse( $log );
1731 ?>
1732 <h2><?php
1733 if (self::$event_anz_display >= $totalanz) {
1734 echo esc_html($totalanz) . " " . esc_html__( 'recent events', 'wp-memory-usage' ) . ", ";
1735 } else {
1736 #echo esc_html(self::$event_anz_display) . " " . esc_html__( 'recent events displayed', 'wp-memory-usage' ) . " (" . esc_html($totalanz) . esc_html__(" stored events", 'wp-memory-usage' ). "), ";
1737 echo esc_html(
1738 sprintf(
1739 /* translators: 1: number of displayed events, 2: number of total stored events */
1740 __( '%1$d recent events displayed (%2$d stored events),', 'wp-memory-usage' ),
1741 self::$event_anz_display,
1742 $totalanz
1743 ) );
1744 }
1745 $currenttime = wp_date( get_option('date_format') . ', ' . get_option('time_format') );
1746 echo esc_html__( 'Servertime', 'wp-memory-usage' ). ": ".esc_html($currenttime);
1747 ?></h2>
1748 <?php if ( empty( $log ) ) : ?>
1749 <p><?php
1750 echo esc_html__( 'No events recorded yet.', 'wp-memory-usage' );
1751 echo "<hr>";
1752
1753 $opts = self::get_settings();
1754
1755 $count_off = 0;
1756
1757 echo esc_html__('Log WARN', 'wp-memory-usage' ).": ";
1758 if ($opts["logop_warn_pct"]=="0") {
1759 echo '<span style="color: red;">';
1760 echo esc_html__('Off', 'wp-memory-usage' );
1761 echo "</span>";
1762 $count_off++;
1763 } else {
1764 echo esc_html__('On', 'wp-memory-usage' );
1765 }
1766 echo "<p>";
1767
1768 echo esc_html__('Log DANGER', 'wp-memory-usage' ).": ";
1769 if ($opts["logop_danger_pct"]=="0") {
1770 echo '<span style="color: red;">';
1771 echo esc_html__('Off', 'wp-memory-usage' );
1772 echo "</span>";
1773 $count_off++;
1774 } else {
1775 echo esc_html__('On', 'wp-memory-usage' );
1776 }
1777 echo "<p>";
1778
1779 echo esc_html__('Log CRITICAL', 'wp-memory-usage' ).": ";
1780 if ($opts["logop_critical_pct"]=="0") {
1781 echo '<span style="color: red;">';
1782 echo esc_html__('Off', 'wp-memory-usage' );
1783 echo "</span>";
1784 $count_off++;
1785 } else {
1786 echo esc_html__('On', 'wp-memory-usage' );
1787 }
1788 /*
1789 echo "<p>";
1790
1791 echo __('Log OK', 'wp-memory-usage' ).": ";;
1792 if ($opts["log_ok"]=="0") {
1793 echo '<span style="color: red;">';
1794 echo __('Off', 'wp-memory-usage' );
1795 echo "</span>";
1796 $count_off++;
1797 } else {
1798 echo __('On', 'wp-memory-usage' );
1799 }
1800 */
1801
1802 if ($count_off==3) {
1803 echo "<hr>";
1804 echo '<span style="color: red;">';
1805 echo esc_html__('Logging switched off', 'wp-memory-usage' );
1806 echo "</span>";
1807 }
1808 #echo "<hr>".print_r($opts, true);
1809 ?></p>
1810 <?php else : ?>
1811 <table class="widefat striped" style="max-width: 1200px;">
1812 <thead>
1813 <tr>
1814 <th><?php echo esc_html__( 'Time', 'wp-memory-usage' ); ?></th>
1815 <th><?php echo esc_html__( 'State', 'wp-memory-usage' ); ?></th>
1816 <th><?php echo esc_html__( 'Usage / Limit', 'wp-memory-usage' ); ?></th>
1817 <th><?php echo esc_html__( 'Type', 'wp-memory-usage' ); ?></th>
1818 <th><?php echo esc_html__( 'URI', 'wp-memory-usage' ); ?></th>
1819 </tr>
1820 </thead>
1821 <tbody>
1822 <?php foreach ( $log as $e1 ) :
1823 $c = isset( $e1['c'] ) && is_array( $e1['c'] ) ? $e1['c'] : array();
1824 $lastmb = (int) $e1['l'];
1825 $perc = $lastmb > 0 ? (int) ( ( (int) $e1['u'] ) / $lastmb * 100 ) : 0;
1826 ?>
1827 <tr>
1828 <td><?php echo esc_html(
1829 wp_date( get_option('date_format') . ', ' . get_option('time_format'), (int) $e1['t'] ) );
1830 ?></td>
1831 <td><strong><?php echo esc_html( strtoupper( (string) $e1['s'] ) ); echo "<br>" . esc_html($perc) . "%"; ?></strong></td>
1832 <td><?php echo esc_html( self::format_bytes( (int) $e1['u'] ) . ' / ' . ( (int) $e1['l'] > 0 ? self::format_bytes( (int) $e1['l'] ) : __( 'unlimited', 'wp-memory-usage' ) ) ); ?></td>
1833 <td><?php echo esc_html( (string) ( isset( $c['type'] ) ? $c['type'] : '' ) ); ?></td>
1834 <td><code><?php echo esc_html( (string) ( isset( $c['uri'] ) ? $c['uri'] : '' ) ); ?></code></td>
1835 </tr>
1836 <?php endforeach; ?>
1837 </tbody>
1838 </table>
1839 <?php endif; ?>
1840 <?php elseif ( 'check_installation' === $tab ) :
1841 // ══════════════════════════════════════════════════════
1842 // CHECK INSTALLATION
1843 // ══════════════════════════════════════════════════════
1844 global $wp_filesystem;
1845 if ( ! function_exists( 'WP_Filesystem' ) ) {
1846 require_once ABSPATH . 'wp-admin/includes/file.php';
1847 }
1848 WP_Filesystem();
1849
1850 $checks = array(); // array of ['label', 'status' (ok|warn|error), 'detail', 'hint']
1851
1852 // ── 1. PHP-Mindestversion ──────────────────────────────
1853 $php_min = '7.4.0';
1854 $php_rec = '8.0.0';
1855 $php_current = PHP_VERSION;
1856 if ( version_compare( $php_current, $php_min, '<' ) ) {
1857 $checks[] = array(
1858 'status' => 'error',
1859 'label' => __( 'PHP Version', 'wp-memory-usage' ),
1860 'hint' => __( 'Upgrade PHP to at least 7.4. PHP 8.1+ is recommended.', 'wp-memory-usage' ),
1861 'detail' => __( 'PHP', 'wp-memory-usage' ) . ' ' . $php_current . '' . __( 'minimum required:', 'wp-memory-usage' ) . ' ' . $php_min,
1862
1863 );
1864 } elseif ( version_compare( $php_current, $php_rec, '<' ) ) {
1865 $checks[] = array(
1866 'label' => __( 'PHP Version', 'wp-memory-usage' ),
1867 'status' => 'warn',
1868 'detail' => __( 'PHP', 'wp-memory-usage' ) . ' ' . $php_current . '' . __( 'works, but PHP', 'wp-memory-usage' ) . ' ' . $php_rec . '+ ' . __( 'is recommended (required for match-expressions)', 'wp-memory-usage' ),
1869 'hint' => __( 'Consider upgrading to PHP 8.0 or higher for full feature support.', 'wp-memory-usage' ),
1870 );
1871 } else {
1872 $checks[] = array(
1873 'label' => __( 'PHP Version', 'wp-memory-usage' ),
1874 'status' => 'ok',
1875 'detail' => __( 'PHP', 'wp-memory-usage' ) . ' ' . $php_current . ': ' . __( 'OK', 'wp-memory-usage' ),
1876 'hint' => '',
1877 );
1878 }
1879
1880 // ── 2. PHP-Erweiterungen ───────────────────────────────
1881 foreach ( array( 'json', 'pcre' ) as $ext ) {
1882 if ( extension_loaded( $ext ) ) {
1883 $checks[] = array(
1884 'label' => __( 'PHP extension:', 'wp-memory-usage' ) . ' ' . $ext,
1885 'status' => 'ok',
1886 'detail' => 'ext/' . $ext . ' ' . __( 'loaded', 'wp-memory-usage' ),
1887 'hint' => ''
1888 );
1889 } else {
1890 $checks[] = array(
1891 'label' => __( 'PHP extension:', 'wp-memory-usage' ) . ' ' . $ext,
1892 'status' => 'error',
1893 'detail' => 'ext/' . $ext . ' ' . __( 'NOT loaded – required by this plugin', 'wp-memory-usage' ),
1894 'hint' => 'ext/' . $ext . ' ' . __( 'in your php.ini.', 'wp-memory-usage' )
1895 );
1896 }
1897 }
1898
1899 // ── 3. Log-Verzeichnis: vorhanden / erstellbar ─────────
1900 $log_dir = self::WPMU_LOG_PATH;
1901 if ( is_dir( $log_dir ) ) {
1902 $checks[] = array(
1903 'label' => __( 'Log directory exists', 'wp-memory-usage' ),
1904 'status' => 'ok',
1905 'detail' => esc_html( $log_dir ),
1906 'hint' => '',
1907 );
1908 } else {
1909 // Versuch, es anzulegen
1910 if ( ! $wp_filesystem->is_dir( $log_dir ) ) {
1911 $created = $wp_filesystem->mkdir( $log_dir, 0755 );
1912 }
1913 #$created = @mkdir( $log_dir, 0755, true );
1914
1915 if ( $created ) {
1916 $checks[] = array(
1917 'label' => __( 'Log directory exists', 'wp-memory-usage' ),
1918 'status' => 'warn',
1919 'detail' => __('Directory did not exist – created now: ', 'wp-memory-usage' ) . esc_html( $log_dir ),
1920 'hint' => __('Make sure this path persists across deployments.', 'wp-memory-usage' ),
1921 );
1922 } else {
1923 $checks[] = array(
1924 'label' => __( 'Log directory exists', 'wp-memory-usage' ),
1925 'status' => 'error',
1926 'detail' => __('Does NOT exist and could not be created: ', 'wp-memory-usage' ) . esc_html( $log_dir ),
1927 'hint' => __('Create the directory manually and make it writable for the webserver user (e.g. www-data). Command: mkdir -p ', 'wp-memory-usage' ) . $log_dir . ' && chmod 755 ' . $log_dir,
1928 );
1929 }
1930 }
1931
1932 // ── 4. Log-Verzeichnis: schreibbar ────────────────────
1933 if ( is_dir( $log_dir ) ) {
1934 #if ( is_writable( $log_dir ) ) {
1935 if ( $wp_filesystem->is_writable( $log_dir ) ) {
1936 $checks[] = array(
1937 'label' => __('Log directory writable', 'wp-memory-usage' ),
1938 'status' => 'ok',
1939 'detail' => __('Directory is writable', 'wp-memory-usage' ),
1940 'hint' => ''
1941 );
1942 } else {
1943 $checks[] = array(
1944 'label' => __('Log directory writable', 'wp-memory-usage' ),
1945 'status' => 'error',
1946 'detail' => __('Directory exists but is NOT writable: ', 'wp-memory-usage' ) . esc_html( $log_dir ),
1947 'hint' => __('Run: chmod 755 ', 'wp-memory-usage' ) . $log_dir . __(' (or chown it to the webserver user)', 'wp-memory-usage' ),
1948 );
1949 }
1950 }
1951
1952 // ── 5. Log-Datei: schreiben & lesen ───────────────────
1953 if ( is_dir( $log_dir ) && $wp_filesystem->is_writable( $log_dir ) ) {
1954 $test_file = $log_dir . 'wpmu-test-' . time() . '.tmp';
1955 $test_data = array( 'wpmu_test' => true, 'ts' => time() );
1956 // phpcs:disable WordPress.WP.AlternativeFunctions
1957 $write_ok = @file_put_contents( $test_file, json_encode( $test_data ) . "\n" );
1958 // phpcs:enable WordPress.WP.AlternativeFunctions
1959 if ( $write_ok !== false ) {
1960 // phpcs:disable WordPress.WP.AlternativeFunctions
1961 $read_back = @file_get_contents( $test_file );
1962 // phpcs:enable WordPress.WP.AlternativeFunctions
1963 $parsed = json_decode( trim( $read_back ), true );
1964 wp_delete_file( $test_file );
1965 if ( is_array( $parsed ) && ! empty( $parsed['wpmu_test'] ) ) {
1966 $checks[] = array(
1967 'label' => __('Log file write & read test', 'wp-memory-usage' ),
1968 'status' => 'ok',
1969 'detail' => __('Write → read → parse: OK', 'wp-memory-usage' ),
1970 'hint' => '' );
1971 } else {
1972 $checks[] = array(
1973 'label' => __('Log file write & read test', 'wp-memory-usage' ),
1974 'status' => 'error',
1975 'detail' => __('File written but JSON parse failed', 'wp-memory-usage' ),
1976 'hint' => __('Check filesystem for corruption or encoding issues.', 'wp-memory-usage' )
1977 );
1978 }
1979 } else {
1980 $checks[] = array(
1981 'label' => __('Log file write & read test', 'wp-memory-usage' ),
1982 'status' => 'error',
1983 'detail' => __('Could not write test file to ', 'wp-memory-usage' ) . esc_html( $log_dir ),
1984 'hint' => __('Check directory permissions and available disk space.', 'wp-memory-usage' )
1985 );
1986 }
1987 } else {
1988 $checks[] = array(
1989 'label' => __('Log file write & read test', 'wp-memory-usage' ),
1990 'status' => 'error',
1991 'detail' => __('Skipped – log directory not writable', 'wp-memory-usage' ),
1992 'hint' => __('Fix the log directory issue first.', 'wp-memory-usage' )
1993 );
1994 }
1995
1996 // ── 6. Vorhandene Logdateien ──────────────────────────
1997 if ( is_dir( $log_dir ) ) {
1998 $log_file = $log_dir . self::WPMU_LOG_FILE;
1999 $digest_files = glob( $log_dir . 'digest_*.cgibak' );
2000 $backup_files = glob( $log_dir . 'wpmu-log_*.cgibak' );
2001 $n_digest = is_array( $digest_files ) ? count( $digest_files ) : 0;
2002 $n_backup = is_array( $backup_files ) ? count( $backup_files ) : 0;
2003 $log_exists = file_exists( $log_file );
2004 $log_size = $log_exists ? size_format( filesize( $log_file ) ) : '';
2005 $detail =
2006 __( 'Active log:', 'wp-memory-usage' ) . ' ' .
2007 ( $log_exists ? __( 'exists', 'wp-memory-usage' ) : __( 'not yet created', 'wp-memory-usage' ) ) .
2008 ' (' . __( 'size:', 'wp-memory-usage' ) . ' ' . $log_size . ') | ' .
2009 __( 'Digest files:', 'wp-memory-usage' ) . ' ' . $n_digest . ' | ' .
2010 __( 'Backup log files:', 'wp-memory-usage' ) . ' ' . $n_backup;
2011
2012 $checks[] = array(
2013 'label' => __('Log files overview', 'wp-memory-usage' ),
2014 'status' => 'ok',
2015 'detail' => $detail,
2016 'hint' => $n_digest === 0 ? __('No digest files yet – they are created automatically on the first cron run.', 'wp-memory-usage' ) : '',
2017 );
2018 }
2019
2020 // ── 7. WP-Cron aktiv ──────────────────────────────────
2021 if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
2022 $checks[] = array(
2023 'label' => 'WP-Cron',
2024 'status' => 'warn',
2025 'detail' => __('DISABLE_WP_CRON is TRUE – WP-Cron is disabled', 'wp-memory-usage' ),
2026 'hint' => __('This plugin uses WP-Cron for log rotation and digest creation. Make sure a real system cron runs wp-cron.php regularly (e.g. every 5 minutes), otherwise no digests will be created.', 'wp-memory-usage' ),
2027 );
2028 } else {
2029 $next_cleanup = wp_next_scheduled( 'wpmu_cleanup_hook' );
2030 if ( $next_cleanup ) {
2031 $checks[] = array(
2032 'label' => __('WP-Cron: cleanup hook scheduled', 'wp-memory-usage' ),
2033 'status' => 'ok',
2034 'detail' => __('Next run: ', 'wp-memory-usage' ) . wp_date( get_option('date_format') . ', ' . get_option('time_format'), $next_cleanup ),
2035 'hint' => '',
2036 );
2037 } else {
2038 $checks[] = array(
2039 'label' => __('WP-Cron: cleanup hook scheduled', 'wp-memory-usage' ),
2040 'status' => 'warn',
2041 'detail' => __('wpmu_cleanup_hook is NOT scheduled', 'wp-memory-usage' ),
2042 'hint' => __('Deactivate and reactivate the plugin, or visit the settings page once to trigger rescheduling.', 'wp-memory-usage' ),
2043 );
2044 }
2045 }
2046
2047 // ── 8. wp_mail verfügbar ──────────────────────────────
2048 if ( function_exists( 'wp_mail' ) ) {
2049 $opts_chk = self::get_settings();
2050 $mail_en = ! empty( $opts_chk['send_email'] );
2051 $mail_to = $opts_chk['email_to'] ?? '';
2052 $mail_valid = is_email( $mail_to );
2053 if ( $mail_en && $mail_valid ) {
2054 $checks[] = array(
2055 'label' => __('Email alerts', 'wp-memory-usage' ),
2056 'status' => 'ok',
2057 'detail' => __('Enabled, recipient: ', 'wp-memory-usage' ) . esc_html( $mail_to ),
2058 'hint' => ''
2059 );
2060 } elseif ( $mail_en && ! $mail_valid ) {
2061 $checks[] = array(
2062 'label' => __('Email alerts', 'wp-memory-usage' ),
2063 'status' => 'error',
2064 'detail' => __('Enabled but recipient address is invalid: "', 'wp-memory-usage' ) . esc_html( $mail_to ) . '"',
2065 'hint' => __('Enter a valid email address in Settings.' , 'wp-memory-usage' )
2066 );
2067 } else {
2068 $checks[] = array(
2069 'label' => __( 'Email alerts', 'wp-memory-usage' ),
2070 'status' => 'warn',
2071 'detail' => __( 'Disabled in settings', 'wp-memory-usage' ),
2072 'hint' => __( 'Enable "Send email alerts" in Settings to receive notifications.', 'wp-memory-usage' ),
2073 );
2074 }
2075 } else {
2076 $checks[] = array(
2077 'label' => __( 'Email alerts', 'wp-memory-usage' ),
2078 'status' => 'error',
2079 'detail' => __( 'wp_mail() not available', 'wp-memory-usage' ),
2080 'hint' => __( 'Email sending is not functional on this installation.', 'wp-memory-usage' ),
2081 );
2082 }
2083
2084 // ── 9. memory_get_peak_usage verfügbar ────────────────
2085 if ( function_exists( 'memory_get_peak_usage' ) ) {
2086 $checks[] = array(
2087 'label' => 'memory_get_peak_usage()',
2088 'status' => 'ok',
2089 'detail' => __('Function available – current peak: ', 'wp-memory-usage' ) . size_format( memory_get_peak_usage( true ) ),
2090 'hint' => '' );
2091 } else {
2092 $checks[] = array(
2093 'label' => __('memory_get_peak_usage()', 'wp-memory-usage' ),
2094 'status' => 'warn',
2095 'detail' => __('Not available on this PHP build', 'wp-memory-usage' ),
2096 'hint' => __('Peak memory tracking is disabled. The plugin falls back to memory_get_usage().', 'wp-memory-usage' )
2097 );
2098 }
2099
2100 // ── 10. Disk-Space ────────────────────────────────────
2101 if ( is_dir( $log_dir ) ) {
2102 $free = @disk_free_space( $log_dir );
2103 if ( $free !== false ) {
2104 $status_disk = $free < 10 * 1024 * 1024 ? 'warn' : 'ok'; // < 10 MB
2105 $checks[] = array(
2106 'label' => __('Disk space (log directory)', 'wp-memory-usage' ),
2107 'status' => $status_disk,
2108 'detail' => __('Free: ', 'wp-memory-usage' ) . size_format( $free ),
2109 'hint' => $status_disk === 'warn' ? __('Very little free disk space – log files may not be written.', 'wp-memory-usage' ) : '',
2110 );
2111 }
2112 }
2113
2114 // ── Render ─────────────────────────────────────────────
2115 $count_ok = count( array_filter( $checks, fn( $c ) => $c['status'] === 'ok' ) );
2116 $count_warn = count( array_filter( $checks, fn( $c ) => $c['status'] === 'warn' ) );
2117 $count_error = count( array_filter( $checks, fn( $c ) => $c['status'] === 'error' ) );
2118
2119 $overall_color = $count_error > 0 ? '#8B0000' : ( $count_warn > 0 ? '#7a5200' : '#2d6a2d' );
2120 $overall_bg = $count_error > 0 ? '#fdf2f2' : ( $count_warn > 0 ? '#fef9ec' : '#f2faf2' );
2121 $overall_icon = $count_error > 0 ? '🆘' : ( $count_warn > 0 ? '⚠️' : '�
2122 ' );
2123 $overall_text = $count_error > 0
2124 ? $count_error . ' ' . __( 'error(s),', 'wp-memory-usage' ) . ' ' . $count_warn . ' ' . __( 'warning(s)', 'wp-memory-usage' )
2125 : ( $count_warn > 0
2126 ? $count_warn . ' ' . __( 'warning(s) no errors', 'wp-memory-usage' )
2127 : __( 'All checks passed', 'wp-memory-usage' ) );
2128 ?>
2129 <h2><?php echo esc_html__( 'Check Installation', 'wp-memory-usage' ); ?></h2>
2130 <p><?php echo esc_html__( 'This tab checks whether the plugin can work correctly on this server.', 'wp-memory-usage' ); ?></p>
2131
2132 <div style="display:inline-flex;align-items:center;gap:10px;padding:10px 18px;border-radius:6px;margin-bottom:18px;background:<?php echo esc_attr( $overall_bg ); ?>;border:2px solid <?php echo esc_attr( $overall_color ); ?>;color:<?php echo esc_attr( $overall_color ); ?>;font-weight:700;font-size:14px;">
2133 <?php echo esc_html($overall_icon); ?> <?php echo esc_html( $overall_text ); ?>
2134 &nbsp;|&nbsp; �
2135 <?php echo (int) $count_ok; ?>
2136 &nbsp; ⚠️ <?php echo (int) $count_warn; ?>
2137 &nbsp; 🆘 <?php echo (int) $count_error; ?>
2138 </div>
2139
2140 <table class="widefat striped" style="max-width:1100px;">
2141 <thead>
2142 <tr>
2143 <th style="width:24px;"></th>
2144 <th style="width:280px;"><?php echo esc_html__( 'Check', 'wp-memory-usage' ); ?></th>
2145 <th><?php echo esc_html__( 'Result', 'wp-memory-usage' ); ?></th>
2146 <th><?php echo esc_html__( 'Hint / Action', 'wp-memory-usage' ); ?></th>
2147 </tr>
2148 </thead>
2149 <tbody>
2150 <?php foreach ( $checks as $chk ) :
2151 $icon = $chk['status'] === 'ok' ? '�
2152 ' : ( $chk['status'] === 'warn' ? '⚠️' : '🆘' );
2153 $color = $chk['status'] === 'ok' ? '' : ( $chk['status'] === 'warn' ? '#7a5200' : '#8B0000' );
2154 $bg = $chk['status'] === 'ok' ? '' : ( $chk['status'] === 'warn' ? '#fef9ec' : '#fdf2f2' );
2155 ?>
2156 <tr<?php echo $bg ? ' style="background:' . esc_attr( $bg ) . ';"' : ''; ?>>
2157 <td style="text-align:center;font-size:16px;"><?php echo esc_html($icon); ?></td>
2158 <td><strong style="color:<?php echo esc_attr( $color ); ?>"><?php echo esc_html( $chk['label'] ); ?></strong></td>
2159 <td style="font-family:monospace;font-size:12px;color:<?php echo esc_attr( $color ); ?>"><?php echo esc_html( $chk['detail'] ); ?></td>
2160 <td style="font-size:12px;color:#555;"><?php echo esc_html( $chk['hint'] ); ?></td>
2161 </tr>
2162 <?php endforeach; ?>
2163 </tbody>
2164 </table>
2165
2166 <?php endif; ?>
2167 </div>
2168 <?php
2169 }
2170
2171 /** ---------------- Helpers ---------------- */
2172 private static function format_bytes( $bytes ) {
2173 $bytes = (int) $bytes;
2174 if ( $bytes <= 0 ) { return '0 B'; }
2175 $units = array( 'B', 'KB', 'MB', 'GB', 'TB' );
2176 $idx = 0;
2177 $val = (float) $bytes;
2178 while ( $val >= 1024 && $idx < count( $units ) - 1 ) { $val /= 1024; $idx++; }
2179 return sprintf( $idx === 0 ? '%.0f %s' : '%.1f %s', $val, $units[ $idx ] );
2180 }
2181
2182 private static function format_pct( $usage, $limit ) {
2183 if ( $limit <= 0 ) { return 'n/a'; }
2184 return sprintf( '%.1f%%', ( $usage / $limit ) * 100 );
2185 }
2186
2187 private static function get_effective_memory_limit_bytes() {
2188 $php_raw = ini_get( 'memory_limit' );
2189 $php = self::parse_size_to_bytes( is_string( $php_raw ) ? $php_raw : '' );
2190 $wp_raw = defined( 'WP_MEMORY_LIMIT' ) ? (string) WP_MEMORY_LIMIT : '';
2191 $wp = self::parse_size_to_bytes( $wp_raw );
2192 if ( '-1' === (string) $php_raw || -1 === $php ) {
2193 return $wp > 0 ? $wp : 0;
2194 }
2195 if ( $php > 0 && $wp > 0 ) { return min( $php, $wp ); }
2196 return $php > 0 ? $php : ( $wp > 0 ? $wp : 0 );
2197 }
2198
2199 private static function parse_size_to_bytes( $val ) {
2200 $val = trim( (string) $val );
2201 if ( '' === $val ) { return 0; }
2202 if ( '-1' === $val ) { return -1; }
2203 if ( ctype_digit( $val ) ) { return (int) $val; }
2204 $unit = strtoupper( substr( $val, -1 ) );
2205 $num = substr( $val, 0, -1 );
2206 if ( ! is_numeric( $num ) ) { return 0; }
2207 $n = (float) $num;
2208 switch ( $unit ) {
2209 case 'K': return (int) round( $n * 1024 );
2210 case 'M': return (int) round( $n * 1024 * 1024 );
2211 case 'G': return (int) round( $n * 1024 * 1024 * 1024 );
2212 case 'T': return (int) round( $n * 1024 * 1024 * 1024 * 1024 );
2213 default: return 0;
2214 }
2215 }
2216
2217 /** ---------------- Threshold evaluation ---------------- */
2218 private static function state_for_usage( $usage, $limit, $opts ) {
2219 if ( $limit <= 0 ) { return 'ok'; }
2220 $ratio = $usage / $limit;
2221 $warn = max( 0.01, ( (int) $opts['warn_pct'] ) / 100 );
2222 $danger = max( 0.01, ( (int) $opts['danger_pct'] ) / 100 );
2223 $critical = max( 0.01, ( (int) $opts['critical_pct'] ) / 100 );
2224 if ( $ratio >= $critical ) { return 'critical'; }
2225 if ( $ratio >= $danger ) { return 'danger'; }
2226 if ( $ratio >= $warn ) { return 'warn'; }
2227 return 'ok';
2228 }
2229
2230 private static function state_rank( $state ) {
2231 switch ( $state ) {
2232 case 'warn': return 1;
2233 case 'danger': return 2;
2234 case 'critical': return 3;
2235 default: return 0;
2236 }
2237 }
2238
2239 private static function get_current_memory_bytes( $use_peak ) {
2240 if ( $use_peak && function_exists( 'memory_get_peak_usage' ) ) {
2241 return (int) memory_get_peak_usage( true );
2242 }
2243 return (int) memory_get_usage( true );
2244 }
2245
2246 } // end class
2247 endif;
2248 // Boot
2249 #WPMU_Threshold_Alerts::init();
2250