Files
hoponhopofftickets/viator-collector/includes/class-vc-settings.php

593 lines
33 KiB
PHP

<?php
defined( 'ABSPATH' ) || exit;
class VC_Settings {
public static function init() {
add_action( 'admin_menu', array( __CLASS__, 'admin_menu' ) );
add_action( 'admin_init', array( __CLASS__, 'register_settings' ) );
add_action( 'admin_post_vc_test_api', array( __CLASS__, 'handle_test_api' ) );
add_action( 'admin_post_vc_manual_sync', array( __CLASS__, 'handle_manual_sync' ) );
add_action( 'admin_post_vc_reset_sync', array( __CLASS__, 'handle_reset_sync' ) );
add_action( 'admin_post_vc_delete_all', array( __CLASS__, 'handle_delete_all' ) );
add_action( 'admin_post_vc_clear_log', array( __CLASS__, 'handle_clear_log' ) );
add_action( 'admin_post_vc_import_product', array( __CLASS__, 'handle_import_product' ) );
add_action( 'wp_ajax_vc_search_destinations', array( __CLASS__, 'ajax_search_destinations' ) );
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'admin_assets' ) );
}
/**
* Load the WordPress media library on our settings screen so the hero
* image can be picked/uploaded without editing plugin files.
*/
public static function admin_assets( $hook ) {
if ( 'settings_page_viator-collector' === $hook ) {
wp_enqueue_media();
}
}
/**
* Keep only numeric, comma-separated destination IDs.
*/
public static function sanitize_id_list( $value ) {
$ids = array_filter( array_map( 'trim', explode( ',', (string) $value ) ), 'ctype_digit' );
return implode( ',', array_values( array_unique( $ids ) ) );
}
/**
* Live destination lookup for the settings screen so the exact Viator IDs
* can be found and added to the crawl filter.
*/
public static function ajax_search_destinations() {
check_ajax_referer( 'vc_search_destinations' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( '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_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' ),
'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();
?>
<div class="wrap">
<h1><?php echo esc_html__( 'Viator Collector', 'viator-collector' ); ?></h1>
<?php if ( $notice ) : ?>
<div class="notice notice-info is-dismissible"><p><?php echo esc_html( $notice ); ?></p></div>
<?php endif; ?>
<h2><?php echo esc_html__( 'Sync status', 'viator-collector' ); ?></h2>
<table class="widefat striped" style="max-width:640px;margin-bottom:1.5em;">
<tbody>
<tr><td><?php echo esc_html__( 'State', 'viator-collector' ); ?></td><td><?php echo $status['in_progress'] ? esc_html__( 'Running', 'viator-collector' ) : esc_html__( 'Idle', 'viator-collector' ); ?></td></tr>
<tr><td><?php echo esc_html__( 'Destinations left in current run', 'viator-collector' ); ?></td><td><?php echo esc_html( (string) $status['queue_remaining'] ); ?></td></tr>
<tr><td><?php echo esc_html__( 'Last started', 'viator-collector' ); ?></td><td><?php echo esc_html( $status['started_at'] ? $status['started_at'] : '—' ); ?></td></tr>
<tr><td><?php echo esc_html__( 'Last completed', 'viator-collector' ); ?></td><td><?php echo esc_html( $status['completed_at'] ? $status['completed_at'] : '—' ); ?></td></tr>
<tr><td><?php echo esc_html__( 'Current run progress', 'viator-collector' ); ?></td><td><?php echo esc_html( wp_json_encode( $status['totals'] ) ); ?></td></tr>
<tr><td><?php echo esc_html__( 'Next automatic run', 'viator-collector' ); ?></td><td><?php echo esc_html( $status['next_scheduled'] ? gmdate( 'Y-m-d H:i:s', (int) $status['next_scheduled'] ) . ' UTC' : __( 'Not scheduled', 'viator-collector' ) ); ?></td></tr>
</tbody>
</table>
<form method="post" action="options.php">
<?php settings_fields( 'vc_settings_group' ); ?>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><label for="vc_api_key"><?php echo esc_html__( 'API key', 'viator-collector' ); ?></label></th>
<td>
<input id="vc_api_key" type="password" class="regular-text" name="vc_settings[api_key]" value="<?php echo esc_attr( $options['api_key'] ); ?>" autocomplete="off">
<?php if ( defined( 'VIATOR_API_KEY' ) && VIATOR_API_KEY ) : ?>
<p class="description"><?php echo esc_html__( 'VIATOR_API_KEY is defined in wp-config.php and will be used instead.', 'viator-collector' ); ?></p>
<?php endif; ?>
</td>
</tr>
<tr><th scope="row"><label for="vc_api_base_url"><?php echo esc_html__( 'API base URL', 'viator-collector' ); ?></label></th><td><input id="vc_api_base_url" type="url" class="regular-text" name="vc_settings[api_base_url]" value="<?php echo esc_attr( $options['api_base_url'] ); ?>"></td></tr>
<tr><th scope="row"><label for="vc_language"><?php echo esc_html__( 'Language', 'viator-collector' ); ?></label></th><td><input id="vc_language" type="text" name="vc_settings[language]" value="<?php echo esc_attr( $options['language'] ); ?>"></td></tr>
<tr><th scope="row"><label for="vc_currency"><?php echo esc_html__( 'Currency', 'viator-collector' ); ?></label></th><td><input id="vc_currency" type="text" name="vc_settings[currency]" value="<?php echo esc_attr( $options['currency'] ); ?>" maxlength="3"></td></tr>
<tr><th scope="row"><label for="vc_timeout"><?php echo esc_html__( 'Timeout (seconds)', 'viator-collector' ); ?></label></th><td><input id="vc_timeout" type="number" min="5" name="vc_settings[timeout]" value="<?php echo esc_attr( $options['timeout'] ); ?>"></td></tr>
<tr>
<th scope="row"><label for="vc_default_post_status"><?php echo esc_html__( 'Default post status', 'viator-collector' ); ?></label></th>
<td>
<select id="vc_default_post_status" name="vc_settings[default_post_status]">
<?php foreach ( array( 'publish', 'draft', 'pending' ) as $post_status ) : ?>
<option value="<?php echo esc_attr( $post_status ); ?>" <?php selected( $options['default_post_status'], $post_status ); ?>><?php echo esc_html( ucfirst( $post_status ) ); ?></option>
<?php endforeach; ?>
</select>
<p class="description"><?php echo esc_html__( 'Deactivated products are always set to draft regardless of this setting.', 'viator-collector' ); ?></p>
</td>
</tr>
<tr>
<th scope="row"><label for="vc_hero_image"><?php echo esc_html__( 'Hero image', 'viator-collector' ); ?></label></th>
<td>
<input id="vc_hero_image" type="url" class="regular-text" name="vc_settings[hero_image]" value="<?php echo esc_attr( $options['hero_image'] ); ?>" placeholder="https://…">
<input id="vc_hero_image_id" type="hidden" name="vc_settings[hero_image_id]" value="<?php echo esc_attr( $options['hero_image_id'] ); ?>">
<button type="button" class="button" id="vc-hero-pick"><?php echo esc_html__( 'Select image', 'viator-collector' ); ?></button>
<button type="button" class="button" id="vc-hero-clear"><?php echo esc_html__( 'Clear', 'viator-collector' ); ?></button>
<p class="description"><?php echo esc_html__( 'Banner at the top of the tours listing / home hero. Pick from the Media Library so the browser gets responsive, device-sized images. Leave empty for a gradient. Destination pages use their own image when one is set.', 'viator-collector' ); ?></p>
<div id="vc-hero-preview" style="margin-top:8px;">
<?php if ( $options['hero_image'] ) : ?>
<img src="<?php echo esc_url( $options['hero_image'] ); ?>" alt="" style="max-width:320px;height:auto;border-radius:8px;">
<?php endif; ?>
</div>
<script>
jQuery(function ($) {
var frame;
$('#vc-hero-pick').on('click', function (e) {
e.preventDefault();
if (frame) { frame.open(); return; }
frame = wp.media({
title: <?php echo wp_json_encode( __( 'Select hero image', 'viator-collector' ) ); ?>,
button: { text: <?php echo wp_json_encode( __( 'Use this image', 'viator-collector' ) ); ?> },
library: { type: 'image' },
multiple: false
});
frame.on('select', function () {
var img = frame.state().get('selection').first().toJSON();
$('#vc_hero_image').val(img.url);
$('#vc_hero_image_id').val(img.id);
$('#vc-hero-preview').html($('<img>', { src: img.url, alt: '' }).css({ maxWidth: '320px', height: 'auto', borderRadius: '8px' }));
});
frame.open();
});
$('#vc-hero-clear').on('click', function (e) {
e.preventDefault();
$('#vc_hero_image').val('');
$('#vc_hero_image_id').val('');
$('#vc-hero-preview').empty();
});
});
</script>
</td>
</tr>
<tr>
<th scope="row"><label><?php echo esc_html__( 'Single tour hero figure', 'viator-collector' ); ?></label></th>
<td>
<input id="vc_single_figure_id" type="hidden" name="vc_settings[single_hero_figure_id]" value="<?php echo esc_attr( $options['single_hero_figure_id'] ); ?>">
<button type="button" class="button" id="vc-figure-pick"><?php echo esc_html__( 'Select image', 'viator-collector' ); ?></button>
<button type="button" class="button" id="vc-figure-clear"><?php echo esc_html__( 'Clear', 'viator-collector' ); ?></button>
<p class="description"><?php echo esc_html__( 'Optional decorative cut-out shown over the single-tour hero (e.g. a landmark PNG with a transparent background). Leave empty to hide it.', 'viator-collector' ); ?></p>
<div id="vc-figure-preview" style="margin-top:8px;">
<?php
if ( $options['single_hero_figure_id'] ) {
echo wp_get_attachment_image( (int) $options['single_hero_figure_id'], 'medium', false, array( 'style' => 'max-width:200px;height:auto;' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
?>
</div>
<script>
jQuery(function ($) {
var frame;
$('#vc-figure-pick').on('click', function (e) {
e.preventDefault();
if (frame) { frame.open(); return; }
frame = wp.media({
title: <?php echo wp_json_encode( __( 'Select hero figure', 'viator-collector' ) ); ?>,
button: { text: <?php echo wp_json_encode( __( 'Use this image', 'viator-collector' ) ); ?> },
library: { type: 'image' },
multiple: false
});
frame.on('select', function () {
var img = frame.state().get('selection').first().toJSON();
$('#vc_single_figure_id').val(img.id);
var src = (img.sizes && img.sizes.medium) ? img.sizes.medium.url : img.url;
$('#vc-figure-preview').html($('<img>', { src: src, alt: '' }).css({ maxWidth: '200px', height: 'auto' }));
});
frame.open();
});
$('#vc-figure-clear').on('click', function (e) {
e.preventDefault();
$('#vc_single_figure_id').val('');
$('#vc-figure-preview').empty();
});
});
</script>
</td>
</tr>
<tr>
<th scope="row"><label for="vc_hero_slides"><?php echo esc_html__( 'Hero slider images', 'viator-collector' ); ?></label></th>
<td>
<input id="vc_hero_slides" type="hidden" name="vc_settings[hero_slides]" value="<?php echo esc_attr( $options['hero_slides'] ); ?>">
<button type="button" class="button" id="vc-slides-pick"><?php echo esc_html__( 'Select images', 'viator-collector' ); ?></button>
<button type="button" class="button" id="vc-slides-clear"><?php echo esc_html__( 'Clear', 'viator-collector' ); ?></button>
<p class="description">
<?php echo esc_html__( 'Slides for the [viator_hero_slider] shortcode. Drag is not supported — reselect to change the order.', 'viator-collector' ); ?>
</p>
<div id="vc-slides-preview" style="display:flex;flex-wrap:wrap;gap:8px;margin-top:10px;">
<?php
$vc_slide_ids = array_filter( array_map( 'absint', explode( ',', (string) $options['hero_slides'] ) ) );
foreach ( $vc_slide_ids as $vc_slide_id ) {
echo wp_get_attachment_image( $vc_slide_id, 'thumbnail', false, array( 'style' => 'width:80px;height:60px;object-fit:cover;border-radius:6px;' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
?>
</div>
<script>
jQuery(function ($) {
var frame;
function paint(list) {
var $p = $('#vc-slides-preview').empty();
list.forEach(function (item) {
$('<img>', { src: item.url }).css({
width: '80px', height: '60px', objectFit: 'cover', borderRadius: '6px'
}).appendTo($p);
});
}
$('#vc-slides-pick').on('click', function (e) {
e.preventDefault();
frame = wp.media({
title: <?php echo wp_json_encode( __( 'Select hero slides', 'viator-collector' ) ); ?>,
button: { text: <?php echo wp_json_encode( __( 'Use these images', 'viator-collector' ) ); ?> },
library: { type: 'image' },
multiple: 'add'
});
frame.on('select', function () {
var ids = [], list = [];
frame.state().get('selection').each(function (a) {
var j = a.toJSON();
ids.push(j.id);
list.push({ url: (j.sizes && j.sizes.thumbnail) ? j.sizes.thumbnail.url : j.url });
});
$('#vc_hero_slides').val(ids.join(','));
paint(list);
});
frame.open();
});
$('#vc-slides-clear').on('click', function (e) {
e.preventDefault();
$('#vc_hero_slides').val('');
$('#vc-slides-preview').empty();
});
});
</script>
</td>
</tr>
<tr>
<th scope="row"><label for="vc_crawl_destinations"><?php echo esc_html__( 'Destination filter', 'viator-collector' ); ?></label></th>
<td>
<input id="vc_crawl_destinations" type="text" class="large-text" name="vc_settings[crawl_destinations]" value="<?php echo esc_attr( $options['crawl_destinations'] ); ?>" placeholder="<?php echo esc_attr__( 'e.g. 664, 662, 761', 'viator-collector' ); ?>">
<p class="description"><?php echo esc_html__( 'Comma-separated Viator destination IDs to crawl. Leave empty to crawl the whole catalogue. Use the search below to find IDs. Used only when no cities are set on the Viator Structure screen.', 'viator-collector' ); ?></p>
<div class="vc-dest-finder" style="margin-top:8px;max-width:640px;">
<input type="search" id="vc-dest-search" class="regular-text" placeholder="<?php echo esc_attr__( 'Search destinations, e.g. Orlando', 'viator-collector' ); ?>">
<button type="button" class="button" id="vc-dest-search-btn"><?php echo esc_html__( 'Search', 'viator-collector' ); ?></button>
<div id="vc-dest-results" style="margin-top:8px;"></div>
</div>
<script>
(function(){
var btn=document.getElementById('vc-dest-search-btn'),
q=document.getElementById('vc-dest-search'),
out=document.getElementById('vc-dest-results'),
field=document.getElementById('vc_crawl_destinations'),
nonce=<?php echo wp_json_encode( wp_create_nonce( 'vc_search_destinations' ) ); ?>;
function addId(id){
var ids=field.value.split(',').map(function(s){return s.trim();}).filter(Boolean);
if(ids.indexOf(id)===-1){ids.push(id);} field.value=ids.join(', ');
}
function search(){
out.textContent='<?php echo esc_js( __( 'Searching…', 'viator-collector' ) ); ?>';
var d=new FormData();
d.append('action','vc_search_destinations');
d.append('_ajax_nonce',nonce);
d.append('q',q.value);
fetch(ajaxurl,{method:'POST',credentials:'same-origin',body:d})
.then(function(r){return r.json();})
.then(function(res){
if(!res||!res.success){out.textContent=(res&&res.data&&res.data.message)||'<?php echo esc_js( __( 'Search failed.', 'viator-collector' ) ); ?>';return;}
if(!res.data.results.length){out.textContent='<?php echo esc_js( __( 'No matches.', 'viator-collector' ) ); ?>';return;}
out.innerHTML='';
res.data.results.forEach(function(item){
var row=document.createElement('div');
row.style.cssText='display:flex;align-items:center;gap:8px;padding:3px 0;';
var b=document.createElement('button');
b.type='button';b.className='button button-small';b.textContent='<?php echo esc_js( __( 'Add', 'viator-collector' ) ); ?>';
b.addEventListener('click',function(){addId(item.id);});
var span=document.createElement('span');
span.textContent=item.name+' (ID '+item.id+')';
row.appendChild(b);row.appendChild(span);out.appendChild(row);
});
})
.catch(function(){out.textContent='<?php echo esc_js( __( 'Request failed.', 'viator-collector' ) ); ?>';});
}
btn.addEventListener('click',search);
q.addEventListener('keydown',function(e){if(e.key==='Enter'){e.preventDefault();search();}});
})();
</script>
</td>
</tr>
<tr><th scope="row"><label for="vc_rate_limit"><?php echo esc_html__( 'Max API requests per minute', 'viator-collector' ); ?></label></th><td><input id="vc_rate_limit" type="number" min="1" max="60" name="vc_settings[rate_limit_per_minute]" value="<?php echo esc_attr( $options['rate_limit_per_minute'] ); ?>"><p class="description"><?php echo esc_html__( 'Requests are throttled to stay under this limit. Viator Basic access allows 25.', 'viator-collector' ); ?></p></td></tr>
<tr><th scope="row"><label for="vc_products_per_page"><?php echo esc_html__( 'Products per search page', 'viator-collector' ); ?></label></th><td><input id="vc_products_per_page" type="number" min="1" max="50" name="vc_settings[products_per_page]" value="<?php echo esc_attr( $options['products_per_page'] ); ?>"><p class="description"><?php echo esc_html__( 'Maximum 50 (Viator limit). Lower this only if full detail is ON and ticks time out.', 'viator-collector' ); ?></p></td></tr>
<tr><th scope="row"><label for="vc_pages_per_run"><?php echo esc_html__( 'Search pages per tick', 'viator-collector' ); ?></label></th><td><input id="vc_pages_per_run" type="number" min="1" max="100" name="vc_settings[pages_per_run]" value="<?php echo esc_attr( $options['pages_per_run'] ); ?>"><p class="description"><?php echo esc_html__( 'How many search pages every background tick processes before scheduling the next. Lower values avoid PHP timeouts.', 'viator-collector' ); ?></p></td></tr>
<tr><th scope="row"><?php echo esc_html__( 'Fetch full detail during crawl', 'viator-collector' ); ?></th><td><label><input type="checkbox" name="vc_settings[fetch_full_detail]" value="1" <?php checked( $options['fetch_full_detail'], 1 ); ?>> <?php echo esc_html__( 'Call /products/{code} for every product while crawling.', 'viator-collector' ); ?></label><p class="description"><?php echo esc_html__( 'Leave OFF (recommended): the crawl stays fast (1 request per 50 products) and full detail (itinerary, inclusions, cancellation policy) is fetched and cached the first time each tour page is viewed. Turn ON only if you must have full detail for every product up front — far slower under the rate limit.', 'viator-collector' ); ?></p></td></tr>
<tr><th scope="row"><label for="vc_detail_batch_size"><?php echo esc_html__( 'Detail batch size per tick', 'viator-collector' ); ?></label></th><td><input id="vc_detail_batch_size" type="number" min="1" max="25" name="vc_settings[detail_batch_size]" value="<?php echo esc_attr( $options['detail_batch_size'] ); ?>"><p class="description"><?php echo esc_html__( 'Only used when full detail is ON. Products processed per cron minute. Lower this if ticks time out; raise it (up to ~20) to go faster, staying under your per-minute limit.', 'viator-collector' ); ?></p></td></tr>
<tr><th scope="row"><label for="vc_log_every"><?php echo esc_html__( 'Log progress every N products', 'viator-collector' ); ?></label></th><td><input id="vc_log_every" type="number" min="1" max="1000" name="vc_settings[log_every]" value="<?php echo esc_attr( $options['log_every'] ); ?>"><p class="description"><?php echo esc_html__( 'How often a progress line is written to the activity log below.', 'viator-collector' ); ?></p></td></tr>
<tr><th scope="row"><?php echo esc_html__( 'Automatic sync', 'viator-collector' ); ?></th><td><label><input type="checkbox" name="vc_settings[automatic_sync]" value="1" <?php checked( $options['automatic_sync'], 1 ); ?>> <?php echo esc_html__( 'Refresh the catalogue every 24 hours', 'viator-collector' ); ?></label></td></tr>
<tr><th scope="row"><?php echo esc_html__( 'Debug logging', 'viator-collector' ); ?></th><td><label><input type="checkbox" name="vc_settings[debug_logging]" value="1" <?php checked( $options['debug_logging'], 1 ); ?>> <?php echo esc_html__( 'Enable debug logging', 'viator-collector' ); ?></label></td></tr>
</table>
<?php submit_button(); ?>
</form>
<hr>
<h2><?php echo esc_html__( 'Manual actions', 'viator-collector' ); ?></h2>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="display:inline-block;margin-right:1em;">
<input type="hidden" name="action" value="vc_test_api">
<?php wp_nonce_field( 'vc_test_api' ); ?>
<?php submit_button( __( 'Test API connection', 'viator-collector' ), 'secondary', 'submit', false ); ?>
</form>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="display:inline-block;margin-right:1em;">
<input type="hidden" name="action" value="vc_manual_sync">
<?php wp_nonce_field( 'vc_manual_sync' ); ?>
<?php submit_button( __( 'Run sync now', 'viator-collector' ), 'primary', 'submit', false ); ?>
</form>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="display:inline-block;" onsubmit="return confirm('<?php echo esc_js( __( 'Reset ingestion progress and start a full crawl on the next run?', 'viator-collector' ) ); ?>');">
<input type="hidden" name="action" value="vc_reset_sync">
<?php wp_nonce_field( 'vc_reset_sync' ); ?>
<?php submit_button( __( 'Reset ingestion', 'viator-collector' ), 'delete', 'submit', false ); ?>
</form>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="display:inline-block;margin-left:1em;" onsubmit="return confirm('<?php echo esc_js( __( 'This permanently deletes ALL imported tours, destination/tag terms, and sync data. Your settings are kept. Continue?', 'viator-collector' ) ); ?>');">
<input type="hidden" name="action" value="vc_delete_all">
<?php wp_nonce_field( 'vc_delete_all' ); ?>
<?php submit_button( __( 'Delete all imported data', 'viator-collector' ), 'delete', 'submit', false ); ?>
<p class="description"><?php echo esc_html__( 'Removes every tour and term this plugin created. Settings (API key, destination filter) are preserved.', 'viator-collector' ); ?></p>
</form>
<hr>
<h2><?php echo esc_html__( 'Import a single product', 'viator-collector' ); ?></h2>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="vc_import_product">
<?php wp_nonce_field( 'vc_import_product' ); ?>
<label for="vc_product_code"><?php echo esc_html__( 'Viator product code', 'viator-collector' ); ?></label>
<input id="vc_product_code" type="text" name="product_code" required>
<?php submit_button( __( 'Import product', 'viator-collector' ), 'secondary', 'submit', false ); ?>
</form>
<hr>
<h2><?php echo esc_html__( 'Activity log', 'viator-collector' ); ?></h2>
<?php $log = VC_Logger::get_log(); ?>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="margin-bottom:1em;">
<input type="hidden" name="action" value="vc_clear_log">
<?php wp_nonce_field( 'vc_clear_log' ); ?>
<?php submit_button( __( 'Clear log', 'viator-collector' ), 'secondary', 'submit', false ); ?>
</form>
<?php if ( empty( $log ) ) : ?>
<p><?php echo esc_html__( 'No activity recorded yet.', 'viator-collector' ); ?></p>
<?php else : ?>
<table class="widefat striped" style="max-width:900px;">
<thead>
<tr>
<th style="width:160px;"><?php echo esc_html__( 'Time', 'viator-collector' ); ?></th>
<th style="width:70px;"><?php echo esc_html__( 'Level', 'viator-collector' ); ?></th>
<th><?php echo esc_html__( 'Message', 'viator-collector' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $log as $entry ) : ?>
<?php $is_error = isset( $entry['level'] ) && 'error' === $entry['level']; ?>
<tr>
<td><?php echo esc_html( isset( $entry['t'] ) ? $entry['t'] : '' ); ?></td>
<td><span style="font-weight:600;color:<?php echo $is_error ? '#b32d2e' : '#2271b1'; ?>;"><?php echo $is_error ? esc_html__( 'ERROR', 'viator-collector' ) : esc_html__( 'INFO', 'viator-collector' ); ?></span></td>
<td><?php echo esc_html( isset( $entry['msg'] ) ? $entry['msg'] : '' ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<?php
}
public static function handle_test_api() {
self::verify_action( 'vc_test_api' );
$result = ( new VC_API() )->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;
}
}