hooks(); } return self::$instance; } /** * Register admin hooks. */ private function hooks() { add_action( 'admin_menu', array( $this, 'menu' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'assets' ) ); add_action( 'admin_notices', array( $this, 'maybe_unwritable_notice' ) ); // Plugins-screen affordances (Code Snippets-style): quick links under // the plugin name + Docs/Support in the row meta. add_filter( 'plugin_action_links_' . CULPRIT_PLUGIN_BASENAME, array( $this, 'action_links' ) ); add_filter( 'plugin_row_meta', array( $this, 'row_meta' ), 10, 2 ); } /** * Action links shown under the plugin name on the Plugins screen. * Free build additionally gets a highlighted "Upgrade to Pro". * * @param array $links Existing links (Deactivate, etc.). * @return array */ public function action_links( $links ) { $ours = array( 'culprit-incidents' => '' . esc_html__( 'Incidents', 'culprit' ) . '', 'culprit-environment' => '' . esc_html__( 'Environment', 'culprit' ) . '', ); if ( class_exists( 'Culprit_License_Admin' ) ) { // Paid build: link straight to the License tab. $ours['culprit-license'] = '' . esc_html__( 'License', 'culprit' ) . ''; } else { // Free build: highlighted upgrade link (rendered after Deactivate, // like Code Snippets' "Upgrade to Pro"). $links['culprit-upgrade'] = '' . esc_html__( 'Upgrade to Pro', 'culprit' ) . ''; } return array_merge( $ours, $links ); } /** * Row meta (the line with Version / By): add Docs and Support links. * * @param array $meta Existing meta links. * @param string $file Plugin basename being rendered. * @return array */ public function row_meta( $meta, $file ) { if ( CULPRIT_PLUGIN_BASENAME !== $file ) { return $meta; } /** Filter the documentation URL shown on the Plugins screen. */ $docs_url = (string) apply_filters( 'culprit_docs_url', 'https://manicsoup.com/docs/culprit/' ); /** Filter the support URL shown on the Plugins screen. */ $support_url = (string) apply_filters( 'culprit_support_url', 'https://manicsoup.com/support/' ); $meta[] = '' . esc_html__( 'Docs', 'culprit' ) . ''; $meta[] = '' . esc_html__( 'Support', 'culprit' ) . ''; return $meta; } /** * Register the top-level menu. */ public function menu() { $cap = Culprit_Settings::capability(); add_menu_page( __( 'Culprit', 'culprit' ), __( 'Culprit', 'culprit' ), $cap, 'culprit', array( $this, 'render' ), 'dashicons-search', 81 ); } /** * Enqueue admin assets on our page only. * * @param string $hook Current admin page hook. */ public function assets( $hook ) { if ( 'toplevel_page_culprit' !== $hook ) { return; } wp_enqueue_style( 'culprit-admin', CULPRIT_PLUGIN_URL . 'assets/css/admin.css', array(), CULPRIT_VERSION ); } /** * Show a notice if the mu-plugins directory isn't writable. */ public function maybe_unwritable_notice() { if ( ! Culprit_Settings::current_user_can_manage() ) { return; } if ( ! get_option( 'culprit_watcher_unwritable' ) ) { return; } echo '
'; echo esc_html__( 'Culprit could not install its early-boot watcher because the mu-plugins directory is not writable. Culprit still catches later crashes, but the earliest boot fatals may be missed.', 'culprit' ); echo '
' . esc_html__( 'No crashes detected. ', 'culprit' ) . '
'; echo '' . esc_html__( 'When a plugin or theme breaks your site with a fatal error or white screen, Culprit will catch it and name the culprit here.', 'culprit' ) . '
'; echo '' . esc_html( sprintf( __( 'Most recent incident: %1$s, first seen %2$s.', 'culprit' ), $name, wp_strip_all_tags( Culprit_Settings::format_datetime( $inc->first_seen ) ) ) ) . '
'; if ( '' !== $diag['good_time'] ) { /* translators: %s: date/time. */ echo '' . esc_html( sprintf( __( 'Last known good state: %s', 'culprit' ), wp_strip_all_tags( Culprit_Settings::format_datetime( $diag['good_time'] ) ) ) ) . '
'; } if ( empty( $diag['events'] ) ) { echo '' . esc_html__( 'No recorded state change leads up to this incident. The cause may be environmental (server, PHP, or specific data) rather than an update.', 'culprit' ) . '
'; } else { echo '' . esc_html__( 'Likely cause:', 'culprit' ) . ' ' . esc_html( $diag['likely_cause'] ) . '
'; } if ( '' !== $diag['confidence_note'] ) { echo '' . esc_html( $diag['confidence_note'] ) . '
'; } echo '' . esc_html__( 'No state changes recorded yet. From now on, every plugin update, activation, theme switch, and WordPress or PHP upgrade will be recorded here.', 'culprit' ) . '
'; } else { echo '| ' . esc_html__( 'When', 'culprit' ) . ' | '; echo '' . esc_html__( 'What changed', 'culprit' ) . ' | '; echo '
|---|---|
| ' . Culprit_Settings::format_datetime( $event['time'] ) . ' | '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- format_datetime escapes. echo '' . esc_html( $event['text'] ) . ' | '; echo '
' . esc_html__( 'Every callback attached to a hook, in the order WordPress will run them — and which plugin or theme owns each one.', 'culprit' ) . '
'; echo ''; $rows = Culprit_Hook_Inspector::inspect( $hook ); if ( empty( $rows ) ) { /* translators: %s: hook name. */ echo '' . esc_html( sprintf( __( 'Nothing is attached to "%s" on this request.', 'culprit' ), $hook ) ) . '
'; } else { echo '| ' . esc_html__( 'Priority', 'culprit' ) . ' | '; echo '' . esc_html__( 'Callback', 'culprit' ) . ' | '; echo '' . esc_html__( 'Owner', 'culprit' ) . ' | '; echo '
|---|---|---|
| ' . esc_html( (string) $row['priority'] ) . ' | '; echo '' . esc_html( $row['callback'] ) . ' | ';
echo '' . esc_html( $row['owner_name'] ); $type_label = self::type_label( $row['owner_type'] ); if ( '' !== $type_label ) { echo ' ' . esc_html( $type_label ) . ''; } echo ' | '; echo '
' . esc_html__( 'No front-end page load recorded yet. Visit your site (while logged in or out) and reload this tab.', 'culprit' ) . '
'; } else { /* translators: 1: URL path, 2: date/time. */ echo '' . esc_html( sprintf( __( 'Last recorded load: %1$s at %2$s.', 'culprit' ), $timeline['url'], wp_strip_all_tags( Culprit_Settings::format_datetime( gmdate( 'Y-m-d H:i:s', $timeline['recorded'] ) ) ) ) ) . '
'; echo '| ' . esc_html__( 'Hook', 'culprit' ) . ' | '; echo '' . esc_html__( 'Fired at (ms)', 'culprit' ) . ' | '; echo '' . esc_html__( 'Stage took (ms)', 'culprit' ) . ' | '; echo '
|---|---|---|
' . esc_html( $row['hook'] ) . ' | ';
echo '' . esc_html( number_format_i18n( $row['at_ms'], 1 ) ) . ' | '; echo '' . esc_html( number_format_i18n( $row['delta_ms'], 1 ) ) . ' | '; echo '
' . esc_html__( 'Everything a support thread asks for, in one place. Export it as a text report and paste it anywhere.', 'culprit' ) . '
'; $export_url = wp_nonce_url( admin_url( 'admin-post.php?action=culprit_export_environment' ), 'culprit_export_environment' ); echo '' . esc_html__( 'Download support report', 'culprit' ) . '
'; foreach ( $sections as $section => $rows ) { echo '| ' . esc_html( $label ) . ' | ' . esc_html( $value ) . ' |
|---|
| ' . esc_html__( 'Culprit', 'culprit' ) . ' | '; echo '' . esc_html__( 'What happened', 'culprit' ) . ' | '; echo '' . esc_html__( 'Confidence', 'culprit' ) . ' | '; echo '' . esc_html__( 'Last seen', 'culprit' ) . ' | '; echo '' . esc_html__( 'Count', 'culprit' ) . ' | '; $show_actions = class_exists( 'Culprit_Deep_Scan_Admin' ) && Culprit_License::can( 'deep_scan' ); if ( $show_actions ) { echo '' . esc_html__( 'Prove', 'culprit' ) . ' | '; } echo '
|---|---|---|---|---|---|
| ' . esc_html( $name ) . ''; if ( '' !== $type_label ) { echo ' ' . esc_html( $type_label ) . ''; } echo ' | '; echo '';
echo esc_html( self::summarize( $inc ) );
// When premium correlation has written an analysis reason, show it.
$reason = self::incident_reason( $inc );
if ( '' !== $reason ) {
echo ' ' . esc_html( $reason ) . ' ';
}
echo ' | ';
echo '' . esc_html( (int) $inc->confidence ) . '% | '; echo '' . Culprit_Settings::format_datetime( $inc->last_seen ) . ' | '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- format_datetime escapes. echo '' . esc_html( (int) $inc->occurrences ) . ' | '; if ( $show_actions ) { echo '' . Culprit_Deep_Scan_Admin::scan_button( $inc ) . ' | '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- button markup escaped internally. } echo '
' . esc_html__( 'No recurring culprits yet. When the same plugin or theme breaks your site more than once, it will appear here.', 'culprit' ) . '
'; } else { echo '| ' . esc_html__( 'Culprit', 'culprit' ) . ' | '; echo '' . esc_html__( 'Incidents', 'culprit' ) . ' | '; echo '' . esc_html__( 'Total occurrences', 'culprit' ) . ' | '; echo '' . esc_html__( 'Last seen', 'culprit' ) . ' | '; echo '
|---|---|---|---|
| ' . esc_html( $name ) . ''; $type_label = self::type_label( $c['type'] ); if ( '' !== $type_label ) { echo ' ' . esc_html( $type_label ) . ''; } echo ' | '; echo '' . esc_html( (int) $c['incidents'] ) . ' | '; echo '' . esc_html( (int) $c['occurrences'] ) . ' | '; echo '' . Culprit_Settings::format_datetime( $c['last_seen'] ) . ' | '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- format_datetime escapes. echo '
' . esc_html__( 'You are running the free version of Culprit. Everything below is already built and waiting in Premium and Elite.', 'culprit' ) . '
'; Culprit_UI::upgrade_box(); echo ''; echo esc_html( $health['message'] ); echo '
'; echo '' . esc_html__( 'Culprit always catches fatal errors that happen after plugins load, with no extra setup. Optionally, it can also install a tiny must-use loader (wp-content/mu-plugins/culprit-watcher.php) that runs before normal plugins, so crashes during earliest boot are captured too. Nothing is installed without your explicit choice here, and disabling removes the file immediately. It is also removed when Culprit is deactivated or uninstalled.', 'culprit' ) . '
'; if ( $health['enabled'] ) { $disable_url = wp_nonce_url( admin_url( 'admin-post.php?action=culprit_watcher_disable' ), 'culprit_watcher_disable' ); echo '' . esc_html__( 'Disable early-boot watcher', 'culprit' ) . '
'; } else { $enable_url = wp_nonce_url( admin_url( 'admin-post.php?action=culprit_watcher_enable' ), 'culprit_watcher_enable' ); echo '' . esc_html__( 'Enable early-boot watcher', 'culprit' ) . '
'; } echo '| ' . esc_html__( 'Early-boot watcher enabled', 'culprit' ) . ' | ' . ( $health['enabled'] ? esc_html__( 'Yes', 'culprit' ) : esc_html__( 'No', 'culprit' ) ) . ' |
|---|---|
| ' . esc_html__( 'Watcher drop-in installed', 'culprit' ) . ' | ' . ( $health['active'] ? esc_html__( 'Yes', 'culprit' ) : esc_html__( 'No', 'culprit' ) ) . ' |
| ' . esc_html__( 'Standard crash handler', 'culprit' ) . ' | ' . ( $health['active'] ? esc_html__( 'Standby (watcher covers it)', 'culprit' ) : esc_html__( 'Active', 'culprit' ) ) . ' |
| ' . esc_html__( 'mu-plugins writable', 'culprit' ) . ' | ' . ( $health['writable'] ? esc_html__( 'Yes', 'culprit' ) : esc_html__( 'No', 'culprit' ) ) . ' |
| ' . esc_html__( 'PHP version', 'culprit' ) . ' | ' . esc_html( PHP_VERSION ) . ' |