settings = $settings;
$this->queue = $queue;
$this->sender = $sender;
$this->inbox = $inbox;
add_action( 'admin_menu', array( $this, 'add_menu' ) );
add_action( 'admin_init', array( $this, 'register_settings' ) );
add_action( 'admin_init', array( $this, 'register_privacy_policy_content' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_action( 'admin_notices', array( $this, 'display_notices' ) );
// Settings AJAX handlers.
add_action( 'wp_ajax_aiadvisors_send_test', array( $this, 'ajax_send_test' ) );
add_action( 'wp_ajax_aiadvisors_flush_queue', array( $this, 'ajax_flush_queue' ) );
add_action( 'wp_ajax_aiadvisors_clear_queue', array( $this, 'ajax_clear_queue' ) );
add_action( 'wp_ajax_aiadvisors_get_status', array( $this, 'ajax_get_status' ) );
// Inbox AJAX handlers.
add_action( 'wp_ajax_aiadvisors_apply_action', array( $this, 'ajax_apply_action' ) );
add_action( 'wp_ajax_aiadvisors_dismiss_action', array( $this, 'ajax_dismiss_action' ) );
add_action( 'wp_ajax_aiadvisors_undo_action', array( $this, 'ajax_undo_action' ) );
add_action( 'wp_ajax_aiadvisors_refresh_inbox', array( $this, 'ajax_refresh_inbox' ) );
add_action( 'wp_ajax_aiadvisors_local_apply', array( $this, 'ajax_local_apply' ) );
}
public function add_menu() {
// Top-level: Inbox (default landing page). Use the cached count only —
// never trigger a blocking remote request during menu rendering.
$pending_count = $this->inbox ? $this->inbox->get_cached_pending_count() : 0;
$badge = $pending_count > 0
? ' ' . $pending_count . ' '
: '';
$this->inbox_hook = add_menu_page(
__( 'AI-Advisors', 'ai-advisors' ),
__( 'AI-Advisors', 'ai-advisors' ) . $badge,
'manage_options',
$this->menu_slug,
array( $this, 'render_inbox' ),
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Inline SVG data-URI for the menu icon, not obfuscation.
'data:image/svg+xml;base64,' . base64_encode( ' ' ),
80
);
// Submenu: Inbox (renames the auto-created first submenu item).
add_submenu_page(
$this->menu_slug,
__( 'Inbox', 'ai-advisors' ),
__( 'Inbox', 'ai-advisors' ) . $badge,
'manage_options',
$this->menu_slug,
array( $this, 'render_inbox' )
);
// Submenu: Settings. Capture the real hook suffix — it embeds the parent
// menu's sanitized title, which changes when the inbox badge is present.
$this->settings_hook = add_submenu_page(
$this->menu_slug,
__( 'Settings', 'ai-advisors' ),
__( 'Settings', 'ai-advisors' ),
'manage_options',
$this->settings_slug,
array( $this, 'render_settings' )
);
}
public function register_settings() {
register_setting(
'aiadvisors_settings_group',
'aiadvisors_settings',
array( $this, 'sanitize_settings' )
);
}
/**
* Add suggested privacy-policy text to Tools > Privacy.
*/
public function register_privacy_policy_content() {
if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) {
return;
}
$content = wp_kses_post(
'
' . __( 'When AI-Advisors tracking is enabled, this site detects visits from AI bots and AI-referral traffic and sends the following to the AI-Advisors platform (app.ai-advisors.ai): the request URL path, user agent, referrer, IP address, HTTP status code, response size, request timing, and any UTM parameters. Regular human traffic is not tracked. No cookies are used.', 'ai-advisors' ) . '
'
);
wp_add_privacy_policy_content( 'AI-Advisors', $content );
}
public function enqueue_assets( $hook ) {
// Compare against the hook suffixes WordPress actually assigned at
// registration (see add_menu()). Reconstructing them by hand breaks when
// the inbox badge alters the menu title the Settings hook derives from.
if ( $hook !== $this->inbox_hook && $hook !== $this->settings_hook ) {
return;
}
wp_enqueue_style(
'aiadvisors-admin',
AIADVISORS_URL . 'assets/css/admin.css',
array(),
AIADVISORS_VERSION
);
wp_enqueue_script(
'aiadvisors-admin',
AIADVISORS_URL . 'assets/js/admin.js',
array( 'jquery' ),
AIADVISORS_VERSION,
true
);
wp_localize_script(
'aiadvisors-admin',
'aiadvisors',
array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'aiadvisors_ajax' ),
'strings' => array(
'confirm_flush' => __( 'Send all queued entries now?', 'ai-advisors' ),
'confirm_clear' => __( 'Delete all queued entries? This cannot be undone.', 'ai-advisors' ),
'confirm_apply' => __( 'Apply this fix to your site?', 'ai-advisors' ),
'confirm_dismiss' => __( 'Dismiss this recommendation?', 'ai-advisors' ),
'confirm_undo' => __( 'Undo this action?', 'ai-advisors' ),
'sending_test' => __( 'Sending test event...', 'ai-advisors' ),
'flushing' => __( 'Sending queued entries...', 'ai-advisors' ),
'applying' => __( 'Applying...', 'ai-advisors' ),
),
)
);
}
public function display_notices() {
$errors = Helpers::check_requirements();
foreach ( $errors as $error ) {
echo '' . esc_html( $error ) . '
';
}
if ( ! function_exists( 'sodium_crypto_secretbox' ) && $this->settings->get( 'domain_key' ) ) {
echo '';
esc_html_e( 'Sodium PHP extension not available. Domain key is stored without encryption.', 'ai-advisors' );
echo '
';
}
// Show a notice when there are pending inbox items (on non-inbox pages).
$screen = get_current_screen();
if ( $this->inbox && $screen && $screen->id !== 'toplevel_page_' . $this->menu_slug ) {
// Cached count only — avoid a blocking remote call on every admin page.
$count = $this->inbox->get_cached_pending_count();
if ( $count > 0 ) {
$inbox_url = admin_url( 'admin.php?page=' . $this->menu_slug );
echo '';
printf(
esc_html(
/* translators: 1: number of items, 2: opening link tag, 3: closing link tag */
_n(
'AI-Advisors: %1$s recommendation ready to review. %2$sView Inbox%3$s',
'AI-Advisors: %1$s recommendations ready to review. %2$sView Inbox%3$s',
$count,
'ai-advisors'
)
),
'' . esc_html( $count ) . ' ',
'',
' '
);
echo '
';
}
}
}
// --- Page Renderers ---
public function render_inbox() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$actions = $this->inbox ? $this->inbox->fetch_actions() : array();
$history = $this->inbox ? $this->inbox->get_recent_history( 10 ) : array();
include AIADVISORS_DIR . 'admin/views/inbox.php';
}
public function render_settings() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$active_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general';
$status = $this->sender->get_status();
$settings = $this->settings->get();
include AIADVISORS_DIR . 'admin/views/settings.php';
}
public function sanitize_settings( $input ) {
$sanitized = $this->settings->get();
$sanitized['enabled'] = ! empty( $input['enabled'] );
if ( ! empty( $input['disconnect'] ) ) {
// Explicit disconnect clears the stored key and stops tracking. An
// empty key field can't mean "clear" — it's the normal state when
// saving other settings — so clearing needs its own control.
$sanitized['domain_key'] = '';
$sanitized['enabled'] = false;
} elseif ( ! empty( $input['domain_key'] ) ) {
$new_key = sanitize_text_field( wp_unslash( $input['domain_key'] ) );
if ( ! empty( $new_key ) && strpos( $new_key, '*' ) === false ) {
$sanitized['domain_key'] = $this->settings->encrypt_value( $new_key );
}
}
if ( isset( $input['endpoint'] ) ) {
$endpoint = esc_url_raw( wp_unslash( $input['endpoint'] ) );
// Only accept https endpoints; fall back to the default otherwise so
// the plugin can't be pointed at an http or malformed fix source.
$sanitized['endpoint'] = ( $endpoint && wp_parse_url( $endpoint, PHP_URL_SCHEME ) === 'https' )
? $endpoint
: AIADVISORS_API_ENDPOINT;
}
if ( isset( $input['redact_query_keys'] ) ) {
$sanitized['redact_query_keys'] = sanitize_text_field( wp_unslash( $input['redact_query_keys'] ) );
}
return $sanitized;
}
// --- Settings AJAX Handlers ---
public function ajax_send_test() {
check_ajax_referer( 'aiadvisors_ajax', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die();
}
wp_send_json( $this->sender->send_test_event() );
}
public function ajax_flush_queue() {
check_ajax_referer( 'aiadvisors_ajax', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die();
}
$processed = $this->sender->process_queue();
wp_send_json(
array(
'success' => $processed,
'message' => $processed
? __( 'Queue processed successfully.', 'ai-advisors' )
: __( 'Failed to process queue.', 'ai-advisors' ),
)
);
}
public function ajax_clear_queue() {
check_ajax_referer( 'aiadvisors_ajax', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die();
}
$this->queue->clear_all();
wp_send_json(
array(
'success' => true,
'message' => __( 'Queue cleared.', 'ai-advisors' ),
)
);
}
public function ajax_get_status() {
check_ajax_referer( 'aiadvisors_ajax', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die();
}
wp_send_json( $this->sender->get_status() );
}
// --- Inbox AJAX Handlers ---
public function ajax_apply_action() {
check_ajax_referer( 'aiadvisors_ajax', 'nonce' );
if ( ! current_user_can( 'manage_options' ) || ! $this->inbox ) {
wp_die();
}
$action_id = isset( $_POST['action_id'] ) ? sanitize_text_field( wp_unslash( $_POST['action_id'] ) ) : '';
if ( empty( $action_id ) ) {
wp_send_json(
array(
'success' => false,
'message' => 'Missing action_id.',
)
);
}
// Pass action data for local storage.
$action_data = array(
'type' => isset( $_POST['action_type'] ) ? sanitize_text_field( wp_unslash( $_POST['action_type'] ) ) : '',
'title' => isset( $_POST['action_title'] ) ? sanitize_text_field( wp_unslash( $_POST['action_title'] ) ) : '',
);
wp_send_json( $this->inbox->apply_action( $action_id, $action_data ) );
}
public function ajax_dismiss_action() {
check_ajax_referer( 'aiadvisors_ajax', 'nonce' );
if ( ! current_user_can( 'manage_options' ) || ! $this->inbox ) {
wp_die();
}
$action_id = isset( $_POST['action_id'] ) ? sanitize_text_field( wp_unslash( $_POST['action_id'] ) ) : '';
if ( empty( $action_id ) ) {
wp_send_json(
array(
'success' => false,
'message' => 'Missing action_id.',
)
);
}
$action_data = array(
'type' => isset( $_POST['action_type'] ) ? sanitize_text_field( wp_unslash( $_POST['action_type'] ) ) : '',
'title' => isset( $_POST['action_title'] ) ? sanitize_text_field( wp_unslash( $_POST['action_title'] ) ) : '',
);
wp_send_json( $this->inbox->dismiss_action( $action_id, $action_data ) );
}
public function ajax_undo_action() {
check_ajax_referer( 'aiadvisors_ajax', 'nonce' );
if ( ! current_user_can( 'manage_options' ) || ! $this->inbox ) {
wp_die();
}
$action_id = isset( $_POST['action_id'] ) ? sanitize_text_field( wp_unslash( $_POST['action_id'] ) ) : '';
if ( empty( $action_id ) ) {
wp_send_json(
array(
'success' => false,
'message' => 'Missing action_id.',
)
);
}
wp_send_json( $this->inbox->undo_action( $action_id ) );
}
public function ajax_refresh_inbox() {
check_ajax_referer( 'aiadvisors_ajax', 'nonce' );
if ( ! current_user_can( 'manage_options' ) || ! $this->inbox ) {
wp_die();
}
$this->inbox->clear_cache();
$actions = $this->inbox->fetch_actions( true );
$history = $this->inbox->get_recent_history( 10 );
wp_send_json(
array(
'success' => true,
'pending_count' => count( $actions ),
'history_count' => count( $history ),
)
);
}
/**
* Apply a free, local-only tool (robots rules, llms.txt, Org schema)
* generated from site data — no platform account required.
*/
public function ajax_local_apply() {
check_ajax_referer( 'aiadvisors_ajax', 'nonce' );
if ( ! current_user_can( 'manage_options' ) || ! $this->inbox ) {
wp_die();
}
$tool = isset( $_POST['tool'] ) ? sanitize_key( wp_unslash( $_POST['tool'] ) ) : '';
if ( empty( $tool ) ) {
wp_send_json(
array(
'success' => false,
'message' => 'Missing tool.',
)
);
}
wp_send_json( $this->inbox->apply_local_tool( $tool ) );
}
}