Agentic SEO. * * Two states: * - Not paired: shows a single field to paste the pairing code. * - Paired: shows status + a Disconnect button. * * Status-redirect query args are nonce-protected so an attacker can't * craft `?visibility_status=error&visibility_msg=` and * inject an arbitrary admin notice by tricking the user into clicking * a link. The nonce is minted at redirect time and verified on render. */ if (!defined('ABSPATH')) { exit; } class Visibility_Settings { public function __construct() { add_action('admin_menu', [$this, 'register_menu']); add_action('admin_post_visibility_pair', [$this, 'handle_pair']); add_action('admin_post_visibility_disconnect', [$this, 'handle_disconnect']); } /** Build the nonce-protected query args for a redirect-after-action. * $message is optional and only attached on the error path. */ private function get_notice_query_args($status, $message = '') { $args = [ 'page' => 'visibility', 'visibility_status' => $status, 'visibility_notice_nonce' => wp_create_nonce('visibility_notice'), ]; if ($message !== '') { $args['visibility_msg'] = rawurlencode($message); } return $args; } /** Read the (nonce-verified) status + message from the current request. * Returns empty strings when no nonce is present or it doesn't verify — * silently ignoring spoofed notice URLs. */ private function get_notice_state() { $notice_nonce = isset($_GET['visibility_notice_nonce']) ? sanitize_text_field(wp_unslash($_GET['visibility_notice_nonce'])) : ''; if ($notice_nonce === '' || !wp_verify_nonce($notice_nonce, 'visibility_notice')) { return [ 'status' => '', 'error_msg' => '', ]; } return [ 'status' => isset($_GET['visibility_status']) ? sanitize_key(wp_unslash($_GET['visibility_status'])) : '', 'error_msg' => isset($_GET['visibility_msg']) ? sanitize_text_field(wp_unslash($_GET['visibility_msg'])) : '', ]; } public function register_menu() { // Dedicated top-level menu so all three plugin screens live together // under one clearly-labelled parent (instead of three lookalike // entries buried under Settings). The Approvals + Activity submenus // are added by Visibility_Requests against this same `visibility` // parent slug. add_menu_page( __('Agentic SEO for Visibility', 'agentic-seo-visibility'), __('Agentic SEO', 'agentic-seo-visibility'), 'manage_options', 'visibility', [$this, 'render'], 'dashicons-admin-site-alt3', 58 ); // First submenu shares the parent slug so the parent click lands on // the connection screen, and we get a readable "Connection" label // instead of WP duplicating the parent title. add_submenu_page( 'visibility', __('Agentic SEO — Connection', 'agentic-seo-visibility'), __('Connection', 'agentic-seo-visibility'), 'manage_options', 'visibility', [$this, 'render'] ); } public function handle_pair() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'agentic-seo-visibility')); } check_admin_referer('visibility_pair'); $code = isset($_POST['visibility_pairing_code']) ? sanitize_text_field(wp_unslash($_POST['visibility_pairing_code'])) : ''; $result = Visibility_Client::pair($code); if (is_wp_error($result)) { $message = $result->get_error_message(); wp_safe_redirect(add_query_arg($this->get_notice_query_args('error', $message), admin_url('admin.php'))); exit; } wp_safe_redirect(add_query_arg($this->get_notice_query_args('paired'), admin_url('admin.php'))); exit; } public function handle_disconnect() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'agentic-seo-visibility')); } check_admin_referer('visibility_disconnect'); Visibility_Client::disconnect(); wp_safe_redirect(add_query_arg($this->get_notice_query_args('disconnected'), admin_url('admin.php'))); exit; } public function render() { if (!current_user_can('manage_options')) { return; } $paired = Visibility_Client::is_paired(); $project_id = get_option('visibility_project_id', ''); $project_name = get_option('visibility_project_name', ''); $company_name = get_option('visibility_company_name', ''); $paired_at = (int) get_option('visibility_paired_at', 0); $last_seen_at = (int) get_option('visibility_last_seen_at', 0); $notice_data = $this->get_notice_state(); $status = $notice_data['status']; $error_msg = $notice_data['error_msg']; $action_url = esc_url(admin_url('admin-post.php')); $dashboard = esc_url(trailingslashit(visibility_api_base_url())); $logo_url = esc_url(plugins_url('assets/icon.svg', VISIBILITY_PLUGIN_FILE)); ?>


  1. your Visibility project and click Connect via plugin to generate a code.', 'agentic-seo-visibility'), $dashboard ), ['a' => ['href' => [], 'target' => [], 'rel' => []], 'strong' => []] ); ?>