connector = $connector; add_action( 'admin_menu', array( $this, 'register_menus' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); add_action( 'admin_notices', array( $this, 'maybe_notice' ) ); add_filter( 'plugin_action_links_' . plugin_basename( AIVISY_FILE ), array( $this, 'filter_plugin_action_links' ), 10, 2 ); } /** * Prepends Settings on plugins.php (to the left of Deactivate). * * @param array $links Existing plugin action links. * @param string $file Plugin file relative to plugins directory. * @return array */ public function filter_plugin_action_links( $links, $file ) { if ( plugin_basename( AIVISY_FILE ) !== $file ) { return $links; } $settings = sprintf( '%s', esc_url( admin_url( 'admin.php?page=aivisy-settings' ) ), esc_html__( 'Settings', 'ai-view-sync' ) ); return array_merge( array( 'settings' => $settings ), $links ); } public function register_menus() { add_menu_page( __( 'AI View Sync', 'ai-view-sync' ), __( 'AI View Sync', 'ai-view-sync' ), 'manage_options', 'aivisy-settings', array( $this, 'render_settings_page' ), 'dashicons-text-page', 81 ); // Slug must match what gcco.io validates in the OAuth callback URL. add_submenu_page( null, __( 'AI View connection callback', 'ai-view-sync' ), '', 'manage_options', 'ai-view-llms-txt', '__return_null' ); } /** * @param string $hook_suffix Current screen. */ public function enqueue_assets( $hook_suffix ) { if ( false === strpos( $hook_suffix, 'aivisy' ) ) { return; } wp_enqueue_style( 'aivisy-admin', AIVISY_URL . 'assets/admin.css', array(), AIVISY_VERSION ); wp_enqueue_style( 'dashicons' ); wp_enqueue_script( 'aivisy-admin', AIVISY_URL . 'assets/admin.js', array(), AIVISY_VERSION, true ); } /** * One-time admin notices from query args. Requires nonce (_aivisy_n) so crafted URLs cannot * trick admins into seeing misleading notices. */ public function maybe_notice() { if ( ! isset( $_GET['aivisy_notice'] ) || ! current_user_can( 'manage_options' ) ) { return; } // Verify nonce produced by aivisy_make_notice_url(). $nonce = isset( $_GET['_aivisy_n'] ) ? sanitize_text_field( wp_unslash( $_GET['_aivisy_n'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'aivisy_notice' ) ) { return; } $code = sanitize_key( wp_unslash( $_GET['aivisy_notice'] ) ); $msg = ''; $type = 'success'; switch ( $code ) { case 'saved': $s = Settings::get_all(); if ( empty( $s['connected'] ) ) { // Inline confirmation on pre-connection manual form. return; } $msg = __( 'Manual content saved. LLMS.txt is updated when manual mode is active.', 'ai-view-sync' ); break; case 'connected': $msg = __( 'Connected to AI View. Your account is linked to this WordPress site.', 'ai-view-sync' ); break; case 'llms_ready': // Success copy is shown in the settings hero; avoid a duplicate admin notice. return; case 'choose_property': $type = 'success'; $msg = __( 'Connected. Next: choose which GA4 property should supply LLMS.txt for this site (see below).', 'ai-view-sync' ); break; case 'property_bound': $msg = __( 'GA4 property saved. Use Sync now to pull LLMS.txt, or open the generator in AI View if you have not created a file yet.', 'ai-view-sync' ); break; case 'property_bind_failed': $type = 'error'; $msg = __( 'Could not save the property link. Try again or check your connection.', 'ai-view-sync' ); break; case 'connect_error': $type = 'error'; $msg = __( 'Connection could not be verified. Please try again.', 'ai-view-sync' ); break; case 'connect_config': $type = 'warning'; $msg = __( 'Connect URL is not available. A theme or plugin may have removed it—check AIVISY_APP_URL / aivisy_connect_base_url.', 'ai-view-sync' ); break; case 'disconnected': $msg = __( 'Disconnected from AI View.', 'ai-view-sync' ); break; case 'synced': $msg = __( 'Sync completed.', 'ai-view-sync' ); break; case 'sync_failed': $type = 'error'; $msg = __( 'Sync failed. Ensure you are connected.', 'ai-view-sync' ); break; case 'mode': $msg = __( 'Active mode updated.', 'ai-view-sync' ); break; case 'root_file_on': $msg = __( 'Physical llms.txt written to the site root. It will be refreshed automatically on every sync/save.', 'ai-view-sync' ); break; case 'root_file_on_pending': $type = 'warning'; $msg = __( 'Setting saved, but the root llms.txt was not written yet. Either there is no content to publish (sync or save content first), a llms.txt not created by this plugin already exists at the site root, or the site root is not writable.', 'ai-view-sync' ); break; case 'root_file_off': $msg = __( 'Physical root llms.txt disabled. Any file created by this plugin at the site root was removed.', 'ai-view-sync' ); break; case 'exclusions_saved': // Inline confirmation on the settings screen (see exclusions form). return; case 'wp_only_merged': // Inline confirmation on pre-connection WordPress merge form. return; case 'merge_wp_connected': $type = 'error'; $msg = __( 'That action is only available when you are not connected to GcCo AI View. Use Sync now to pull GA4-ranked content.', 'ai-view-sync' ); break; default: return; } printf( '

%2$s

', esc_attr( $type ), esc_html( $msg ) ); } /** * Save manual LLMS.txt from POST. */ public function handle_save_manual() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Forbidden.', 'ai-view-sync' ) ); } check_admin_referer( 'aivisy_save_manual' ); $raw = isset( $_POST['aivisy_manual_content'] ) ? sanitize_textarea_field( wp_unslash( $_POST['aivisy_manual_content'] ) ) : ''; $body = Settings::sanitize_llms_body( $raw ); Settings::update( array( 'manual_content' => $body ) ); wp_safe_redirect( aivisy_make_notice_url( 'saved' ) ); exit; } /** * Append published WordPress URLs to manual LLMS.txt (requires not connected). */ public function handle_merge_wordpress_only() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Forbidden.', 'ai-view-sync' ) ); } check_admin_referer( 'aivisy_merge_wordpress' ); $s = Settings::get_all(); if ( ! empty( $s['connected'] ) ) { wp_safe_redirect( aivisy_make_notice_url( 'merge_wp_connected' ) ); exit; } $base = trim( (string) $s['manual_content'] ); if ( '' === $base ) { $base = Llms_Merge::default_wordpress_only_base(); } $merged = Llms_Merge::merge( $base, $s ); $body = Settings::sanitize_llms_body( $merged['content'] ); Llms_Merge::maybe_write_physical_file( $body ); Settings::update( array( 'manual_content' => $body, 'mode' => 'manual', 'last_synced' => time(), 'last_sync_ga4_count' => (int) $merged['ga4_count'], 'last_sync_wp_count' => (int) $merged['wp_count'], ) ); wp_safe_redirect( aivisy_make_notice_url( 'wp_only_merged' ) ); exit; } /** * Set active mode manual | connected. */ public function handle_set_mode() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Forbidden.', 'ai-view-sync' ) ); } check_admin_referer( 'aivisy_set_mode' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $mode = isset( $_POST['aivisy_mode'] ) ? sanitize_key( wp_unslash( $_POST['aivisy_mode'] ) ) : 'manual'; if ( ! in_array( $mode, array( 'manual', 'connected' ), true ) ) { $mode = 'manual'; } if ( 'connected' === $mode ) { $s = Settings::get_all(); if ( empty( $s['connected'] ) || Settings::needs_property_selection() ) { $mode = 'manual'; } } Settings::update( array( 'mode' => $mode ) ); wp_safe_redirect( aivisy_make_notice_url( 'mode' ) ); exit; } /** * Disconnect from AI View. */ public function handle_disconnect() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Forbidden.', 'ai-view-sync' ) ); } check_admin_referer( 'aivisy_disconnect' ); $this->connector->disconnect( true ); wp_safe_redirect( aivisy_make_notice_url( 'disconnected' ) ); exit; } /** * Sync now button. */ public function handle_sync() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Forbidden.', 'ai-view-sync' ) ); } check_admin_referer( 'aivisy_sync' ); $ok = $this->connector->sync_now(); if ( $ok ) { wp_safe_redirect( aivisy_make_notice_url( 'synced' ) ); } else { $detail = get_transient( 'aivisy_sync_error' ); // When sync_now stored a specific reason in a transient, redirect without a notice query arg // so the transient-driven error renders on page load (avoids a duplicate generic notice). if ( ! is_string( $detail ) || '' === $detail ) { wp_safe_redirect( aivisy_make_notice_url( 'sync_failed' ) ); } else { wp_safe_redirect( admin_url( 'admin.php?page=aivisy-settings' ) ); } } exit; } /** * Save exclusion patterns and post types (connected flow). */ public function handle_save_exclusions() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Forbidden.', 'ai-view-sync' ) ); } check_admin_referer( 'aivisy_save_exclusions' ); $raw_patterns = isset( $_POST['aivisy_exclude_path_patterns'] ) ? sanitize_textarea_field( wp_unslash( $_POST['aivisy_exclude_path_patterns'] ) ) : ''; $patterns = Settings::sanitize_llms_body( $raw_patterns ); $exclude_types = array(); if ( isset( $_POST['aivisy_exclude_post_types'] ) && is_array( $_POST['aivisy_exclude_post_types'] ) ) { $exclude_types = array_values( array_unique( array_filter( array_map( 'sanitize_key', wp_unslash( $_POST['aivisy_exclude_post_types'] ) ), static function ( $t ) { return '' !== $t; } ) ) ); } Settings::update( array( 'exclude_path_patterns' => $patterns, 'exclude_post_types' => array_values( array_unique( $exclude_types ) ), ) ); wp_safe_redirect( aivisy_make_notice_url( 'exclusions_saved' ) ); exit; } /** * Toggle the physical root llms.txt copy (hosts that serve .txt statically, e.g. WP Engine). */ public function handle_save_root_file() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Forbidden.', 'ai-view-sync' ) ); } check_admin_referer( 'aivisy_save_root_file' ); $enable = ! empty( $_POST['aivisy_write_root_file'] ); Settings::update( array( 'write_root_file' => $enable ) ); if ( $enable ) { $content = Settings::get_active_content(); $written = '' !== trim( (string) $content ) ? Llms_Merge::maybe_write_root_file( $content ) : false; wp_safe_redirect( aivisy_make_notice_url( $written ? 'root_file_on' : 'root_file_on_pending' ) ); } else { Llms_Merge::delete_root_file(); wp_safe_redirect( aivisy_make_notice_url( 'root_file_off' ) ); } exit; } /** * Save GA4 property for this WordPress site (after connect). */ public function handle_bind_property() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Forbidden.', 'ai-view-sync' ) ); } check_admin_referer( 'aivisy_bind_property' ); $fail_url = aivisy_make_notice_url( 'property_bind_failed' ); try { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $raw_pid = isset( $_POST['aivisy_property_id'] ) ? wp_unslash( $_POST['aivisy_property_id'] ) : ''; $pid = is_string( $raw_pid ) ? sanitize_text_field( $raw_pid ) : ''; if ( '' === $pid ) { wp_safe_redirect( $fail_url ); exit; } $s = Settings::get_all(); if ( empty( $s['connected'] ) || empty( $s['connection_token'] ) ) { wp_safe_redirect( $fail_url ); exit; } $bind = Remote_API::bind_property( $pid, $s ); if ( is_wp_error( $bind ) ) { set_transient( 'aivisy_sync_error', $bind->get_error_message(), 120 ); wp_safe_redirect( $fail_url ); exit; } Settings::update( array( 'bound_property_id' => $pid, 'mode' => 'connected', ) ); delete_transient( Connector::TRANSIENT_SUGGESTED_PROPERTY ); delete_transient( Connector::TRANSIENT_SUGGESTED_LABEL ); // Pull LLMS.txt immediately so the user does not have to click "Sync now". $sync_ok = $this->connector->sync_now(); if ( $sync_ok ) { wp_safe_redirect( aivisy_make_notice_url( 'synced' ) ); } else { wp_safe_redirect( aivisy_make_notice_url( 'property_bound' ) ); } exit; } catch ( \Throwable $e ) { if ( function_exists( 'error_log' ) ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'aivisy: bind_property failed: ' . $e->getMessage() ); } set_transient( 'aivisy_sync_error', __( 'Could not save the property link. Please try again.', 'ai-view-sync' ), 120 ); wp_safe_redirect( $fail_url ); exit; } } /** * Main settings screen. */ public function render_settings_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } $s = Settings::get_all(); $public_url = Settings::get_public_llms_url(); $active_content = Settings::get_active_content(); $connect_url = $this->connector->get_connect_base_url(); $connect_href = wp_nonce_url( add_query_arg( 'aivisy_start_connect', '1', admin_url( 'admin.php?page=aivisy-settings' ) ), 'aivisy_connect' ); $last_synced = ''; if ( ! empty( $s['last_synced'] ) ) { $last_synced = wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), (int) $s['last_synced'] ); } else { $last_synced = __( 'Never', 'ai-view-sync' ); } $status_connected = ! empty( $s['connected'] ); $needs_prop = Settings::needs_property_selection(); $generator_url = Settings::get_generator_url_with_wp_return(); $generator_url_base = Settings::get_generator_url(); $regenerate_href = '' !== $generator_url ? $generator_url : $generator_url_base; $dashboard_url = Settings::get_dashboard_url(); // Prefer configured app dashboard; fallback for display link only. $ai_view_dashboard_href = '' !== $dashboard_url ? $dashboard_url : 'https://gcco.io/ai-dashboard/login.php'; $properties_fetch = null; $properties_list = array(); $suggested_pid = get_transient( Connector::TRANSIENT_SUGGESTED_PROPERTY ); $suggested_pid = is_string( $suggested_pid ) ? trim( $suggested_pid ) : ''; if ( $status_connected && $needs_prop ) { $properties_fetch = Remote_API::fetch_properties( $s ); if ( ! is_wp_error( $properties_fetch ) ) { $properties_list = $properties_fetch; } } $sync_err = get_transient( 'aivisy_sync_error' ); if ( is_string( $sync_err ) && $sync_err !== '' ) { delete_transient( 'aivisy_sync_error' ); } else { $sync_err = ''; } $connected_ready = $status_connected && ! $needs_prop; $prop_display_name = ! empty( $s['bound_property_label'] ) ? (string) $s['bound_property_label'] : ''; if ( '' === $prop_display_name && ! empty( $s['bound_property_id'] ) ) { $prop_display_name = (string) $s['bound_property_id']; } $manual_trimmed = trim( (string) $s['manual_content'] ); $has_manual_saved = '' !== $manual_trimmed; $has_usable_source = $status_connected || $has_manual_saved; $show_status_panel = $has_usable_source; $show_mode_switch = $status_connected && $has_manual_saved; $active_trimmed = trim( (string) $active_content ); $show_preview = $has_usable_source && '' !== $active_trimmed; if ( 'connected' === $s['mode'] && $status_connected ) { $live_source_label = __( 'AI View', 'ai-view-sync' ); } else { $live_source_label = __( 'Manual', 'ai-view-sync' ); } $sync_ga4_n = isset( $s['last_sync_ga4_count'] ) ? (int) $s['last_sync_ga4_count'] : 0; $sync_wp_n = isset( $s['last_sync_wp_count'] ) ? (int) $s['last_sync_wp_count'] : 0; $exclude_patterns_text = isset( $s['exclude_path_patterns'] ) ? (string) $s['exclude_path_patterns'] : ''; $exclude_post_types = isset( $s['exclude_post_types'] ) && is_array( $s['exclude_post_types'] ) ? $s['exclude_post_types'] : array(); $public_post_types = get_post_types( array( 'public' => true, ), 'objects' ); unset( $public_post_types['attachment'] ); // Verify nonce before trusting the notice query arg (same nonce produced by aivisy_make_notice_url()). $_aivisy_nonce_raw = isset( $_GET['_aivisy_n'] ) ? sanitize_text_field( wp_unslash( $_GET['_aivisy_n'] ) ) : ''; $notice_query = ( wp_verify_nonce( $_aivisy_nonce_raw, 'aivisy_notice' ) && isset( $_GET['aivisy_notice'] ) ) ? sanitize_key( wp_unslash( $_GET['aivisy_notice'] ) ) : ''; $exclusions_inline_saved = $connected_ready && 'exclusions_saved' === $notice_query; $manual_inline_saved = ! $status_connected && 'saved' === $notice_query; $wp_merge_inline_saved = ! $status_connected && 'wp_only_merged' === $notice_query; ?>


'ai-view-llms-disconnect-success', 'class' => 'button button-link ai-view-llms-disconnect-btn', ) ); ?>

name ) ) { continue; } $slug = (string) $pt_obj->name; $lab = isset( $pt_obj->labels->singular_name ) ? (string) $pt_obj->labels->singular_name : $slug; $id = 'aivisy_ex_pt_' . $slug; ?>

'ai-view-llms-save-exclusions' ) ); ?> >

get_error_message() ); ?>

'ai-view-llms-save-manual-pre' ) ); ?> >

'ai-view-llms-wp-merge-submit-pre' ) ); ?> >

  • :
  • :
  • :
  • :