__( 'Not allowed.', 'viator-collector' ) ) ); } $query = isset( $_POST['q'] ) ? sanitize_text_field( wp_unslash( $_POST['q'] ) ) : ''; $map = ( new VC_API() )->get_destination_map(); if ( is_wp_error( $map ) ) { wp_send_json_error( array( 'message' => $map->get_error_message() ) ); } $needle = strtolower( $query ); $results = array(); foreach ( (array) $map as $id => $name ) { if ( '' === $query || false !== strpos( strtolower( $name ), $needle ) ) { $results[] = array( 'id' => (string) $id, 'name' => $name, ); if ( count( $results ) >= 40 ) { break; } } } // Suggested URL slug for the Viator Structure screen: keep an existing // term's slug so live /tour-destination/{slug}/ links still resolve // (and 301), otherwise WordPress's own slug for the name. $existing = array(); if ( $results ) { $terms = get_terms( array( 'taxonomy' => 'viator_destination', 'hide_empty' => false, 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery array( 'key' => '_vc_viator_destination_id', 'value' => array_column( $results, 'id' ), 'compare' => 'IN', ), ), ) ); foreach ( is_wp_error( $terms ) ? array() : $terms as $term ) { $existing[ (string) get_term_meta( $term->term_id, '_vc_viator_destination_id', true ) ] = $term->slug; } } foreach ( $results as &$result ) { $result['slug'] = isset( $existing[ $result['id'] ] ) ? $existing[ $result['id'] ] : sanitize_title( $result['name'] ); } unset( $result ); wp_send_json_success( array( 'results' => $results ) ); } public static function defaults() { return array( 'api_key' => '', 'api_base_url' => 'https://api.viator.com/partner', 'language' => 'en-US', 'currency' => 'USD', 'timeout' => 20, 'default_post_status' => 'publish', 'contact_email' => '', 'hero_image' => '', 'hero_image_id' => 0, 'single_hero_figure_id' => 0, 'hero_slides' => '', 'crawl_destinations' => '', 'rate_limit_per_minute' => 25, 'products_per_page' => 50, 'pages_per_run' => 10, 'fetch_full_detail' => 0, 'detail_batch_size' => 15, 'log_every' => 10, 'automatic_sync' => 0, 'debug_logging' => 0, ); } public static function admin_menu() { add_options_page( __( 'Viator Collector', 'viator-collector' ), __( 'Viator Collector', 'viator-collector' ), 'manage_options', 'viator-collector', array( __CLASS__, 'render_page' ) ); } public static function register_settings() { register_setting( 'vc_settings_group', 'vc_settings', array( 'type' => 'array', 'sanitize_callback' => array( __CLASS__, 'sanitize_settings' ), 'default' => self::defaults(), ) ); } public static function sanitize_settings( $input ) { $input = is_array( $input ) ? $input : array(); $defaults = self::defaults(); $output = array( 'api_key' => sanitize_text_field( $input['api_key'] ?? '' ), 'api_base_url' => esc_url_raw( $input['api_base_url'] ?? $defaults['api_base_url'] ), 'language' => sanitize_text_field( $input['language'] ?? 'en-US' ), 'currency' => strtoupper( sanitize_text_field( $input['currency'] ?? 'USD' ) ), 'timeout' => max( 5, absint( $input['timeout'] ?? 20 ) ), 'default_post_status' => sanitize_key( $input['default_post_status'] ?? 'publish' ), 'contact_email' => sanitize_email( $input['contact_email'] ?? '' ), 'hero_image' => esc_url_raw( $input['hero_image'] ?? '' ), 'hero_image_id' => absint( $input['hero_image_id'] ?? 0 ), 'single_hero_figure_id' => absint( $input['single_hero_figure_id'] ?? 0 ), 'hero_slides' => self::sanitize_id_list( $input['hero_slides'] ?? '' ), 'crawl_destinations' => self::sanitize_id_list( $input['crawl_destinations'] ?? '' ), 'rate_limit_per_minute' => max( 1, min( 60, absint( $input['rate_limit_per_minute'] ?? 25 ) ) ), 'products_per_page' => max( 1, min( 50, absint( $input['products_per_page'] ?? 50 ) ) ), 'pages_per_run' => max( 1, min( 100, absint( $input['pages_per_run'] ?? 10 ) ) ), 'fetch_full_detail' => ! empty( $input['fetch_full_detail'] ) ? 1 : 0, 'detail_batch_size' => max( 1, min( 25, absint( $input['detail_batch_size'] ?? 15 ) ) ), 'log_every' => max( 1, min( 1000, absint( $input['log_every'] ?? 10 ) ) ), 'automatic_sync' => ! empty( $input['automatic_sync'] ) ? 1 : 0, 'debug_logging' => ! empty( $input['debug_logging'] ) ? 1 : 0, ); if ( ! in_array( $output['default_post_status'], array( 'publish', 'draft', 'pending' ), true ) ) { $output['default_post_status'] = 'publish'; } return $output; } public static function render_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } $options = wp_parse_args( vc_get_option( '', array() ), self::defaults() ); $notice = isset( $_GET['vc_notice'] ) ? sanitize_text_field( wp_unslash( $_GET['vc_notice'] ) ) : ''; $status = VC_Cron::status(); ?>




