get_destination_map();
if ( is_wp_error( $map ) ) {
$errors = array( __( 'Could not load Viator destinations; nothing was saved.', 'viator-collector' ) );
} else {
$errors = VC_Structure::validate( $structure, (array) $map, self::taken_slugs( $structure ) );
// Names always come from Viator, never from the form.
foreach ( $structure as &$region ) {
foreach ( $region['cities'] as &$city ) {
if ( isset( $map[ $city['id'] ] ) ) {
$city['name'] = $map[ $city['id'] ];
}
}
unset( $city );
}
unset( $region );
}
if ( $errors ) {
set_transient(
self::draft_key(),
array(
'errors' => $errors,
'structure' => $structure,
),
10 * MINUTE_IN_SECONDS
);
self::redirect();
}
$structure = self::sync_terms( VC_Structure::get(), $structure );
update_option( VC_Structure::OPTION, $structure );
VC_Structure::flush_cache();
update_option( 'vc_flush_rewrite', 1 );
VC_Logger::info( sprintf( 'Structure saved: %d regions, %d cities.', count( $structure ), count( VC_Structure::cities( $structure ) ) ) );
self::redirect( __( 'Structure saved.', 'viator-collector' ) );
}
private static function redirect( $notice = '' ) {
$args = array( 'page' => self::PAGE );
if ( '' !== $notice ) {
$args['vc_notice'] = rawurlencode( $notice );
}
wp_safe_redirect( add_query_arg( $args, admin_url( 'options-general.php' ) ) );
exit;
}
/**
* Slugs (from this structure) already owned by something outside it:
* slug => human label.
*/
public static function taken_slugs( array $structure ) {
global $wpdb;
$slugs = array();
foreach ( $structure as $region ) {
$slugs[] = $region['slug'];
}
$cities = VC_Structure::cities( $structure );
foreach ( $cities as $city ) {
$slugs[] = $city['slug'];
}
$slugs = array_values( array_filter( array_unique( $slugs ) ) );
if ( ! $slugs ) {
return array();
}
$taken = array();
$types = array_diff( get_post_types( array( 'public' => true ) ), array( 'attachment', 'viator_tour' ) );
if ( $types ) {
$slug_in = implode( ',', array_fill( 0, count( $slugs ), '%s' ) );
$type_in = implode( ',', array_fill( 0, count( $types ), '%s' ) );
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery
$rows = $wpdb->get_results( $wpdb->prepare( "SELECT post_name, post_title, post_type, post_parent FROM {$wpdb->posts} WHERE post_name IN ($slug_in) AND post_type IN ($type_in) AND post_status NOT IN ('trash','auto-draft','inherit')", array_merge( $slugs, array_values( $types ) ) ) );
foreach ( (array) $rows as $row ) {
if ( 'page' === $row->post_type && (int) $row->post_parent ) {
continue; // Child pages live under their parent's path.
}
$label = 'page' === $row->post_type ? 'page "%s"' : 'post "%s"';
$taken[ $row->post_name ] = sprintf( $label, $row->post_title );
}
}
// Destination terms that the structure does not own.
$region_ids = array_filter( array_map( 'intval', array_column( $structure, 'term_id' ) ) );
// A new region (no term yet) adopts an existing term with its slug.
$adoptable = array();
foreach ( $structure as $region ) {
if ( ! (int) $region['term_id'] ) {
$adoptable[] = $region['slug'];
}
}
$terms = get_terms(
array(
'taxonomy' => 'viator_destination',
'slug' => $slugs,
'hide_empty' => false,
)
);
foreach ( is_wp_error( $terms ) ? array() : $terms as $term ) {
if ( in_array( (int) $term->term_id, $region_ids, true ) || in_array( $term->slug, $adoptable, true ) ) {
continue;
}
$viator_id = (string) get_term_meta( $term->term_id, '_vc_viator_destination_id', true );
if ( '' !== $viator_id && isset( $cities[ $viator_id ] ) ) {
continue;
}
$taken[ $term->slug ] = sprintf( 'destination "%s"', $term->name );
}
return $taken;
}
/**
* Mirror the structure into viator_destination terms: regions are
* top-level terms flagged _vc_region, cities are re-parented under them.
* Products are never touched.
*
* @return array $new with every region's term_id filled in.
*/
public static function sync_terms( array $old, array $new ) {
$tax = 'viator_destination';
foreach ( $new as &$region ) {
$term_id = (int) $region['term_id'];
if ( $term_id && ! get_term( $term_id, $tax ) ) {
$term_id = 0;
}
if ( ! $term_id ) {
$existing = get_term_by( 'slug', $region['slug'], $tax );
$term_id = $existing ? (int) $existing->term_id : 0;
}
if ( $term_id ) {
$result = wp_update_term(
$term_id,
$tax,
array(
'name' => $region['name'],
'slug' => $region['slug'],
'parent' => 0,
)
);
} else {
$result = wp_insert_term( $region['name'], $tax, array( 'slug' => $region['slug'] ) );
}
if ( is_wp_error( $result ) ) {
VC_Logger::error( 'Structure: region "' . $region['name'] . '": ' . $result->get_error_message() );
continue;
}
$region['term_id'] = (int) $result['term_id'];
update_term_meta( $region['term_id'], '_vc_region', 1 );
foreach ( $region['cities'] as $city ) {
$city_term = VC_Taxonomies::get_or_create_destination_by_id( $city['id'], $city['name'] );
if ( ! $city_term ) {
VC_Logger::error( 'Structure: could not create city term for ' . $city['id'] );
continue;
}
$result = wp_update_term(
$city_term,
$tax,
array(
'slug' => $city['slug'],
'parent' => $region['term_id'],
)
);
if ( is_wp_error( $result ) ) {
VC_Logger::error( 'Structure: city "' . $city['name'] . '": ' . $result->get_error_message() );
}
}
}
unset( $region );
// Regions that were removed: delete our own terms; an adopted Viator
// destination term just loses its region flag (it may carry products).
$kept = array_map( 'intval', array_column( $new, 'term_id' ) );
foreach ( $old as $region ) {
$term_id = (int) $region['term_id'];
if ( ! $term_id || in_array( $term_id, $kept, true ) ) {
continue;
}
if ( '' !== (string) get_term_meta( $term_id, '_vc_viator_destination_id', true ) ) {
delete_term_meta( $term_id, '_vc_region' );
} else {
wp_delete_term( $term_id, $tax );
}
}
// Cities that were removed go back to the top level.
$new_cities = VC_Structure::cities( $new );
foreach ( array_keys( VC_Structure::cities( $old ) ) as $id ) {
if ( isset( $new_cities[ (string) $id ] ) ) {
continue;
}
$found = get_terms(
array(
'taxonomy' => $tax,
'hide_empty' => false,
'number' => 1,
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery
array(
'key' => '_vc_viator_destination_id',
'value' => (string) $id,
),
),
)
);
if ( ! is_wp_error( $found ) && $found ) {
wp_update_term( $found[0]->term_id, $tax, array( 'parent' => 0 ) );
}
}
return $new;
}
/**
* WordPress does not stop a Page from taking a region/city slug, so warn.
*/
public static function page_clash_notice() {
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
if ( ! $screen || 'post' !== $screen->base || 'page' !== $screen->post_type ) {
return;
}
$post = get_post();
if ( ! $post || '' === $post->post_name || $post->post_parent ) {
return;
}
$structure = VC_Structure::get();
$owner = '';
foreach ( $structure as $region ) {
if ( $region['slug'] === $post->post_name ) {
$owner = $region['name'];
}
}
foreach ( VC_Structure::cities( $structure ) as $city ) {
if ( $city['slug'] === $post->post_name ) {
$owner = $city['name'];
}
}
if ( '' === $owner ) {
return;
}
echo '
' . esc_html( sprintf( __( 'This page\'s URL is also used by the Viator region/city "%s"; the Viator page will take priority.', 'viator-collector' ), $owner ) ) . '