__( '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;
}
}
}
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',
'hero_image' => '',
'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' ),
'hero_image' => esc_url_raw( $input['hero_image'] ?? '' ),
'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;
}
}