Add viator-collector plugin (Hop On Hop Off front-end reskin)

Consolidate the plugin into the TourWebsite repository (its separate
nested git repo has been removed). Includes the full new-design reskin:
scoped design stylesheet, SVG icon sprite, rebuilt listing/single/section
templates, and homepage hero/FAQ/testimonials shortcodes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J4hNL1PSHxXbu94SCenpKj
This commit is contained in:
2026-09-12 21:11:20 +03:30
parent 99e5413fee
commit 7ada8860f8
42 changed files with 9019 additions and 0 deletions

View File

@@ -0,0 +1,217 @@
<?php
defined( 'ABSPATH' ) || exit;
class VC_API {
public function get( $endpoint ) {
return $this->request( 'GET', $endpoint );
}
public function post( $endpoint, array $body = array() ) {
return $this->request( 'POST', $endpoint, $body );
}
public function request( $method, $endpoint, array $body = array() ) {
$api_key = vc_get_api_key();
if ( '' === $api_key ) {
return new WP_Error( 'vc_missing_api_key', __( 'Viator API key is missing.', 'viator-collector' ) );
}
$this->throttle();
$base = untrailingslashit( esc_url_raw( vc_get_option( 'api_base_url', 'https://api.viator.com/partner' ) ) );
$endpoint = '/' . ltrim( $endpoint, '/' );
$url = $base . $endpoint;
$timeout = max( 5, absint( vc_get_option( 'timeout', 20 ) ) );
$args = array(
'method' => strtoupper( $method ),
'timeout' => $timeout,
'headers' => array(
'Accept' => 'application/json;version=2.0',
'Accept-Language' => sanitize_text_field( vc_get_option( 'language', 'en-US' ) ),
'Content-Type' => 'application/json',
'exp-api-key' => $api_key,
),
);
if ( ! empty( $body ) && 'GET' !== strtoupper( $method ) ) {
$args['body'] = wp_json_encode( $body );
}
$response = wp_remote_request( $url, $args );
if ( is_wp_error( $response ) ) {
VC_Logger::log( 'API request failed: ' . $response->get_error_message(), true );
return $response;
}
$code = (int) wp_remote_retrieve_response_code( $response );
$raw = wp_remote_retrieve_body( $response );
if ( $code < 200 || $code >= 300 ) {
$message = sprintf( __( 'Viator API request failed with HTTP status %d.', 'viator-collector' ), $code );
$data = json_decode( $raw, true );
if ( is_array( $data ) && ! empty( $data['message'] ) ) {
$message .= ' ' . sanitize_text_field( $data['message'] );
}
VC_Logger::log( $message, true );
return new WP_Error( 'vc_api_http_error', $message, array( 'status' => $code ) );
}
$data = json_decode( $raw, true );
if ( JSON_ERROR_NONE !== json_last_error() ) {
return new WP_Error( 'vc_api_json_error', __( 'Viator API returned invalid JSON.', 'viator-collector' ) );
}
return is_array( $data ) ? $data : array();
}
/**
* Block until we are under the configured per-minute request budget.
*
* Timestamps of recent requests are persisted in an option so the limit
* holds across the separate PHP processes that WP-Cron continuation
* ticks run in, not just within a single request.
*/
private function throttle() {
$limit = max( 1, (int) vc_get_option( 'rate_limit_per_minute', 25 ) );
$now = microtime( true );
$window = get_option( 'vc_rate_window', array() );
if ( ! is_array( $window ) ) {
$window = array();
}
$window = array_values(
array_filter(
$window,
static function ( $t ) use ( $now ) {
return ( $now - (float) $t ) < 60.0;
}
)
);
if ( count( $window ) >= $limit ) {
$oldest = min( $window );
$wait = 60.0 - ( $now - (float) $oldest ) + 0.05;
if ( $wait > 0 ) {
usleep( (int) min( 60.0, $wait ) * 1000000 );
}
$now = microtime( true );
$window = array_values(
array_filter(
$window,
static function ( $t ) use ( $now ) {
return ( $now - (float) $t ) < 60.0;
}
)
);
}
$window[] = $now;
update_option( 'vc_rate_window', $window, false );
}
/**
* Search products within a single destination. This is the only way to
* enumerate the catalogue on Basic access: a destination filter is
* required and results are returned in pages (max 50 per page).
*
* @return array|WP_Error Decoded response, typically with `products` and `totalCount`.
*/
public function search_products( $destination, $start = 1, $count = 50, $currency = 'USD', array $extra_filters = array() ) {
$filtering = array_merge( array( 'destination' => (string) $destination ), $extra_filters );
$body = array(
'filtering' => $filtering,
'pagination' => array(
'start' => max( 1, (int) $start ),
'count' => max( 1, min( 50, (int) $count ) ),
),
'currency' => sanitize_text_field( $currency ),
);
return $this->post( '/products/search', $body );
}
public function get_product( string $product_code ) {
return $this->get( '/products/' . rawurlencode( $product_code ) );
}
public function get_destinations() {
$cache_key = 'vc_destinations_' . md5( vc_get_option( 'language', 'en-US' ) );
$cached = get_transient( $cache_key );
if ( false !== $cached ) {
return $cached;
}
$data = $this->get( '/destinations' );
if ( ! is_wp_error( $data ) ) {
set_transient( $cache_key, $data, 25 * HOUR_IN_SECONDS );
}
return $data;
}
public function get_tags() {
$cache_key = 'vc_tags_' . md5( vc_get_option( 'language', 'en-US' ) );
$cached = get_transient( $cache_key );
if ( false !== $cached ) {
return $cached;
}
$data = $this->get( '/products/tags' );
if ( ! is_wp_error( $data ) ) {
set_transient( $cache_key, $data, 25 * HOUR_IN_SECONDS );
}
return $data;
}
/**
* Destination dictionary as a map of destinationId => name.
*/
public function get_destination_map() {
$data = $this->get_destinations();
if ( is_wp_error( $data ) ) {
return $data;
}
$map = array();
foreach ( (array) vc_array_get( $data, 'destinations', array() ) as $destination ) {
$id = vc_array_get( $destination, 'destinationId' );
$name = vc_first_non_empty( array( vc_array_get( $destination, 'name' ), vc_array_get( $destination, 'destinationName' ) ) );
if ( null !== $id && '' !== (string) $name ) {
$map[ (string) $id ] = $name;
}
}
return $map;
}
/**
* Tag dictionary as a map of tagId => localized name.
*/
public function get_tag_map() {
$data = $this->get_tags();
if ( is_wp_error( $data ) ) {
return $data;
}
$lang = vc_get_option( 'language', 'en-US' );
$short = strtolower( substr( (string) $lang, 0, 2 ) );
$map = array();
foreach ( (array) vc_array_get( $data, 'tags', array() ) as $tag ) {
$id = vc_array_get( $tag, 'tagId' );
$names = vc_array_get( $tag, 'allNamesByLocale', array() );
$name = '';
if ( is_array( $names ) && ! empty( $names ) ) {
if ( isset( $names[ $lang ] ) ) {
$name = $names[ $lang ];
} elseif ( isset( $names[ $short ] ) ) {
$name = $names[ $short ];
} elseif ( isset( $names['en'] ) ) {
$name = $names['en'];
} else {
$name = reset( $names );
}
}
if ( '' === (string) $name ) {
$name = vc_array_get( $tag, 'name', '' );
}
if ( null !== $id && '' !== (string) $name ) {
$map[ (string) $id ] = $name;
}
}
return $map;
}
}