plugin_name = $plugin_name; $this->version = $version; } /** * Register the stylesheets for the admin area. * * @param string $hook The current admin page hook. */ public function enqueue_styles( $hook ) { if ( false === strpos( $hook, 'affiliateboxes' ) ) { return; } $this->is_plugin_page = true; wp_enqueue_style( 'affiliateboxes-inter-font', AFFILIATEBOXES_PLUGIN_URL . 'admin/vendor/inter/inter.css', array(), $this->version ); wp_enqueue_style( $this->plugin_name, AFFILIATEBOXES_PLUGIN_URL . 'admin/css/affiliateboxes-admin.css', array( 'affiliateboxes-inter-font' ), $this->version, 'all' ); } /** * Print the bootstrap-icons SVG sprite (built by scripts/build-icon-sprite.js from only * the bi-* glyphs this plugin actually uses) once per plugin admin page, so `` references in the page markup resolve without loading an icon font. */ public function render_icon_sprite() { if ( ! $this->is_plugin_page ) { return; } $sprite_path = AFFILIATEBOXES_PLUGIN_DIR . 'admin/vendor/bootstrap-icons/sprite.svg'; if ( ! file_exists( $sprite_path ) ) { return; } // Plugin-authored build output, not user input - safe to print as-is. echo file_get_contents( $sprite_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Register the block editor assets. */ public function enqueue_block_editor_assets() { // Enqueue Choices.js. wp_enqueue_script( 'choices-js', AFFILIATEBOXES_PLUGIN_URL . 'admin/js/choices.min.js', array(), $this->version, true ); // Registered on init in AffiliateBoxes_Public::register_blocks(), so that the iframed // canvas can enqueue the same handles. wp_enqueue_style( 'affiliateboxes-editor' ); } /** * Register the JavaScript for the admin area. * * @param string $hook The current admin page hook. */ public function enqueue_scripts( $hook ) { if ( false === strpos( $hook, 'affiliateboxes' ) ) { return; } $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; wp_enqueue_script( 'affiliateboxes-bootstrap-js', AFFILIATEBOXES_PLUGIN_URL . 'admin/vendor/bootstrap.bundle' . $suffix . '.js', array(), $this->version, true ); wp_enqueue_script( $this->plugin_name, AFFILIATEBOXES_PLUGIN_URL . 'admin/js/affiliateboxes-admin.js', array( 'jquery', 'wp-i18n', 'affiliateboxes-bootstrap-js' ), $this->version, true ); // Set up translations. if ( function_exists( 'wp_set_script_translations' ) ) { wp_set_script_translations( $this->plugin_name, 'affiliateboxes' ); } // Localize the script with data. wp_localize_script( $this->plugin_name, 'affiliateboxes_ajax', array( 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'affiliateboxes_nonce' ), 'site_name' => get_bloginfo( 'name' ), 'site_url' => get_site_url(), // These will now be handled by wp.i18n in JS. 'i18n' => array( 'syncing' => __( 'Syncing...', 'affiliateboxes' ), 'synced' => __( 'Synced successfully!', 'affiliateboxes' ), 'error' => __( 'Error occurred. Please try again.', 'affiliateboxes' ), 'confirm_reset_sync' => __( 'This will permanently delete all synced AffiliateBoxes products and tables from your database, then re-sync everything. Continue?', 'affiliateboxes' ), 'confirm_disconnect' => __( 'Are you sure you want to disconnect your site? This will remove the API key and you will need to reconnect.', 'affiliateboxes' ), 'disconnecting' => __( 'Disconnecting...', 'affiliateboxes' ), 'last_synced' => __( 'Last synchronized: ', 'affiliateboxes' ), ), ) ); } /** * Handle disconnecting the site by removing the API key. * * @throws Exception If settings update fails. */ public function disconnect_site() { check_ajax_referer( 'affiliateboxes_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Unauthorized', 'affiliateboxes' ) ); } // Get current settings. $settings = get_option( 'affiliateboxes_settings', array() ); // Check if API key exists. if ( empty( $settings['api_key'] ) ) { $error = 'No API key found to disconnect.'; AffiliateBoxes_Logger::log( 'Disconnect error: ' . $error ); wp_send_json_error( $error, 400 ); } // Only the write is guarded. wp_send_json_success() ends the request via // wp_die(), so calling it inside this try would let the catch below // intercept the handler's own termination wherever wp_die() throws // instead of exiting - under a filtered wp_die_handler, or a test // harness - and emit a contradictory error after the success payload. try { $settings['api_key'] = ''; $updated = update_option( 'affiliateboxes_settings', $settings ); if ( false === $updated ) { throw new Exception( 'Failed to update settings.' ); } } catch ( Exception $e ) { $error = 'Error disconnecting: ' . $e->getMessage(); AffiliateBoxes_Logger::log( 'Disconnect error: ' . $error ); wp_send_json_error( $error, 500 ); } wp_send_json_success( array( 'message' => __( 'Successfully disconnected. Refreshing...', 'affiliateboxes' ), ) ); } /** * Add a "Settings" link under the plugin name on the plugins screen. * * @param array $links The plugin's action links. * @return array */ public function add_plugin_action_links( $links ) { $settings_link = sprintf( '%s', esc_url( admin_url( 'admin.php?page=affiliateboxes' ) ), esc_html__( 'Settings', 'affiliateboxes' ) ); array_unshift( $links, $settings_link ); return $links; } /** * Whether the post-activation onboarding notice belongs on the current screen. * * The notice is only worth showing to someone who can act on it, on the screen they land * on after activating, and only while the site still has no account connected. * * @return bool */ private function should_show_activation_notice() { global $pagenow; if ( 'plugins.php' !== $pagenow || ! current_user_can( 'manage_options' ) ) { return false; } if ( ! get_transient( 'affiliateboxes_activation_notice' ) ) { return false; } $settings = get_option( 'affiliateboxes_settings', array() ); if ( ! empty( $settings['api_key'] ) ) { // Connected in the meantime, so there is nothing left to nudge about. delete_transient( 'affiliateboxes_activation_notice' ); return false; } return true; } /** * Render the post-activation onboarding notice. */ public function render_activation_notice() { if ( ! $this->should_show_activation_notice() ) { return; } ?>

—

should_show_activation_notice() ) { return; } $nonce = wp_create_nonce( 'affiliateboxes_dismiss_notice' ); $script = 'document.addEventListener( "click", function ( event ) {' . 'if ( ! ( event.target instanceof Element ) ) { return; }' . 'if ( ! event.target.closest( ".affiliateboxes-activation-notice .notice-dismiss" ) ) { return; }' . 'var data = new FormData();' . 'data.append( "action", "affiliateboxes_dismiss_activation_notice" );' . 'data.append( "nonce", ' . wp_json_encode( $nonce ) . ' );' . 'window.fetch( window.ajaxurl, { method: "POST", credentials: "same-origin", body: data } );' . '} );'; wp_add_inline_script( 'common', $script ); } /** * Record that the onboarding notice has been dismissed. */ public function dismiss_activation_notice() { check_ajax_referer( 'affiliateboxes_dismiss_notice', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Unauthorized', 'affiliateboxes' ), 403 ); } delete_transient( 'affiliateboxes_activation_notice' ); wp_send_json_success(); } /** * Add the admin menu for specified pages. */ public function add_plugin_admin_menu() { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode $svg_icon = 'data:image/svg+xml;base64,' . base64_encode( ' ' ); add_menu_page( __( 'Affiliate Boxes', 'affiliateboxes' ), __( 'Affiliate Boxes', 'affiliateboxes' ), 'manage_options', 'affiliateboxes', array( $this, 'display_plugin_admin_page' ), $svg_icon, 76 ); } /** * Display the main plugin admin page. */ public function display_plugin_admin_page() { include_once AFFILIATEBOXES_PLUGIN_DIR . 'admin/partials/affiliateboxes-admin-display.php'; } /** * Sync products from affiliateboxes.com. */ public function sync_products() { check_ajax_referer( 'affiliateboxes_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Unauthorized', 'affiliateboxes' ) ); } $current_settings = get_option( 'affiliateboxes_settings' ); $api_key = $current_settings['api_key'] ?? ''; if ( empty( $api_key ) ) { wp_send_json_error( __( 'API key is not set', 'affiliateboxes' ) ); } $this->reset_synced_products_data(); // Trigger sync on remote API. $sync_url = 'https://affiliateboxes.com/api/'; $sync_args = array( 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, 'Accept' => 'application/json', ), 'timeout' => 15, ); /** This filter is documented in includes/class-affiliateboxes-utils.php. */ $response = wp_remote_get( $sync_url, apply_filters( 'affiliateboxes_http_request_args', $sync_args, $sync_url ) ); if ( is_wp_error( $response ) ) { wp_send_json_error( $response->get_error_message() ); } $body = wp_remote_retrieve_body( $response ); $response_data = json_decode( $body, true ); // Check if the response has the expected structure. if ( empty( $response_data ) || ! is_array( $response_data ) || ! isset( $response_data['success'] ) || true !== $response_data['success'] ) { wp_send_json_error( __( 'Invalid API response format', 'affiliateboxes' ) ); } // Return success immediately. The sync is handled by the remote server pushing data to the /wp-json/affiliateboxes/v1/receive endpoint. wp_send_json_success( array( 'message' => $response_data['message'] ?? __( 'Sync triggered successfully', 'affiliateboxes' ), ) ); } /** * Delete all synced local products/tables and related meta before a full re-sync. */ private function reset_synced_products_data() { global $wpdb; $affiliateboxes_products = get_posts( array( 'post_type' => 'affiliate_product', 'post_status' => 'any', 'numberposts' => -1, 'fields' => 'ids', ) ); if ( ! empty( $affiliateboxes_products ) ) { foreach ( $affiliateboxes_products as $affiliateboxes_product_id ) { wp_delete_post( $affiliateboxes_product_id, true ); } } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Direct query is necessary for bulk cleanup. $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->postmeta WHERE meta_key LIKE %s", '\_affprod\_%' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Direct query is necessary for bulk cleanup. $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->postmeta WHERE meta_key LIKE %s", '\_affiliateboxes\_%' ) ); if ( function_exists( 'as_unschedule_all_actions' ) ) { as_unschedule_all_actions( 'affiliateboxes_update_product', null, 'affiliateboxes' ); as_unschedule_all_actions( 'affiliateboxes_delete_product', null, 'affiliateboxes' ); } delete_option( 'affiliateboxes_last_sync' ); } /** * Get sync queue status for polling. */ public function get_sync_status() { check_ajax_referer( 'affiliateboxes_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Unauthorized', 'affiliateboxes' ) ); } $batch_id = isset( $_POST['batch_id'] ) ? sanitize_text_field( wp_unslash( $_POST['batch_id'] ) ) : ''; if ( empty( $batch_id ) ) { wp_send_json_error( __( 'Batch ID is required', 'affiliateboxes' ) ); } // Force run pending actions to ensure status is up to date. AffiliateBoxes_Action_Scheduler::force_run_queue(); $status = AffiliateBoxes_Action_Scheduler::get_sync_queue_status( $batch_id ); if ( isset( $status['error'] ) ) { wp_send_json_error( $status['error'] ); } wp_send_json_success( $status ); } /** * Get overall queue status (not batch-specific). */ public function get_queue_status() { check_ajax_referer( 'affiliateboxes_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Unauthorized', 'affiliateboxes' ) ); } // Force run pending actions to ensure status is up to date. AffiliateBoxes_Action_Scheduler::force_run_queue(); $status = AffiliateBoxes_Action_Scheduler::get_overall_queue_status(); wp_send_json_success( $status ); } /** * Get available products HTML. */ public function get_available_products() { check_ajax_referer( 'affiliateboxes_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Unauthorized', 'affiliateboxes' ) ); } ob_start(); include AFFILIATEBOXES_PLUGIN_DIR . 'admin/partials/affiliateboxes-admin-products-list.php'; $html = ob_get_clean(); wp_send_json_success( array( 'html' => $html ) ); } /** * Start the connection process by generating a secret. */ public function start_connection() { check_ajax_referer( 'affiliateboxes_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Unauthorized', 'affiliateboxes' ) ); } // Generate a random secret. $secret = wp_generate_password( 32, false ); // Store it in a transient for 10 minutes. set_transient( 'affiliateboxes_connection_secret', $secret, 10 * MINUTE_IN_SECONDS ); wp_send_json_success( array( 'secret' => $secret, ) ); } }