get_destinations(); if ( is_wp_error( $result ) ) { VC_Logger::error( 'API test failed: ' . $result->get_error_message() ); self::redirect_notice( $result->get_error_message() ); } VC_Logger::info( 'API test connection successful.' ); self::redirect_notice( __( 'API connection successful.', 'viator-collector' ) ); } public static function handle_manual_sync() { self::verify_action( 'vc_manual_sync' ); VC_Cron::run(); $totals = get_option( 'vc_ingest_totals', array() ); if ( get_option( 'vc_ingest_in_progress' ) ) { self::redirect_notice( sprintf( /* translators: %s: JSON progress counts. */ __( 'Sync started and continues in the background. Progress so far: %s', 'viator-collector' ), wp_json_encode( $totals ) ) ); } self::redirect_notice( sprintf( /* translators: %s: JSON result counts. */ __( 'Sync complete. Result: %s', 'viator-collector' ), wp_json_encode( get_option( 'vc_last_sync_totals', $totals ) ) ) ); } public static function handle_reset_sync() { self::verify_action( 'vc_reset_sync' ); VC_Cron::reset(); self::redirect_notice( __( 'Ingestion progress reset. The next run will crawl the full catalogue.', 'viator-collector' ) ); } public static function handle_clear_log() { self::verify_action( 'vc_clear_log' ); VC_Logger::clear_log(); self::redirect_notice( __( 'Activity log cleared.', 'viator-collector' ) ); } /** * Delete every imported tour, all plugin taxonomy terms, and sync/log * state. Plugin settings (API key, filter, etc.) are kept. */ public static function handle_delete_all() { self::verify_action( 'vc_delete_all' ); global $wpdb; // Stop and clear any in-progress run first. VC_Cron::reset(); // Tour posts + their meta + term links. $ids = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s", 'viator_tour' ) ); if ( ! empty( $ids ) ) { $in = implode( ',', array_map( 'intval', $ids ) ); $wpdb->query( "DELETE FROM {$wpdb->postmeta} WHERE post_id IN ($in)" ); // phpcs:ignore WordPress.DB.PreparedSQL $wpdb->query( "DELETE FROM {$wpdb->term_relationships} WHERE object_id IN ($in)" ); // phpcs:ignore WordPress.DB.PreparedSQL $wpdb->query( "DELETE FROM {$wpdb->posts} WHERE ID IN ($in)" ); // phpcs:ignore WordPress.DB.PreparedSQL } // Plugin taxonomy terms (WP API is safe; counts are small). foreach ( array( 'viator_destination', 'viator_tag', 'viator_category' ) as $taxonomy ) { $terms = get_terms( array( 'taxonomy' => $taxonomy, 'hide_empty' => false, 'fields' => 'ids' ) ); if ( ! is_wp_error( $terms ) ) { foreach ( $terms as $term_id ) { wp_delete_term( (int) $term_id, $taxonomy ); } } } // Sync state, logs, transients — but keep the saved settings. $wpdb->query( "DELETE FROM {$wpdb->options} WHERE ( option_name LIKE 'vc\\_%' AND option_name <> 'vc_settings' ) OR option_name LIKE '\\_transient\\_vc\\_%' OR option_name LIKE '\\_transient\\_timeout\\_vc\\_%'" ); // phpcs:ignore WordPress.DB.PreparedSQL wp_cache_flush(); $count = is_array( $ids ) ? count( $ids ) : 0; self::redirect_notice( sprintf( /* translators: %d: number of deleted tours. */ __( 'Deleted %d tours and all imported data. Your settings were kept.', 'viator-collector' ), $count ) ); } public static function handle_import_product() { self::verify_action( 'vc_import_product' ); $product_code = isset( $_POST['product_code'] ) ? sanitize_text_field( wp_unslash( $_POST['product_code'] ) ) : ''; $result = ( new VC_Importer() )->import_product_by_code( $product_code ); if ( is_wp_error( $result ) ) { VC_Logger::error( 'Manual product import failed (' . $product_code . '): ' . $result->get_error_message() ); self::redirect_notice( $result->get_error_message() ); } VC_Logger::info( 'Manual product import succeeded: ' . $product_code ); self::redirect_notice( __( 'Product imported successfully.', 'viator-collector' ) ); } private static function verify_action( $nonce_action ) { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to perform this action.', 'viator-collector' ) ); } check_admin_referer( $nonce_action ); } private static function redirect_notice( $message ) { wp_safe_redirect( add_query_arg( array( 'page' => 'viator-collector', 'vc_notice' => rawurlencode( sanitize_text_field( $message ) ), ), admin_url( 'options-general.php' ) ) ); exit; } }