get_capability(); $this->dashboard_hook = add_menu_page( __( 'ErrorWatcher', 'errorwatcher' ), __( 'ErrorWatcher', 'errorwatcher' ), $capability, 'errorwatcher', array( $this, 'render_dashboard_page' ), 'dashicons-visibility', 98 ); add_submenu_page( 'errorwatcher', __( 'Dashboard & Logs - ErrorWatcher', 'errorwatcher' ), __( 'Dashboard / Logs', 'errorwatcher' ), $capability, 'errorwatcher', array( $this, 'render_dashboard_page' ) ); $this->settings_hook = add_submenu_page( 'errorwatcher', __( 'Settings - ErrorWatcher', 'errorwatcher' ), __( 'Settings', 'errorwatcher' ), $capability, 'errorwatcher-settings', array( $this, 'render_settings_page' ) ); } /** * Enqueue stylesheet and JavaScript on plugin pages only. * * @param string $hook * @return void */ public function enqueue_assets( string $hook ): void { if ( $hook !== $this->dashboard_hook && $hook !== $this->settings_hook ) { return; } wp_enqueue_style( 'errorwatcher-admin', ERRORWATCHER_URL . 'admin/css/admin.css', array(), ERRORWATCHER_VERSION ); wp_enqueue_script( 'errorwatcher-admin', ERRORWATCHER_URL . 'admin/js/admin.js', array( 'jquery' ), ERRORWATCHER_VERSION, true ); $localize_data = array( 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'errorwatcher_nonce' ), 'i18n' => array( 'confirm_clear_all' => __( 'Are you sure you want to clear all error logs? This action cannot be undone.', 'errorwatcher' ), 'confirm_bulk_delete' => __( 'Are you sure you want to delete the selected logs?', 'errorwatcher' ), 'confirm_delete' => __( 'Are you sure you want to delete this log?', 'errorwatcher' ), 'copied' => __( 'Copied to clipboard!', 'errorwatcher' ), 'sending_email' => __( 'Sending test email...', 'errorwatcher' ), 'loading' => __( 'Loading...', 'errorwatcher' ), 'error_occurred' => __( 'An error occurred. Please try again.', 'errorwatcher' ), ), ); wp_localize_script( 'errorwatcher-admin', 'errorwatcher_admin', $localize_data ); } /** * Handle settings form submission. * * @return void */ public function handle_settings_save(): void { if ( ! isset( $_POST['errorwatcher_save_settings'] ) ) { return; } if ( ! current_user_can( $this->get_capability() ) ) { wp_die( esc_html__( 'Unauthorized access.', 'errorwatcher' ), '', array( 'response' => 403 ) ); } check_admin_referer( 'errorwatcher_settings_action', 'errorwatcher_settings_nonce' ); $options = array( 'enabled' => isset( $_POST['enabled'] ) ? 1 : 0, 'admin_bar_badge' => isset( $_POST['admin_bar_badge'] ) ? 1 : 0, 'capture_fatals' => isset( $_POST['capture_fatals'] ) ? 1 : 0, 'capture_errors' => isset( $_POST['capture_errors'] ) ? 1 : 0, 'capture_warnings' => isset( $_POST['capture_warnings'] ) ? 1 : 0, 'capture_notices' => isset( $_POST['capture_notices'] ) ? 1 : 0, 'capture_deprecated' => isset( $_POST['capture_deprecated'] ) ? 1 : 0, 'email_reports_enabled' => isset( $_POST['email_reports_enabled'] ) ? 1 : 0, 'notification_email' => isset( $_POST['notification_email'] ) ? $this->sanitize_multiple_emails( sanitize_text_field( wp_unslash( $_POST['notification_email'] ) ) ) : get_option( 'admin_email' ), 'morning_time' => isset( $_POST['morning_time'] ) ? sanitize_text_field( wp_unslash( $_POST['morning_time'] ) ) : '08:00', 'evening_time' => isset( $_POST['evening_time'] ) ? sanitize_text_field( wp_unslash( $_POST['evening_time'] ) ) : '20:00', ); update_option( 'errorwatcher_settings', $options ); // Reschedule cron tasks to reflect updated times or state. ErrorWatcher_Cron::get_instance()->reschedule_events(); wp_safe_redirect( add_query_arg( array( 'page' => 'errorwatcher-settings', 'updated' => 'true' ), admin_url( 'admin.php' ) ) ); exit; } /** * Sanitize and validate comma-separated email addresses. * * @param string $raw * @return string */ private function sanitize_multiple_emails( string $raw ): string { if ( empty( $raw ) ) { return get_option( 'admin_email' ); } $valid = array(); foreach ( explode( ',', $raw ) as $part ) { $email = sanitize_email( trim( $part ) ); if ( is_email( $email ) ) { $valid[] = $email; } } return ! empty( $valid ) ? implode( ', ', array_unique( $valid ) ) : get_option( 'admin_email' ); } /** * Handle CSV/TXT log export download. * * @return void */ public function handle_export(): void { if ( ! isset( $_GET['errorwatcher_export'] ) ) { return; } if ( ! current_user_can( $this->get_capability() ) ) { wp_die( esc_html__( 'Unauthorized access.', 'errorwatcher' ), '', array( 'response' => 403 ) ); } check_admin_referer( 'errorwatcher_export_action' ); $raw_format = sanitize_key( $_GET['errorwatcher_export'] ); $format = ( 'csv' === $raw_format ) ? 'csv' : 'txt'; $args = array( 'search' => isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '', 'severity' => isset( $_GET['severity'] ) ? sanitize_text_field( wp_unslash( $_GET['severity'] ) ) : '', 'date_range' => isset( $_GET['date_range'] ) ? sanitize_text_field( wp_unslash( $_GET['date_range'] ) ) : '', 'start_date' => isset( $_GET['start_date'] ) ? sanitize_text_field( wp_unslash( $_GET['start_date'] ) ) : '', 'end_date' => isset( $_GET['end_date'] ) ? sanitize_text_field( wp_unslash( $_GET['end_date'] ) ) : '', ); $logs = ErrorWatcher_DB::get_instance()->get_logs_for_export( $args ); $filename = 'errorwatcher-logs-' . gmdate( 'Y-m-d-His' ); if ( 'csv' === $format ) { header( 'Content-Type: text/csv; charset=UTF-8' ); header( 'Content-Disposition: attachment; filename="' . $filename . '.csv"' ); header( 'Pragma: no-cache' ); header( 'Expires: 0' ); $output = fopen( 'php://output', 'w' ); // Add UTF-8 BOM for Excel compatibility. fprintf( $output, chr( 0xEF ) . chr( 0xBB ) . chr( 0xBF ) ); fputcsv( $output, array( 'ID', 'Severity/Type', 'Severity Code', 'Message', 'File', 'Line', 'Request URL', 'HTTP Method', 'User ID', 'IP Address', 'Date Time' ), ',', '"', '\\' ); foreach ( $logs as $row ) { fputcsv( $output, array( $row->id, $row->error_type, $row->severity_code, $row->message, $row->file, $row->line, $row->request_url, $row->http_method, $row->user_id, $row->ip_address, $row->created_at, ), ',', '"', '\\' ); } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing php://output stream. fclose( $output ); exit; } elseif ( 'txt' === $format ) { header( 'Content-Type: text/plain; charset=UTF-8' ); header( 'Content-Disposition: attachment; filename="' . $filename . '.txt"' ); header( 'Pragma: no-cache' ); header( 'Expires: 0' ); echo "=======================================================\n"; echo " ERRORWATCHER - LOG EXPORT\n"; echo " Site: " . esc_html( get_bloginfo( 'name' ) ) . " (" . esc_url( site_url() ) . ")\n"; echo " Exported At: " . esc_html( current_time( 'mysql' ) ) . "\n"; echo " Total Records: " . count( $logs ) . "\n"; echo "=======================================================\n\n"; foreach ( $logs as $index => $row ) { $i = $index + 1; echo "-------------------------------------------------------\n"; echo esc_html( "[#{$i}] {$row->error_type} | {$row->created_at}\n" ); echo esc_html( "Message: {$row->message}\n" ); echo esc_html( "Location: {$row->file}:{$row->line}\n" ); if ( ! empty( $row->request_url ) ) { echo esc_html( "Request: [{$row->http_method}] {$row->request_url}\n" ); } echo "\n"; } exit; } } /** * Retrieve source code snippet around a specific line safely. * * @param string $file Absolute file path. * @param int $target_line Target line number. * @param int $context Lines of context before and after (default 4). * @return array|null Snippet data or null if unavailable. */ public function get_code_snippet( string $file, int $target_line, int $context = 4 ): ?array { if ( empty( $file ) || $target_line <= 0 ) { return null; } $real_file = realpath( $file ); if ( ! $real_file || ! is_file( $real_file ) || ! is_readable( $real_file ) ) { return null; } // Security constraint: Must reside inside WordPress installation directory. $is_safe = false; $allowed_bases = array_filter( array( realpath( ABSPATH ), defined( 'WP_CONTENT_DIR' ) ? realpath( WP_CONTENT_DIR ) : '', defined( 'WPMU_PLUGIN_DIR' ) ? realpath( WPMU_PLUGIN_DIR ) : '', ) ); foreach ( $allowed_bases as $base ) { if ( $base && 0 === strpos( $real_file, $base ) ) { $is_safe = true; break; } } if ( ! $is_safe ) { return null; } $is_config = ( 'wp-config.php' === strtolower( basename( $real_file ) ) ); try { $file_obj = new \SplFileObject( $real_file, 'r' ); $start = max( 1, $target_line - $context ); $end = $target_line + $context; $lines = array(); $current_line = 1; while ( ! $file_obj->eof() ) { $line_content = $file_obj->fgets(); if ( $current_line >= $start && $current_line <= $end ) { // Defense in depth: Mask sensitive database / salt credentials if viewing wp-config.php. if ( $is_config && ( false !== stripos( $line_content, 'DB_PASSWORD' ) || false !== stripos( $line_content, 'AUTH_KEY' ) || false !== stripos( $line_content, 'SECURE_AUTH_KEY' ) || false !== stripos( $line_content, 'LOGGED_IN_KEY' ) || false !== stripos( $line_content, 'NONCE_KEY' ) ) ) { $line_content = (string) preg_replace( "/(define\s*\(\s*['\"][^'\"]+['\"]\s*,\s*['\"])([^'\"]+)(['\"]\s*\);?)/i", '$1********$3', $line_content ); } $lines[] = array( 'number' => $current_line, 'is_target' => ( $current_line === $target_line ), 'code' => rtrim( $line_content, "\r\n" ), ); } if ( $current_line > $end ) { break; } $current_line++; } if ( empty( $lines ) ) { return null; } return array( 'file' => $real_file, 'filename' => basename( $real_file ), 'start_line' => $start, 'end_line' => min( $end, $current_line - 1 ), 'target_line' => $target_line, 'lines' => $lines, ); } catch ( \Throwable $e ) { return null; } } /** * AJAX endpoint to retrieve a single log record. * * @return void */ public function ajax_get_log(): void { check_ajax_referer( 'errorwatcher_nonce', 'nonce' ); if ( ! current_user_can( $this->get_capability() ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized.', 'errorwatcher' ) ), 403 ); } $id = isset( $_POST['id'] ) ? absint( wp_unslash( $_POST['id'] ) ) : 0; $log = ErrorWatcher_DB::get_instance()->get_log( $id ); if ( ! $log ) { wp_send_json_error( array( 'message' => __( 'Log record not found.', 'errorwatcher' ) ), 404 ); } $user_info = 'Guest / Non-authenticated'; if ( ! empty( $log->user_id ) ) { $user = get_userdata( (int) $log->user_id ); if ( $user ) { $user_info = sprintf( '%s (#%d - %s)', $user->display_name, $user->ID, $user->user_email ); } } $code_snippet = $this->get_code_snippet( (string) $log->file, (int) $log->line, 4 ); $siblings_data = array(); if ( ! empty( $log->sibling_issues ) && is_array( $log->sibling_issues ) ) { foreach ( $log->sibling_issues as $s ) { $s_file = (string) ( $s['file'] ?? $log->file ); $s_line = (int) ( $s['line'] ?? 0 ); $siblings_data[] = array( 'id' => $s['id'] ?? 0, 'issue_number' => $s['issue_number'] ?? 2, 'error_type' => $s['error_type'] ?? 'Error', 'message' => esc_html( $s['message'] ?? '' ), 'file' => esc_html( $s_file ), 'line' => $s_line, 'source' => esc_html( ErrorWatcher_DB::detect_source( $s_file ) ), 'code_snippet' => ( $s_line > 0 ) ? $this->get_code_snippet( $s_file, $s_line, 3 ) : null, 'stack_trace' => ! empty( $s['stack_trace'] ) ? esc_html( $s['stack_trace'] ) : '', 'created_at' => esc_html( $s['last_seen'] ?? $s['created_at'] ?? '' ), ); } } wp_send_json_success( array( 'id' => $log->id, 'issue_number' => 1, 'error_type' => $log->error_type, 'severity' => $log->severity_code, 'message' => esc_html( $log->message ), 'file' => esc_html( $log->file ), 'line' => (int) $log->line, 'source' => esc_html( ErrorWatcher_DB::detect_source( $log->file ) ), 'code_snippet' => $code_snippet, 'stack_trace' => ! empty( $log->stack_trace ) ? esc_html( $log->stack_trace ) : __( 'No stack trace available.', 'errorwatcher' ), 'request_url' => ! empty( $log->request_url ) ? esc_url( $log->request_url ) : 'N/A', 'http_method' => esc_html( $log->http_method ), 'user_info' => esc_html( $user_info ), 'ip_address' => esc_html( $log->ip_address ?: 'N/A' ), 'occurrences' => isset( $log->occurrences ) ? (int) $log->occurrences : 1, 'last_seen' => ! empty( $log->last_seen ) ? esc_html( $log->last_seen ) : esc_html( $log->created_at ), 'created_at' => esc_html( $log->created_at ), 'file_issues_count' => isset( $log->file_issues_count ) ? (int) $log->file_issues_count : 1, 'sibling_issues' => $siblings_data, ) ); } /** * AJAX endpoint to delete a single log record. * * @return void */ public function ajax_delete_log(): void { check_ajax_referer( 'errorwatcher_nonce', 'nonce' ); if ( ! current_user_can( $this->get_capability() ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized.', 'errorwatcher' ) ), 403 ); } $id = isset( $_POST['id'] ) ? absint( wp_unslash( $_POST['id'] ) ) : 0; $deleted = ErrorWatcher_DB::get_instance()->delete_log( $id ); if ( $deleted ) { wp_send_json_success( array( 'message' => __( 'Log entry deleted successfully.', 'errorwatcher' ) ) ); } else { wp_send_json_error( array( 'message' => __( 'Failed to delete log entry.', 'errorwatcher' ) ) ); } } /** * AJAX endpoint for bulk deleting logs. * * @return void */ public function ajax_bulk_delete(): void { check_ajax_referer( 'errorwatcher_nonce', 'nonce' ); if ( ! current_user_can( $this->get_capability() ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized.', 'errorwatcher' ) ), 403 ); } $ids = isset( $_POST['ids'] ) && is_array( $_POST['ids'] ) ? array_map( 'absint', wp_unslash( $_POST['ids'] ) ) : array(); if ( empty( $ids ) ) { wp_send_json_error( array( 'message' => __( 'No items selected.', 'errorwatcher' ) ), 403 ); } $deleted_count = ErrorWatcher_DB::get_instance()->delete_logs( $ids ); /* translators: %d: Number of deleted log entries. */ $message = sprintf( _n( '%d log entry deleted.', '%d log entries deleted.', $deleted_count, 'errorwatcher' ), $deleted_count ); wp_send_json_success( array( 'message' => $message, 'count' => $deleted_count, ) ); } /** * AJAX endpoint to clear all logs. * * @return void */ public function ajax_clear_all(): void { check_ajax_referer( 'errorwatcher_nonce', 'nonce' ); if ( ! current_user_can( $this->get_capability() ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized.', 'errorwatcher' ) ), 403 ); } $cleared = ErrorWatcher_DB::get_instance()->clear_all_logs(); if ( $cleared ) { wp_send_json_success( array( 'message' => __( 'All error logs have been cleared.', 'errorwatcher' ) ) ); } else { wp_send_json_error( array( 'message' => __( 'Failed to clear logs.', 'errorwatcher' ) ) ); } } /** * AJAX endpoint to send a test email. * * @return void */ public function ajax_send_test_email(): void { check_ajax_referer( 'errorwatcher_nonce', 'nonce' ); if ( ! current_user_can( $this->get_capability() ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized.', 'errorwatcher' ) ), 403 ); } $email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : ''; $result = ErrorWatcher_Cron::get_instance()->send_test_email( $email ); if ( $result['success'] ) { wp_send_json_success( array( 'message' => $result['message'] ) ); } else { wp_send_json_error( array( 'message' => $result['message'] ) ); } } /** * AJAX endpoint to trigger the scheduled digest manually on demand. * * @return void */ public function ajax_run_scheduled_report(): void { check_ajax_referer( 'errorwatcher_nonce', 'nonce' ); if ( ! current_user_can( $this->get_capability() ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized.', 'errorwatcher' ) ), 403 ); } $report_type = isset( $_POST['report_type'] ) ? sanitize_key( wp_unslash( $_POST['report_type'] ) ) : 'morning'; if ( 'evening' === $report_type ) { $sent = ErrorWatcher_Cron::get_instance()->send_evening_report( true ); } else { $sent = ErrorWatcher_Cron::get_instance()->send_morning_report( true ); } if ( $sent ) { /* translators: %s: Report schedule label (e.g. Morning or Evening). */ $message = sprintf( __( 'Scheduled %s report generated and delivered to Mailpit/inbox successfully!', 'errorwatcher' ), ucfirst( $report_type ) ); wp_send_json_success( array( 'message' => $message, ) ); } else { wp_send_json_error( array( 'message' => __( 'Failed to dispatch scheduled report. Please verify your recipient email in settings.', 'errorwatcher' ), ) ); } } /** * Render the Dashboard and Logs page. * * @return void */ public function render_dashboard_page(): void { include_once ERRORWATCHER_DIR . 'admin/views/dashboard.php'; } /** * Render the Settings page. * * @return void */ public function render_settings_page(): void { include_once ERRORWATCHER_DIR . 'admin/views/settings.php'; } /** * Ensure required assets (such as dashicons) are enqueued when admin bar is shown. * * @return void */ public function enqueue_admin_bar_assets(): void { if ( is_admin_bar_showing() && current_user_can( $this->get_capability() ) ) { wp_enqueue_style( 'dashicons' ); } } /** * Add real-time ErrorWatcher quick-status badge to the WordPress admin bar. * * @param \WP_Admin_Bar $wp_admin_bar * @return void */ public function add_admin_bar_menu( \WP_Admin_Bar $wp_admin_bar ): void { // Only display for logged-in users authorized to view logs. if ( ! is_user_logged_in() || ! current_user_can( $this->get_capability() ) ) { return; } $options = get_option( 'errorwatcher_settings', array() ); // Check if admin bar badge is disabled in settings. if ( isset( $options['admin_bar_badge'] ) && ! $options['admin_bar_badge'] ) { return; } $is_enabled = ! isset( $options['enabled'] ) || (bool) $options['enabled']; if ( ! $is_enabled ) { // Monitoring is turned off. $node_title = sprintf( '%s %s', esc_html__( 'ErrorWatcher', 'errorwatcher' ), esc_html__( 'Paused', 'errorwatcher' ) ); $wp_admin_bar->add_node( array( 'id' => 'errorwatcher-status', 'title' => $node_title, 'href' => admin_url( 'admin.php?page=errorwatcher-settings' ), 'meta' => array( 'title' => __( 'ErrorWatcher monitoring is paused. Click to configure settings.', 'errorwatcher' ), 'class' => 'errorwatcher-ab-node errorwatcher-ab-disabled', ), ) ); $wp_admin_bar->add_node( array( 'id' => 'errorwatcher-ab-header', 'parent' => 'errorwatcher-status', 'title' => sprintf( '%s', esc_html__( 'Monitoring is currently paused', 'errorwatcher' ) ), 'href' => admin_url( 'admin.php?page=errorwatcher-settings' ), ) ); $wp_admin_bar->add_node( array( 'id' => 'errorwatcher-ab-settings', 'parent' => 'errorwatcher-status', 'title' => sprintf( '%s', esc_html__( 'Enable Monitoring in Settings', 'errorwatcher' ) ), 'href' => admin_url( 'admin.php?page=errorwatcher-settings' ), ) ); return; } $stats = ErrorWatcher_DB::get_instance()->get_statistics(); $fatal_count = (int) ( ( $stats['fatal'] ?? 0 ) + ( $stats['exception'] ?? 0 ) ); $warning_count = (int) ( ( $stats['warning'] ?? 0 ) + ( $stats['error'] ?? 0 ) ); $notice_count = (int) ( ( $stats['notice'] ?? 0 ) + ( $stats['deprecated'] ?? 0 ) ); $total_count = (int) ( $stats['total'] ?? 0 ); // Determine severity state, styling, and text. $state_class = 'errorwatcher-ab-clean'; $pill_html = ''; $default_query = ''; if ( $fatal_count > 0 ) { $state_class = 'errorwatcher-ab-critical'; $default_query = '&severity=Fatal+Error'; /* translators: %d: Number of fatal errors. */ $fatal_label = sprintf( _n( '%d Fatal', '%d Fatals', $fatal_count, 'errorwatcher' ), $fatal_count ); if ( $warning_count > 0 ) { $pill_html = sprintf( '%s +%d', esc_html( $fatal_label ), $warning_count ); } else { $pill_html = sprintf( '%s', esc_html( $fatal_label ) ); } } elseif ( $warning_count > 0 ) { $state_class = 'errorwatcher-ab-warning'; $default_query = '&severity=Warning'; /* translators: %d: Number of warnings. */ $warn_label = sprintf( _n( '%d Warning', '%d Warnings', $warning_count, 'errorwatcher' ), $warning_count ); $pill_html = sprintf( '%s', esc_html( $warn_label ) ); } elseif ( $notice_count > 0 ) { $state_class = 'errorwatcher-ab-notice'; $default_query = '&severity=Notice'; /* translators: %d: Number of notices. */ $notice_label = sprintf( _n( '%d Notice', '%d Notices', $notice_count, 'errorwatcher' ), $notice_count ); $pill_html = sprintf( '%s', esc_html( $notice_label ) ); } else { $state_class = 'errorwatcher-ab-clean'; $pill_html = sprintf( ' %s', esc_html__( 'Clean', 'errorwatcher' ) ); } $node_title = sprintf( '%s: %s', esc_html__( 'ErrorWatcher', 'errorwatcher' ), $pill_html ); $dashboard_url = admin_url( 'admin.php?page=errorwatcher' . $default_query ); $wp_admin_bar->add_node( array( 'id' => 'errorwatcher-status', 'title' => $node_title, 'href' => $dashboard_url, 'meta' => array( 'title' => sprintf( /* translators: %d: Total logs count. */ __( 'ErrorWatcher: %d active issue(s) detected. Click to view logs.', 'errorwatcher' ), $total_count ), 'class' => 'errorwatcher-ab-node ' . $state_class, ), ) ); // Submenu Item 1: Status Overview Header. if ( $total_count === 0 ) { $wp_admin_bar->add_node( array( 'id' => 'errorwatcher-ab-header', 'parent' => 'errorwatcher-status', 'title' => sprintf( '%s', esc_html__( 'Site Healthy ยท 0 Active Errors', 'errorwatcher' ) ), 'href' => admin_url( 'admin.php?page=errorwatcher' ), ) ); } else { $wp_admin_bar->add_node( array( 'id' => 'errorwatcher-ab-header', 'parent' => 'errorwatcher-status', 'title' => sprintf( '%s%s', esc_html__( 'Active PHP Issues', 'errorwatcher' ), /* translators: %d: Total issues count. */ sprintf( _n( '%d Total', '%d Totals', $total_count, 'errorwatcher' ), $total_count ) ), 'href' => admin_url( 'admin.php?page=errorwatcher' ), ) ); } // Submenu Items: Granular filter deep-links with unified 20px icon column. if ( $fatal_count > 0 ) { $wp_admin_bar->add_node( array( 'id' => 'errorwatcher-ab-filter-fatal', 'parent' => 'errorwatcher-status', 'title' => sprintf( '%s%d', esc_html__( 'Fatal Errors / Exceptions', 'errorwatcher' ), $fatal_count ), 'href' => admin_url( 'admin.php?page=errorwatcher&severity=Fatal+Error' ), ) ); } if ( (int) ( $stats['warning'] ?? 0 ) > 0 ) { $wp_admin_bar->add_node( array( 'id' => 'errorwatcher-ab-filter-warning', 'parent' => 'errorwatcher-status', 'title' => sprintf( '%s%d', esc_html__( 'Warnings', 'errorwatcher' ), (int) $stats['warning'] ), 'href' => admin_url( 'admin.php?page=errorwatcher&severity=Warning' ), ) ); } if ( (int) ( $stats['error'] ?? 0 ) > 0 ) { $wp_admin_bar->add_node( array( 'id' => 'errorwatcher-ab-filter-recoverable', 'parent' => 'errorwatcher-status', 'title' => sprintf( '%s%d', esc_html__( 'Recoverable / User Errors', 'errorwatcher' ), (int) $stats['error'] ), 'href' => admin_url( 'admin.php?page=errorwatcher' ), ) ); } if ( (int) ( $stats['notice'] ?? 0 ) > 0 ) { $wp_admin_bar->add_node( array( 'id' => 'errorwatcher-ab-filter-notice', 'parent' => 'errorwatcher-status', 'title' => sprintf( '%s%d', esc_html__( 'Notices', 'errorwatcher' ), (int) $stats['notice'] ), 'href' => admin_url( 'admin.php?page=errorwatcher&severity=Notice' ), ) ); } if ( (int) ( $stats['deprecated'] ?? 0 ) > 0 ) { $wp_admin_bar->add_node( array( 'id' => 'errorwatcher-ab-filter-deprecated', 'parent' => 'errorwatcher-status', 'title' => sprintf( '%s%d', esc_html__( 'Deprecated Calls', 'errorwatcher' ), (int) $stats['deprecated'] ), 'href' => admin_url( 'admin.php?page=errorwatcher&severity=Deprecated' ), ) ); } // Submenu Item: View Dashboard. $wp_admin_bar->add_node( array( 'id' => 'errorwatcher-ab-dashboard-link', 'parent' => 'errorwatcher-status', 'title' => sprintf( '%s→', esc_html__( 'Open Diagnostics Dashboard', 'errorwatcher' ) ), 'href' => admin_url( 'admin.php?page=errorwatcher' ), ) ); // Submenu Item: Settings. $wp_admin_bar->add_node( array( 'id' => 'errorwatcher-ab-settings-link', 'parent' => 'errorwatcher-status', 'title' => sprintf( '%s', esc_html__( 'Monitoring Settings', 'errorwatcher' ) ), 'href' => admin_url( 'admin.php?page=errorwatcher-settings' ), ) ); } /** * Output scoped CSS styles for the ErrorWatcher Admin Bar status badge. * * @return void */ public function print_admin_bar_css(): void { if ( ! is_user_logged_in() || ! current_user_can( $this->get_capability() ) ) { return; } $options = get_option( 'errorwatcher_settings', array() ); if ( isset( $options['admin_bar_badge'] ) && ! $options['admin_bar_badge'] ) { return; } ?>