scheduler = $scheduler ?? new ABDS_SEO_Audit_Scheduler(); } /** * Registers this class's action hooks with the plugin's hook loader. * * @param ABDS_SEO_Loader $loader Hook loader. */ public function register_hooks( ABDS_SEO_Loader $loader ): void { $loader->add_action( 'admin_bar_menu', $this, 'maybe_add_admin_bar_node', 100 ); $loader->add_action( 'wp_enqueue_scripts', $this, 'maybe_enqueue_highlight_assets' ); } /** * Adds the "SEO issues: N" admin-bar node — only when there's something * real to show: a singular page, a user who can see it, and a latest * completed run that found at least one issue for this exact object. * * @param WP_Admin_Bar $admin_bar Core admin bar instance. */ public function maybe_add_admin_bar_node( $admin_bar ): void { if ( is_admin() || ! is_singular() ) { return; } $post_id = get_queried_object_id(); if ( ! $post_id || ! $this->can_see_audit_for( $post_id ) ) { return; } $count = count( $this->current_page_issues( $post_id ) ); if ( 0 === $count ) { return; } $admin_bar->add_node( array( 'id' => 'abds-seo-highlight-toggle', 'title' => sprintf( /* translators: %d: number of SEO issues found on this page. */ __( 'SEO issues: %d', 'abds-seo' ), $count ), 'href' => $this->toggle_url(), ) ); } /** * The scaffold's own enqueue point — M3a's highlight-overlay class hooks * the SAME abds_seo_highlight_mode_active action this fires (rather than * re-deriving its own capability/nonce/singular checks), so there is * exactly one place that decides "is this request allowed to see * highlight mode," not one per consumer. */ public function maybe_enqueue_highlight_assets(): void { if ( is_admin() || ! is_singular() ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- the nonce itself IS what's being verified two lines down; this isn't a state-changing request, just a read-only mode toggle. if ( ! isset( $_GET[ self::QUERY_VAR ] ) ) { return; } $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- verified explicitly on the next line. if ( ! wp_verify_nonce( $nonce, self::NONCE_ACTION ) ) { return; } $post_id = get_queried_object_id(); if ( ! $post_id || ! $this->can_see_audit_for( $post_id ) ) { return; } /** * Fires once every gate (singular, nonce, capability, toggle param) * has passed for this exact request — the single point M3a's (and * Pro's M3b's) own asset-enqueue hooks into, so neither has to * re-implement this class's own access checks. * * @param int $post_id The current singular post's ID. */ do_action( 'abds_seo_highlight_mode_active', $post_id ); } /** * Whether the current user is allowed to see audit data for $post_id on * the frontend: both the general audit capability (consistent with the * Audit screen's own REST routes) AND the ability to edit this specific * post (so the "Edit" action the overlay will offer actually works for * them) — the narrower of the two is what decides visibility. * * @param int $post_id Post ID. * @return bool */ protected function can_see_audit_for( int $post_id ): bool { return current_user_can( ABDS_SEO_Activator::AUDIT_CAPABILITY ) && current_user_can( 'edit_post', $post_id ); } /** * This post's own issues from the latest completed audit run, or an * empty array if no run has ever completed. * * @param int $post_id Post ID. * @return array */ protected function current_page_issues( int $post_id ): array { $run_id = $this->scheduler->get_latest_completed_run_id(); if ( ! $run_id ) { return array(); } return $this->scheduler->get_object_issues( $run_id, 'post', $post_id ); } /** * The nonce-signed URL the admin-bar node links to: the current page, * plus the highlight query var. * * @return string */ protected function toggle_url(): string { $url = add_query_arg( self::QUERY_VAR, '1', get_permalink() ); return wp_nonce_url( $url, self::NONCE_ACTION ); } }