array( $this, 'sanitize_settings' ) ) ); } /** * Sanitize settings. * * @param array $input Input values. * @return array */ public function sanitize_settings( $input ) { $sanitized = array(); if ( isset( $input['pixel_token'] ) ) { $sanitized['pixel_token'] = sanitize_text_field( $input['pixel_token'] ); } if ( isset( $input['store_id'] ) ) { $sanitized['store_id'] = sanitize_text_field( $input['store_id'] ); } if ( isset( $input['webhook_secret'] ) ) { $sanitized['webhook_secret'] = sanitize_text_field( $input['webhook_secret'] ); } if ( isset( $input['api_endpoint'] ) ) { $sanitized['api_endpoint'] = esc_url_raw( $input['api_endpoint'] ); } if ( isset( $input['tracking_enabled'] ) ) { $sanitized['tracking_enabled'] = (bool) $input['tracking_enabled']; } $current = get_option( 'agentshop_settings', array() ); return array_merge( $current, $sanitized ); } /** * Maybe redirect after activation. */ public function maybe_redirect() { if ( get_transient( 'agentshop_activated' ) ) { delete_transient( 'agentshop_activated' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- WordPress core parameter during activation. if ( ! isset( $_GET['activate-multi'] ) ) { wp_safe_redirect( admin_url( 'admin.php?page=agentshop&welcome=1' ) ); exit; } } } /** * Enqueue admin assets. * * @param string $hook Current admin page. */ public function enqueue_assets( $hook ) { if ( 'woocommerce_page_agentshop' !== $hook && 'marketing_page_agentshop' !== $hook ) { return; } wp_enqueue_style( 'agentshop-admin', AGENTSHOP_PLUGIN_URL . 'assets/css/admin.css', array(), AGENTSHOP_VERSION ); wp_enqueue_script( 'agentshop-admin', AGENTSHOP_PLUGIN_URL . 'assets/js/admin.js', array( 'jquery' ), AGENTSHOP_VERSION, array( 'in_footer' => true, 'strategy' => 'defer', ) ); wp_localize_script( 'agentshop-admin', 'agentshopAdmin', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'agentshop_admin' ), 'strings' => array( 'saving' => __( 'Saving...', 'agentshop' ), 'saved' => __( 'Settings saved!', 'agentshop' ), 'error' => __( 'An error occurred.', 'agentshop' ), 'testing' => __( 'Testing...', 'agentshop' ), 'connected' => __( 'Connected!', 'agentshop' ), 'disconnecting' => __( 'Disconnecting...', 'agentshop' ), 'disconnected' => __( 'Disconnected.', 'agentshop' ), 'confirmDisconnect' => __( 'Are you sure you want to disconnect?', 'agentshop' ), 'confirmBlogSetup' => __( 'This will create an Application Password and share it with AgentShop servers to enable blog publishing. Continue?', 'agentshop' ), ), ) ); } /** * Render settings page. */ public function render_page() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'Permission denied.', 'agentshop' ) ); } // Handle connection callback. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- OAuth callback uses state verification. if ( isset( $_GET['action'] ) && 'connected' === $_GET['action'] ) { $this->handle_connection_callback(); } // Auto-sync if not connected. $this->maybe_sync_from_backend(); // Auto-setup WordPress credentials if connected but not configured. $this->maybe_auto_setup_wordpress_credentials(); $options = agentshop()->get_options(); $is_connected = agentshop()->is_connected(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display flag only, no action taken. $is_welcome = isset( $_GET['welcome'] ) && '1' === $_GET['welcome']; include AGENTSHOP_PLUGIN_DIR . 'views/settings-page.php'; } /** * Handle OAuth connection callback. */ private function handle_connection_callback() { // Verify user has appropriate permissions. if ( ! current_user_can( 'manage_woocommerce' ) ) { agentshop()->log( 'OAuth callback: insufficient permissions', 'error' ); return; } // phpcs:disable WordPress.Security.NonceVerification.Recommended -- OAuth callback verified with mandatory state token. // Get state from request. $state = isset( $_GET['state'] ) ? sanitize_text_field( wp_unslash( $_GET['state'] ) ) : ''; // State parameter is REQUIRED - reject if missing. if ( empty( $state ) ) { agentshop()->log( 'OAuth callback: missing state parameter', 'error' ); return; } // Get stored state - REQUIRED to exist. $stored_state = get_transient( 'agentshop_oauth_state' ); // Stored state MUST exist - reject if transient expired or missing. if ( empty( $stored_state ) ) { agentshop()->log( 'OAuth callback: no stored state found (expired or missing)', 'error' ); return; } // States MUST match exactly. if ( ! hash_equals( $stored_state, $state ) ) { agentshop()->log( 'OAuth state mismatch - possible CSRF attack', 'error' ); delete_transient( 'agentshop_oauth_state' ); return; } // State validated - delete it immediately to prevent replay attacks. delete_transient( 'agentshop_oauth_state' ); // Get tokens from callback. $pixel_token = isset( $_GET['pixel_token'] ) ? sanitize_text_field( wp_unslash( $_GET['pixel_token'] ) ) : ''; $store_id = isset( $_GET['store_id'] ) ? sanitize_text_field( wp_unslash( $_GET['store_id'] ) ) : ''; $webhook_secret = isset( $_GET['webhook_secret'] ) ? sanitize_text_field( wp_unslash( $_GET['webhook_secret'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended if ( $pixel_token && $store_id ) { agentshop()->update_options( array( 'pixel_token' => $pixel_token, 'store_id' => $store_id, 'webhook_secret' => $webhook_secret, 'connected_at' => current_time( 'mysql' ), ) ); // Register webhooks. if ( agentshop()->webhooks ) { agentshop()->webhooks->register_all(); } // Create WordPress Application Password for blog publishing. $this->setup_wordpress_credentials(); // Register and flush rewrite rules to enable llms.txt endpoints. $this->register_llms_txt_rewrite_rules(); flush_rewrite_rules(); wp_safe_redirect( admin_url( 'admin.php?page=agentshop&connected=1' ) ); exit; } } /** * Sync connection from backend. */ private function maybe_sync_from_backend() { // Verify user has appropriate permissions. if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } if ( agentshop()->is_connected() ) { return; } $last_check = get_transient( 'agentshop_sync_check' ); if ( $last_check ) { return; } set_transient( 'agentshop_sync_check', true, 30 ); $result = agentshop()->api->check_plugin_status(); if ( is_wp_error( $result ) ) { return; } $data = $result['data'] ?? array(); if ( empty( $data['connected'] ) || empty( $data['verified'] ) || empty( $data['tokens'] ) ) { return; } $tokens = $data['tokens']; agentshop()->update_options( array( 'pixel_token' => $tokens['pixel_token'] ?? '', 'store_id' => $tokens['store_id'] ?? '', 'webhook_secret' => $tokens['webhook_secret'] ?? '', 'connected_at' => current_time( 'mysql' ), ) ); if ( agentshop()->webhooks ) { agentshop()->webhooks->register_all(); } // Create WordPress Application Password for blog publishing. $this->setup_wordpress_credentials(); // Register and flush rewrite rules to enable llms.txt endpoints. $this->register_llms_txt_rewrite_rules(); flush_rewrite_rules(); wp_safe_redirect( admin_url( 'admin.php?page=agentshop&synced=1' ) ); exit; } /** * Register llms.txt rewrite rules. * Called before flush_rewrite_rules() to ensure rules are registered. */ private function register_llms_txt_rewrite_rules() { add_rewrite_rule( '^llms\.txt$', 'index.php?agentshop_llms_txt=standard', 'top' ); add_rewrite_rule( '^\.well-known/llms\.txt$', 'index.php?agentshop_llms_txt=standard', 'top' ); add_rewrite_rule( '^llms-full\.txt$', 'index.php?agentshop_llms_txt=full', 'top' ); } /** * Generate OAuth state. * * @return string */ public function generate_oauth_state() { $state = wp_generate_password( 32, false ); set_transient( 'agentshop_oauth_state', $state, 15 * MINUTE_IN_SECONDS ); return $state; } /** * Auto-setup WordPress credentials if connected but not configured. * Handles the case where user connected via frontend dashboard. */ private function maybe_auto_setup_wordpress_credentials() { // Verify user has appropriate permissions. if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } // Only run if connected. if ( ! agentshop()->is_connected() ) { return; } // Check if already configured. $options = agentshop()->get_options(); if ( ! empty( $options['wp_app_password_configured'] ) ) { return; } // Check if we've already tried (don't spam). $attempted = get_transient( 'agentshop_wp_creds_attempted' ); if ( $attempted ) { return; } // Mark as attempted (expires in 1 hour). set_transient( 'agentshop_wp_creds_attempted', true, HOUR_IN_SECONDS ); // Try to setup. agentshop()->log( 'Auto-setting up WordPress credentials for connected store', 'info' ); $this->setup_wordpress_credentials(); } /** * Get status message. * * @return string */ public function get_status_message() { if ( ! agentshop()->is_connected() ) { return __( 'Not connected', 'agentshop' ); } $connected_at = agentshop()->get_options( 'connected_at' ); if ( $connected_at ) { return sprintf( /* translators: %s: date */ __( 'Connected since %s', 'agentshop' ), date_i18n( get_option( 'date_format' ), strtotime( $connected_at ) ) ); } return __( 'Connected', 'agentshop' ); } /** * AJAX: Save settings. */ public function ajax_save_settings() { check_ajax_referer( 'agentshop_admin', 'nonce' ); if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied.', 'agentshop' ) ) ); } $tracking_enabled = isset( $_POST['tracking_enabled'] ) && '1' === $_POST['tracking_enabled']; agentshop()->update_options( array( 'tracking_enabled' => $tracking_enabled ) ); wp_send_json_success( array( 'message' => __( 'Settings saved.', 'agentshop' ) ) ); } /** * AJAX: Test connection. */ public function ajax_test_connection() { check_ajax_referer( 'agentshop_admin', 'nonce' ); if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied.', 'agentshop' ) ) ); } if ( ! agentshop()->is_connected() ) { wp_send_json_error( array( 'message' => __( 'Not connected.', 'agentshop' ) ) ); } $result = agentshop()->api->verify_connection(); if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => sprintf( /* translators: %s: error message */ __( 'Test failed: %s', 'agentshop' ), $result->get_error_message() ), ) ); } wp_send_json_success( array( 'message' => __( 'Connection successful!', 'agentshop' ) ) ); } /** * AJAX: Disconnect. */ public function ajax_disconnect() { check_ajax_referer( 'agentshop_admin', 'nonce' ); if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied.', 'agentshop' ) ) ); } // Unregister webhooks. if ( agentshop()->webhooks ) { agentshop()->webhooks->unregister_all(); } // Notify backend. agentshop()->api->disconnect(); // Clear settings. agentshop()->update_options( array( 'pixel_token' => '', 'store_id' => '', 'webhook_secret' => '', 'connected_at' => '', 'wp_app_password_configured' => false, 'wp_app_password_user' => '', ) ); delete_transient( 'agentshop_wp_creds_attempted' ); wp_send_json_success( array( 'message' => __( 'Disconnected.', 'agentshop' ) ) ); } /** * AJAX: Setup blog publishing. */ public function ajax_setup_blog() { check_ajax_referer( 'agentshop_admin', 'nonce' ); if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied.', 'agentshop' ) ) ); } if ( ! agentshop()->is_connected() ) { wp_send_json_error( array( 'message' => __( 'Connect first.', 'agentshop' ) ) ); } if ( ! wp_is_application_passwords_available() ) { wp_send_json_error( array( 'message' => __( 'HTTPS required.', 'agentshop' ) ) ); } $result = $this->setup_wordpress_credentials(); if ( $result ) { wp_send_json_success( array( 'message' => __( 'Blog publishing enabled!', 'agentshop' ) ) ); } else { wp_send_json_error( array( 'message' => __( 'Setup failed.', 'agentshop' ) ) ); } } /** * Setup WordPress credentials. * * @return bool */ private function setup_wordpress_credentials() { // Verify user has WooCommerce management permissions. if ( ! current_user_can( 'manage_woocommerce' ) ) { agentshop()->log( 'setup_wordpress_credentials: insufficient permissions', 'error' ); return false; } if ( ! class_exists( 'WP_Application_Passwords' ) ) { return false; } if ( ! wp_is_application_passwords_available() ) { return false; } $current_user = wp_get_current_user(); if ( ! $current_user->exists() || ! user_can( $current_user, 'edit_posts' ) ) { return false; } $user_id = $current_user->ID; $app_name = 'AgentShop Blog Publishing'; // Delete existing. $existing = \WP_Application_Passwords::get_user_application_passwords( $user_id ); foreach ( $existing as $password ) { if ( $password['name'] === $app_name ) { \WP_Application_Passwords::delete_application_password( $user_id, $password['uuid'] ); break; } } // Create new. $result = \WP_Application_Passwords::create_new_application_password( $user_id, array( 'name' => $app_name, 'app_id' => 'agentshop', ) ); if ( is_wp_error( $result ) ) { agentshop()->log( 'Failed to create Application Password: ' . $result->get_error_message(), 'error' ); return false; } $password = $result[0]; // Send to backend immediately (don't store locally). $api_result = agentshop()->api->send_wordpress_credentials( $current_user->user_login, $password ); if ( is_wp_error( $api_result ) ) { agentshop()->log( 'Failed to send credentials to backend: ' . $api_result->get_error_message(), 'error' ); return false; } // Store only flag (NOT the password). agentshop()->update_options( array( 'wp_app_password_configured' => true, 'wp_app_password_user' => $current_user->user_login, ) ); agentshop()->log( 'Blog publishing configured', 'info' ); return true; } }