admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'vc_search_destinations' ), 'i18n' => array( 'searching' => __( 'Searching…', 'viator-collector' ), 'noMatches' => __( 'No matches.', 'viator-collector' ), 'failed' => __( 'Search failed.', 'viator-collector' ), 'add' => __( 'Add', 'viator-collector' ), 'used' => __( 'Already added', 'viator-collector' ), 'confirm' => __( 'Remove this region and all its cities from the structure?', 'viator-collector' ), ), ) ); } private static function draft_key() { return 'vc_structure_draft_' . get_current_user_id(); } public static function render_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } $draft = get_transient( self::draft_key() ); $errors = array(); $structure = VC_Structure::get(); if ( is_array( $draft ) ) { delete_transient( self::draft_key() ); $errors = (array) $draft['errors']; $structure = (array) $draft['structure']; } $notice = isset( $_GET['vc_notice'] ) ? sanitize_text_field( wp_unslash( $_GET['vc_notice'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification ?>

(ID ) // 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 ) ) . '

'; } }