post = get_post($post); } elseif ($post instanceof \WP_Post) { $this->post = $post; } else { global $post; $this->post = $post; } } /** * Get the property WordPress post ID. * * @return int Post ID, or `0` when no post is loaded. */ public function get_id() { return $this->post ? $this->post->ID : 0; } /** * Get the property post title (WordPress post_title). * * @return string Empty string when no post is loaded. */ public function get_title() { return $this->post ? $this->post->post_title : ''; } /** * Get the property description with WordPress content filters applied. * * Runs `the_content` filter on post_content (shortcodes, autop, etc.). * * @return string HTML description or empty string. */ public function get_description() { if (!$this->post) { return ''; } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress core content filter. return apply_filters('the_content', $this->post->post_content); } /** * Read arbitrary post meta for this property. * * @param string $key Meta key as stored in postmeta (no prefix added). * @param mixed $default Returned when post ID is missing or value is empty string. * @return mixed */ public function get_meta($key, $default = '') { if (!$this->get_id()) { return $default; } return bizdex_uplisting_get_post_meta((int) $this->get_id(), (string) $key, $default); } // Getters for specific fields /** * Admin-managed nightly price (`uplisting_price`). * * Not updated by the calendar API. Used for inquiry/embed/button booking options. * * @return float Zero when unset or non-numeric. */ public function get_price(): float { $price = $this->get_meta('uplisting_price'); if (!is_numeric($price) || floatval($price) <= 0) { $price = $this->get_meta('uplisting_from_price'); } if (!is_numeric($price) || floatval($price) <= 0) { $price = $this->get_meta('uplisting_base_price'); } return is_numeric($price) ? floatval($price) : 0.0; } /** * Whether the frontend should show the admin manual rate (`uplisting_price`). */ public function uses_manual_listing_price(): bool { return $this->get_meta('booking_option', '') === 'uplisting_booking_form'; } /** * Whether the frontend should show the synced calendar day rate. */ public function uses_calendar_listing_price(): bool { return !$this->uses_manual_listing_price(); } /** * Today's calendar API nightly rate for `booking_form` properties. * * Meta keys: `uplisting_calendar_day_price`, `uplisting_calendar_day_price_date`. * * @return float Zero when unset, stale, or non-numeric. */ public function get_calendar_day_price(): float { $today = wp_date('Y-m-d'); $price_date = (string) $this->get_meta('uplisting_calendar_day_price_date', ''); if ($price_date === '') { $price_date = (string) $this->get_meta('uplisting_day_price_date', ''); } $price = 0.0; if ($price_date === $today) { $price = (float) $this->get_meta('uplisting_calendar_day_price', 0); if ($price <= 0) { $price = (float) $this->get_meta('uplisting_day_price', 0); } } if ($price > 0) { return $price; } $stale_price = (float) $this->get_meta('uplisting_calendar_day_price', 0); if ($stale_price <= 0) { $stale_price = (float) $this->get_meta('uplisting_day_price', 0); } return $stale_price > 0 ? $stale_price : 0.0; } /** * Frontend nightly rate — depends on `booking_option`. * * Default: synced calendar API rate. Inquiry form (`uplisting_booking_form`) uses `uplisting_price`. */ public function get_display_price(): float { if ($this->uses_manual_listing_price()) { return $this->get_price(); } return $this->get_calendar_day_price(); } /** * Whether the archive price should be prefixed with "From". */ public function listing_price_is_from_rate(): bool { return false; } /** * Formatted frontend nightly rate based on `booking_option`. */ public function get_formatted_display_price(): string { $price = $this->get_display_price(); if ($price <= 0) { return ''; } if (function_exists('hostaway_format_price')) { return hostaway_format_price($price); } if (function_exists('bizdex_uplisting_format_price')) { return bizdex_uplisting_format_price($price); } return '$' . number_format($price, 2); } /** * Get extra fees synced from Uplisting (`uplisting_fees` meta). * * Each fee: `name`, `label`, `amount`, `type` (fixed|percentage). * * @return array> */ public function get_fees() { $fees = $this->get_meta('uplisting_fees', []); return is_array($fees) ? $fees : []; } /** * Get extra taxes configured for this property. * * @return array */ public function get_taxes() { $taxes = $this->get_meta('uplisting_taxes', []); return is_array($taxes) ? $taxes : []; } /** * Get length-of-stay discounts for this property. * * @return array */ public function get_discounts() { $discounts = $this->get_meta('uplisting_discounts', []); return is_array($discounts) ? $discounts : []; } /** * Get security deposit settings for this property. * * @return array */ public function get_security_deposit() { $deposit = $this->get_meta('uplisting_security_deposit', []); return is_array($deposit) ? $deposit : []; } /** * Get the cleaning fee amount. * * @return float */ public function get_cleaning_fee() { return floatval($this->get_meta('cleaning_fee') ?: $this->get_meta('uplisting_cleaning_fee', 0)); } /** * Get the per-booking tax percentage. * * @return float */ public function get_tax_percentage() { $percentage = $this->get_meta('per_booking_percentage', ''); if ($percentage === '' || $percentage === false) { $percentage = $this->get_meta('uplisting_tax_percentage', ''); } if ($percentage === '' || $percentage === false || (float) $percentage <= 0) { foreach ($this->get_taxes() as $tax) { if (!is_array($tax)) { continue; } if (strtolower((string) ($tax['label'] ?? '')) !== 'per_booking_percentage') { continue; } $percentage = $tax['amount'] ?? 0; break; } } return floatval($percentage); } /** * Whether synced extra charges (cleaning, fees, taxes) apply on checkout. * * Meta key: `apply_extra_charges_at_checkout`. Default: off. * * Filter: uplisting_property_apply_extra_charges_at_checkout */ public function applies_extra_charges_at_checkout(): bool { $enabled = in_array($this->get_meta('apply_extra_charges_at_checkout'), ['1', 'yes', 'on', true], true); return (bool) apply_filters('bizdex_uplisting_property_apply_extra_charges_at_checkout', $enabled, $this); } /** * Whether a synced charge row is enabled for checkout (per-item checkbox). * * @param mixed $value Meta or array value (`apply_at_checkout` key). */ public static function charge_toggle_is_checked($value): bool { if ($value === '' || $value === null) { return true; } return in_array($value, [1, '1', true, 'yes', 'on'], true); } /** * Whether a fee, tax, or discount row is enabled for checkout display. * * @param array $item Fee, tax, or discount row. */ public static function is_charge_item_enabled_at_checkout(array $item): bool { if (!array_key_exists('apply_at_checkout', $item)) { return true; } return self::charge_toggle_is_checked($item['apply_at_checkout']); } /** * Whether the property cleaning fee is included in checkout pricing. */ public function is_cleaning_fee_enabled_at_checkout(): bool { if (!$this->applies_extra_charges_at_checkout()) { return false; } return self::charge_toggle_is_checked($this->get_meta('cleaning_fee_apply_at_checkout')); } /** * Whether the per-booking tax percentage is included in checkout pricing. */ public function is_per_booking_tax_enabled_at_checkout(): bool { if (!$this->applies_extra_charges_at_checkout()) { return false; } return self::charge_toggle_is_checked($this->get_meta('per_booking_percentage_apply_at_checkout')); } /** * Whether the security deposit is included in checkout pricing. */ public function is_security_deposit_enabled_at_checkout(): bool { if (!$this->applies_extra_charges_at_checkout()) { return false; } return self::charge_toggle_is_checked($this->get_meta('security_deposit_apply_at_checkout')); } /** * Whether a charge row applies to the current property at checkout. * * @param array $item Fee, tax, or discount row. */ public function charge_item_applies_at_checkout(array $item): bool { return self::is_charge_item_enabled_at_checkout($item); } /** * Extra fees excluding cleaning, filtered by per-item checkout toggles. * * @return array> */ public function get_checkout_fees(): array { return array_values(array_filter($this->get_fees(), static function ($fee) { if (!is_array($fee)) { return false; } $label = strtolower((string) ($fee['label'] ?? '')); if (in_array($label, ['cleaning_fee', 'cleaning'], true)) { return false; } return self::is_charge_item_enabled_at_checkout($fee); })); } /** * Return tax rows that apply at checkout for this property. * * @return array> */ public function get_checkout_taxes(): array { return array_values(array_filter($this->get_taxes(), static function ($tax) { if (!is_array($tax)) { return false; } $label = strtolower((string) ($tax['label'] ?? '')); if ($label === 'per_booking_percentage') { return false; } return self::is_charge_item_enabled_at_checkout($tax); })); } /** * Return discount rows that apply at checkout for this property. * * @return array> */ public function get_checkout_discounts(): array { return array_values(array_filter($this->get_discounts(), static function ($discount) { return is_array($discount) && self::is_charge_item_enabled_at_checkout($discount); })); } /** * Get the city/locality for this property. * * Meta key: `city`. * * @return string */ public function get_city() { return $this->get_meta('city'); } /** * Get street address line. * * Meta key: `street`. * * @return string */ public function get_street() { return $this->get_meta('street'); } /** * Get suite / unit number. * * Meta key: `suite`. * * @return string */ public function get_suite() { return $this->get_meta('suite'); } /** * Get postal / ZIP code. * * Meta key: `zip_code`. * * @return string */ public function get_zip_code() { return $this->get_meta('zip_code'); } /** * Get maximum guest capacity for listings/filters. * * Meta key: `guests`. Used by archive meta_query filters. * * @return int|string */ public function get_guests() { return $this->get_meta('guests'); } /** * Get person capacity. * * @return int */ public function get_person_capacity() { return (int) $this->get_meta('person_capacity', 0); } /** * Get bedroom count. * * Meta key: `bedrooms`. * * @return int|string */ public function get_bedrooms() { return $this->get_meta('bedrooms'); } /** * Get bathroom count. * * Meta key: `bathrooms`. * * @return int|string */ public function get_bathrooms() { return $this->get_meta('bathrooms'); } /** * Get bed count. * * Meta key: `beds`. * * @return int|string */ public function get_beds() { return $this->get_meta('beds'); } /** * Get latitude coordinate. * * Meta key: `latitude`. * * @return string|float */ public function get_latitude() { return $this->get_meta('latitude'); } /** * Get longitude coordinate. * * Meta key: `longitude`. * * @return string|float */ public function get_longitude() { return $this->get_meta('longitude'); } /** * Get house rules and policy bullet points. * * Meta key: `house_rules_and_policies` (array of strings). * * @return array */ public function get_house_rules() { return $this->get_meta('house_rules_and_policies', []); } /** * Get manually entered property reviews. * * Meta key: `reviews`. Each entry: name, month, year, content. * * @return array> */ public function get_reviews() { return $this->get_meta('reviews', []); } /** * Get which booking UI layout to render on single property. * * Uplisting Connector uses a single WordPress booking form mode. * Multi-mode booking options (inquiry/button/embed) are not offered. * * @return string */ public function get_booking_option() { return (string) apply_filters('bizdex_uplisting_property_booking_option', 'booking_form', $this); } /** * Get raw HTML/JS embed code for external booking widgets. * * Meta key: `booking_form_embed_code` (unsanitized on save). * * @return string */ public function get_booking_form_embed_code() { return $this->get_meta('booking_form_embed_code'); } /** * Get custom CTA buttons ({text, url} pairs) for button-only layout. * * Meta key: `booking_buttons`. * * @return array */ public function get_booking_buttons() { return $this->get_meta('booking_buttons', []); } /** * Get helper text displayed above the booking form. * * Meta key: `booking_form_text`. * * @return string */ public function get_booking_form_text() { return $this->get_meta('booking_form_text'); } /** * Get mobile sticky booking button label. * * Meta key: `mobile_button_text`. Default: `Book now`. * * @return string */ public function get_mobile_button_text() { return $this->get_meta('mobile_button_text', 'Book now'); } /** * Get custom booking button label for inquiry form layout. * * Meta key: `uplisting_button_text`. Default: `Book Now`. * * @return string */ public function get_uplisting_button_text() { return (string) $this->get_meta('uplisting_button_text', 'Book Now'); } /** * Get helper text shown below the inquiry form button. * * Meta key: `uplisting_below_button_text`. * * @return string */ public function get_uplisting_below_button_text() { return (string) $this->get_meta('uplisting_below_button_text', ''); } /** * Get total row label on inquiry pricing breakdown. * * Meta key: `total_text`. Default: `Estimated total *`. * * @return string */ public function get_total_text() { return (string) $this->get_meta('total_text', 'Estimated total *'); } /** * Get disclaimer below inquiry pricing total. * * Meta key: `total_disclaimer_text`. * * @return string */ public function get_total_disclaimer_text() { return (string) $this->get_meta('total_disclaimer_text', 'Pricing and dates subject to availability'); } /** * Registered booking option choices for the property admin UI. * * Filter: uplisting_property_booking_options * * @return array */ public static function get_booking_options(): array { return apply_filters('bizdex_uplisting_property_booking_options', [ 'booking_form' => __('Booking Form (WordPress only)', 'advanced-connector-for-uplisting'), ]); } /** * Map booking option slugs to frontend template parts. * * Filter: uplisting_booking_option_templates * * @return array */ public static function get_booking_option_templates(): array { return apply_filters('bizdex_uplisting_booking_option_templates', [ 'booking_form' => 'single-property/booking-form-default', ]); } /** * Whether this property uses an on-page calendar booking card. */ public function booking_card_uses_calendar_form(): bool { $option = $this->get_booking_option() ?: 'booking_form'; return in_array($option, ['booking_form', 'uplisting_booking_form'], true); } /** * Whether the property booking card should render the pricing breakdown. * * Filter: uplisting_booking_card_show_pricing_breakdown */ public function booking_card_shows_pricing_breakdown(): bool { return (bool) apply_filters( 'bizdex_uplisting_booking_card_show_pricing_breakdown', $this->booking_card_uses_calendar_form(), $this ); } /** * CSS classes for the booking card wrapper element. * * Filter: uplisting_booking_card_classes * * @return string[] */ public function get_booking_card_classes(): array { $option = $this->get_booking_option() ?: 'booking_form'; return apply_filters('bizdex_uplisting_booking_card_classes', [ 'booking-card', 'booking-card--' . sanitize_html_class($option), ], $this); } /** * Primary CTA label for the property booking card. * * Filter: uplisting_property_booking_button_text */ public function get_booking_card_button_text(): string { $default = $this->get_booking_option() === 'uplisting_booking_form' ? $this->get_uplisting_button_text() : __('Book Now', 'advanced-connector-for-uplisting'); return (string) apply_filters('bizdex_uplisting_property_booking_button_text', $default, $this); } /** * Primary CTA classes for the property booking card button. * * Filter: uplisting_booking_card_button_classes * * @return string[] */ public function get_booking_card_button_classes(): array { $classes = ['reserve-btn', 'button-uplisting-primary']; if ($this->get_booking_option() === 'uplisting_booking_form') { $classes[] = 'open_booking_form'; } return apply_filters('bizdex_uplisting_booking_card_button_classes', $classes, $this); } /** * Effective min stay for calendar validation (property meta with fallback). */ public function get_effective_min_stay(): int { $min = (int) $this->get_uplisting_min_stay(); return $min > 0 ? $min : (int) apply_filters('bizdex_uplisting_property_default_min_stay', 1, $this); } /** * Effective max stay for calendar validation (property meta with fallback). */ public function get_effective_max_stay(): int { $max = (int) $this->get_uplisting_max_stay(); return $max > 0 ? $max : (int) apply_filters('bizdex_uplisting_property_default_max_stay', 0, $this); } /** * Get the state or territory. * * @return array|mixed|string */ public function get_state_territory() { return $this->get_meta('state_territory'); } /** * Get the country. * * @return array|mixed|string */ public function get_country() { return $this->get_meta('country'); } /** * Build a single-line mailing address from property meta fields. */ public function get_formatted_address(): string { $parts = []; $street = trim((string) $this->get_street()); $suite = trim((string) $this->get_suite()); if ($street !== '') { $parts[] = $suite !== '' ? $street . ', ' . $suite : $street; } elseif ($suite !== '') { $parts[] = $suite; } $city_line = array_filter([ trim((string) $this->get_city()), trim((string) $this->get_state_territory()), trim((string) $this->get_zip_code()), ], static function ($value) { return $value !== ''; }); if (!empty($city_line)) { $parts[] = implode(', ', $city_line); } $country = trim((string) $this->get_country()); if ($country !== '') { $parts[] = $country; } return implode(', ', $parts); } /** * Featured image URL for listing/summary cards. * * Uses the post thumbnail when set, otherwise the first gallery image * (synced attachment or remote Uplisting URL). */ public function get_featured_image_url(string $size = 'medium_large'): string { $url = get_the_post_thumbnail_url($this->get_id(), $size); if (is_string($url) && $url !== '') { return $url; } $gallery = $this->get_gallery_images(); if (empty($gallery[0]) || !is_array($gallery[0])) { return ''; } $first = $gallery[0]; if (!empty($first['id']) && is_numeric($first['id'])) { $attachment_url = wp_get_attachment_image_url((int) $first['id'], $size); if (is_string($attachment_url) && $attachment_url !== '') { return $attachment_url; } } foreach (['thumb', 'full', 'url'] as $key) { if (!empty($first[$key]) && is_string($first[$key])) { return $first[$key]; } } return ''; } /** * Set the property featured image only when none is already assigned. * * Never overwrites an existing featured image during import/sync. * * @param int $post_id Property post ID. * @param int $attachment_id Media attachment ID. * @return bool True when the featured image was set. */ public static function maybe_set_featured_image(int $post_id, int $attachment_id): bool { $post_id = absint($post_id); $attachment_id = absint($attachment_id); if ($post_id <= 0 || $attachment_id <= 0) { return false; } if (has_post_thumbnail($post_id)) { return false; } if (get_post_type($attachment_id) !== 'attachment') { return false; } return (bool) set_post_thumbnail($post_id, $attachment_id); } /** * Get the Uplisting property name. * * @return array|mixed|string */ public function get_property_name() { return $this->get_meta('property_name'); } /** * Get the Uplisting property source ID. * * @return array|mixed|string */ public function get_property_source_id() { return $this->get_meta('property_source_id'); } /** * Get the property license number. * * @return array|mixed|string */ public function get_property_license() { return $this->get_meta('property_license'); } /** * Get the check-in time. * * @return array|mixed|string */ public function get_check_in_time() { return $this->get_meta('check_in_time'); } /** * Get the reservation cutoff time. * * @return array|mixed|string */ public function get_reservation_time() { return $this->get_meta('reservation_time'); } /** * Get check-in instructions or info text. * * @return array|mixed|string */ public function get_check_in_info() { return $this->get_meta('check_in_info'); } /** * Get the minimum stay requirement from Uplisting. * * @return array|mixed|string */ public function get_uplisting_min_stay() { return $this->get_meta('uplisting_min_stay'); } /** * Get the maximum stay limit from Uplisting. * * @return array|mixed|string */ public function get_uplisting_max_stay() { return $this->get_meta('uplisting_max_stay'); } /** * Get the iCal calendar link for this property. * * @return array|mixed|string */ public function get_ical_link() { return $this->get_meta('ical_link'); } /** * iCal URL for inquiry / button-only booking options. */ public function get_ical_calendar_url(): string { $url = $this->get_meta('ical_calendar_url'); return is_string($url) && $url !== '' ? $url : (string) $this->get_ical_link(); } /** * iCal URL for embed-code booking option. */ public function get_ical_embed_code_url(): string { $url = $this->get_meta('ical_embed_code_url'); return is_string($url) && $url !== '' ? $url : (string) $this->get_ical_link(); } /** * Whether calendar busy dates and live rates come from the Uplisting API. * * Filter: uplisting_property_uses_api_calendar */ public function uses_uplisting_api_for_calendar(): bool { return (bool) apply_filters( 'bizdex_uplisting_property_uses_api_calendar', $this->get_booking_option() === 'booking_form', $this ); } /** * Whether the Calendar Rates UI should render for this property. * * Requires all three: saved Uplisting API key, booking_option === booking_form, * and the "Show more day rates" setting enabled. * * @since 1.0.0 * * @return bool */ public function should_show_calendar_rates(): bool { $api_key = (string) get_option( defined('BIZDEX_UPLISTING_API_KEY') ? BIZDEX_UPLISTING_API_KEY : 'bizdex_uplisting_api_key', '' ); if ($api_key === '') { return false; } if ($this->get_booking_option() !== 'booking_form') { return false; } $show_rates = get_option( defined('BIZDEX_UPLISTING_SHOW_MORE_DAY_RATES') ? BIZDEX_UPLISTING_SHOW_MORE_DAY_RATES : 'bizdex_uplisting_show_more_day_rates', '' ); return in_array($show_rates, [true, 1, '1', 'yes', 'on'], true); } /** * Whether date-range pricing is calculated via the Uplisting API. * * Filter: uplisting_property_uses_api_pricing */ public function uses_uplisting_api_for_pricing(): bool { return (bool) apply_filters( 'bizdex_uplisting_property_uses_api_pricing', $this->get_booking_option() === 'booking_form', $this ); } /** * Whether bookings are stored on this WordPress site only (no Uplisting API booking). * * Used by `uplisting_booking_form` — local CPT entry, confirmation emails, manual follow-up. * Connector has no Uplisting booking-push flow; all form bookings save in WordPress. * * Filter: uplisting_property_creates_local_booking_only */ public function creates_local_site_booking_only(): bool { return (bool) apply_filters( 'bizdex_uplisting_property_creates_local_booking_only', $this->uses_local_booking_fields(), $this ); } /** * Whether pricing uses local property meta (`uplisting_booking_form_fields`). * * Filter: uplisting_property_uses_local_booking_fields */ public function uses_local_booking_fields(): bool { return (bool) apply_filters( 'bizdex_uplisting_property_uses_local_booking_fields', $this->get_booking_option() === 'uplisting_booking_form', $this ); } /** * Synced busy dates for calendar blocking and availability checks. * * `uplisting_booking_form` uses iCal-synced dates only. * `booking_form` prefers API-synced dates with iCal as fallback. * * Filter: uplisting_property_busy_dates * * @return string[] Y-m-d date strings. */ public function get_busy_dates(): array { $ical_busy = bizdex_uplisting_get_post_meta($this->get_id(), 'ical_busy_dates', true); $api_busy = bizdex_uplisting_get_post_meta($this->get_id(), 'api_busy_dates', true); $ical = is_array($ical_busy) ? $ical_busy : []; $api = is_array($api_busy) ? $api_busy : []; if ($this->uses_uplisting_api_for_calendar()) { $busy = array_unique(array_merge($api, $ical)); } else { $busy = $ical; } $busy = array_values(array_filter(array_map('strval', $busy))); return apply_filters('bizdex_uplisting_property_busy_dates', $busy, $this); } /** * Map Uplisting photo IDs to synced WordPress attachment IDs for a property. * * @return array */ public static function get_gallery_attachment_map(int $property_id): array { if ($property_id <= 0) { return []; } $map = []; $candidate_ids = []; $gallery_meta = bizdex_uplisting_get_post_meta($property_id, 'property_gallery_images', true); if (!empty($gallery_meta)) { $candidate_ids = array_merge( $candidate_ids, array_filter(array_map('trim', explode(',', (string) $gallery_meta))) ); } $children = get_posts([ 'post_type' => 'attachment', 'post_parent' => $property_id, 'posts_per_page' => -1, 'post_status' => 'inherit', 'fields' => 'ids', ]); if (!empty($children)) { $candidate_ids = array_merge($candidate_ids, array_map('strval', $children)); } foreach (array_unique($candidate_ids) as $image_id) { if (!is_numeric($image_id)) { continue; } $uplisting_id = (string) bizdex_uplisting_get_post_meta((int) $image_id, 'uplisting_image_id'); if ($uplisting_id !== '') { $map[$uplisting_id] = (int) $image_id; } } return $map; } /** * Find a previously synced attachment for an Uplisting photo ID. */ public static function find_gallery_attachment_id(int $property_id, string $uplisting_id): int { if ($property_id <= 0 || $uplisting_id === '') { return 0; } $map = self::get_gallery_attachment_map($property_id); return $map[$uplisting_id] ?? 0; } /** * Find a synced attachment by its original Uplisting source URL. */ public static function find_gallery_attachment_by_url(int $property_id, string $url): int { if ($property_id <= 0 || $url === '') { return 0; } // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Deduplicate gallery imports by source URL. $existing = get_posts([ 'post_type' => 'attachment', 'post_parent' => $property_id, 'posts_per_page' => 1, 'post_status' => 'inherit', 'fields' => 'ids', 'meta_key' => bizdex_uplisting_meta_key('_uplisting_source_url'), 'meta_value' => esc_url_raw($url), ]); // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.SlowDBQuery.slow_db_query_meta_value return !empty($existing) ? (int) $existing[0] : 0; } /** * Persist ordered attachment IDs when imports exist but gallery meta is empty. * * @param array $remote_photos */ public function repair_gallery_image_meta(array $remote_photos = []): void { if (!$this->get_id()) { return; } if (!empty($remote_photos)) { foreach ($remote_photos as $photo) { if (!is_array($photo)) { continue; } $uplisting_id = (string) ($photo['id'] ?? ''); $url = (string) ($photo['url'] ?? ''); if ($uplisting_id === '' || $url === '') { continue; } $attachment_id = self::find_gallery_attachment_id($this->get_id(), $uplisting_id); if ($attachment_id <= 0) { $attachment_id = self::find_gallery_attachment_by_url($this->get_id(), $url); if ($attachment_id > 0) { update_post_meta($attachment_id, bizdex_uplisting_meta_key('uplisting_image_id'), $uplisting_id); update_post_meta($attachment_id, bizdex_uplisting_meta_key('uplisting_image_order'), (int) ($photo['order'] ?? 999)); } } } } $current_meta = (string) $this->get_meta('property_gallery_images'); if ($current_meta !== '') { return; } $attachment_map = self::get_gallery_attachment_map($this->get_id()); if (empty($attachment_map)) { return; } $ordered_ids = []; if (!empty($remote_photos)) { foreach ($remote_photos as $photo) { if (!is_array($photo)) { continue; } $uplisting_id = (string) ($photo['id'] ?? ''); if ($uplisting_id !== '' && isset($attachment_map[$uplisting_id])) { $ordered_ids[] = (int) $attachment_map[$uplisting_id]; } } } if (empty($ordered_ids)) { $ordered_ids = array_values($attachment_map); } if (!empty($ordered_ids)) { update_post_meta($this->get_id(), bizdex_uplisting_meta_key('property_gallery_images'), implode(',', array_unique($ordered_ids))); } } /** * Build gallery image objects from Uplisting CDN URLs. * * @return array */ public function get_gallery_images() { $images_array = []; $remote_photos = $this->get_remote_gallery_photos(); foreach ($remote_photos as $photo) { if (!is_array($photo)) { continue; } $url = isset($photo['url']) ? (string) $photo['url'] : ''; if ($url === '') { continue; } $uplisting_id = (string) ($photo['id'] ?? ''); $images_array[] = [ 'id' => 'remote-' . ($uplisting_id !== '' ? $uplisting_id : md5($url)), 'url' => $url, 'thumb' => $url, 'full' => $url, 'remote' => true, ]; } if (empty($images_array) && has_post_thumbnail($this->get_id())) { $thumb_id = (int) get_post_thumbnail_id($this->get_id()); $formatted = $this->format_gallery_attachment($thumb_id); if ($formatted) { $images_array[] = $formatted; } } return $images_array; } /** * Remote gallery photo metadata from Uplisting (not yet imported to media). * * @return array */ public function get_remote_gallery_photos(): array { $json = $this->get_meta('property_gallery_remote'); if (empty($json)) { return []; } $decoded = json_decode($json, true); return is_array($decoded) ? $decoded : []; } /** * Map synced remote photo IDs to local attachment IDs. * * @return array */ private function get_synced_gallery_attachments_map(): array { return self::get_gallery_attachment_map($this->get_id()); } /** * Format an attachment ID into gallery image URLs, or null when missing. * * @return array{id: int, url: string, thumb: string, full: string}|null */ private function format_gallery_attachment(int $image_id): ?array { if (!$image_id || !wp_attachment_is_image($image_id)) { return null; } $full = (string) wp_get_attachment_url($image_id); if ($full === '') { return null; } $thumb = (string) wp_get_attachment_image_url($image_id, 'medium_large'); if ($thumb === '') { $thumb = (string) wp_get_attachment_image_url($image_id, 'medium'); } if ($thumb === '') { $thumb = (string) wp_get_attachment_image_url($image_id, 'large'); } if ($thumb === '') { $thumb = $full; } return [ 'id' => $image_id, 'url' => $full, 'thumb' => $thumb, 'full' => $full, ]; } /** * Gallery images for listing card carousel, respecting archive settings. * * @return array */ public function get_carousel_images() { $images = $this->get_gallery_images(); $max = absint(get_option('bizdex_uplisting_gallery_images_count', 10)); if ($max > 0 && count($images) > $max) { $images = array_slice($images, 0, $max); } if (get_option('bizdex_uplisting_enable_carousel', '1') !== '1' && count($images) > 1) { $images = array_slice($images, 0, 1); } return $images; } /** * Get amenity term names assigned to this property. * * Taxonomy: `property_amenities`. Sorted alphabetically by term name. * * @return string[] */ public function get_amenities() { if (!$this->get_id()) { return []; } $amenities_terms = wp_get_object_terms($this->get_id(), 'bizdexul_amenity', ['orderby' => 'name', 'order' => 'ASC']); $amenities_list = []; if (!is_wp_error($amenities_terms) && !empty($amenities_terms)) { foreach ($amenities_terms as $term) { $amenities_list[] = $term->name; } } if (!empty($amenities_list)) { return $amenities_list; } $meta = $this->get_meta('bizdexul_amenity'); if (is_array($meta)) { return array_values(array_filter($meta)); } if (is_string($meta) && '' !== $meta) { return array_values(array_filter(array_map('trim', explode(',', $meta)))); } return []; } /** * List scalar meta field keys saved from the property admin meta box. * * Used by {@see save_meta()} for bulk `update_post_meta()` on text fields. * * @return string[] */ public static function get_basic_fields() { return [ 'street', 'suite', 'city', 'state_territory', 'zip_code', 'country', 'latitude', 'longitude', 'guests', 'bedrooms', 'bathrooms', 'beds', 'property_name', 'property_source_id', 'property_license', 'check_in_time', 'reservation_time', 'check_in_info', 'cleaning_fee', 'per_booking_percentage', 'per_booking_amount', 'booking_option', 'uplisting_min_stay', 'uplisting_max_stay', 'uplisting_price', 'uplisting_cleaning_fee', 'uplisting_button_text', 'uplisting_below_button_text', 'total_text', 'total_disclaimer_text', 'ical_link', 'ical_calendar_url', 'ical_embed_code_url', 'booking_form_text', 'mobile_button_text' ]; } /** * Collect property meta box input from the current request. * * Values are still re-sanitized/validated in {@see save_meta()}. * * @return array */ public static function get_request_meta_input(): array { $data = []; foreach (self::get_basic_fields() as $field) { if (!Request::has('post', $field)) { continue; } if (in_array($field, ['ical_link', 'ical_calendar_url', 'ical_embed_code_url'], true)) { $data[$field] = Request::url('post', $field); } elseif (in_array($field, ['guests', 'bedrooms', 'bathrooms', 'beds', 'uplisting_min_stay', 'uplisting_max_stay'], true)) { $data[$field] = (string) Request::absint('post', $field); } elseif ($field === 'booking_option') { // Connector: fixed WordPress booking form mode (no admin selector). $data[$field] = 'booking_form'; } else { $data[$field] = Request::text('post', $field); } } if (Request::has('post', 'booking_form_embed_code')) { $data['booking_form_embed_code'] = Request::kses( 'post', 'booking_form_embed_code', self::get_booking_embed_allowed_html() ); } if (Request::has('post', 'booking_buttons')) { $data['booking_buttons'] = Request::array('post', 'booking_buttons'); } if (Request::has('post', 'apply_extra_charges_at_checkout')) { $data['apply_extra_charges_at_checkout'] = Request::bool('post', 'apply_extra_charges_at_checkout') ? '1' : '0'; } if (Request::has('post', 'property_gallery_images')) { $data['property_gallery_images'] = Request::text('post', 'property_gallery_images'); } foreach ([ 'cleaning_fee_apply_at_checkout', 'per_booking_percentage_apply_at_checkout', 'security_deposit_apply_at_checkout', 'security_deposit_enabled', ] as $flag) { $data[$flag] = Request::has('post', $flag) && Request::bool('post', $flag) ? '1' : '0'; } foreach (['uplisting_fees', 'uplisting_taxes', 'uplisting_discounts'] as $charge_group) { if (Request::has('post', $charge_group)) { $data[$charge_group] = Request::array('post', $charge_group); } } return $data; } /** * Allowed HTML for booking embed widgets (iframe embeds, no scripts). * * @return array> */ public static function get_booking_embed_allowed_html(): array { $allowed = wp_kses_allowed_html('post'); $allowed['iframe'] = [ 'src' => true, 'width' => true, 'height' => true, 'style' => true, 'class' => true, 'id' => true, 'name' => true, 'frameborder' => true, 'allow' => true, 'allowfullscreen' => true, 'loading' => true, 'referrerpolicy' => true, 'title' => true, 'sandbox' => true, ]; $allowed['div']['data-*'] = true; $allowed['span']['data-*'] = true; /** * Filter allowed HTML tags for booking form embed code. * * @param array> $allowed */ return (array) apply_filters('bizdex_uplisting_booking_embed_allowed_html', $allowed); } /** * Sanitize booking form embed markup for storage/output. */ public static function sanitize_booking_form_embed_code(string $embed_code, int $post_id = 0): string { $embed_code = wp_unslash($embed_code); /** * Filter booking form embed markup before it is stored or printed. * * @param string $embed_code * @param int $post_id */ $embed_code = (string) apply_filters('bizdex_uplisting_sanitize_booking_form_embed_code', $embed_code, $post_id); return wp_kses($embed_code, self::get_booking_embed_allowed_html()); } /** * Persist property meta box POST data to post meta. * * Saves basic fields, booking buttons, extra charges, and gallery meta. * Called from {@see PostTypes::save_meta_boxes()}. * * @param int $post_id Property post ID. * @param array $post_data Sanitized request payload (see get_request_meta_input()). * @return void */ public static function save_meta($post_id, $post_data) { $post_id = absint($post_id); if ($post_id <= 0 || !is_array($post_data)) { return; } /** * Filter raw property meta POST data before save. * * @param array $post_data * @param int $post_id */ $post_data = apply_filters('bizdex_uplisting_before_save_property_meta', $post_data, $post_id); if (!is_array($post_data)) { return; } /** * Fires before property meta is written. * * @param int $post_id * @param array $post_data */ do_action('bizdex_uplisting_before_save_property_meta', $post_id, $post_data); $url_fields = ['ical_link', 'ical_calendar_url', 'ical_embed_code_url']; $int_fields = ['guests', 'bedrooms', 'bathrooms', 'beds', 'uplisting_min_stay', 'uplisting_max_stay']; $float_fields = ['latitude', 'longitude', 'per_booking_amount', 'uplisting_price']; $import_only_amount_fields = ['cleaning_fee', 'per_booking_percentage', 'uplisting_cleaning_fee']; $booking_options = array_keys(self::get_booking_options()); foreach (self::get_basic_fields() as $field) { if (!isset($post_data[$field])) { continue; } if (in_array($field, $import_only_amount_fields, true)) { continue; } $raw_value = is_string($post_data[$field]) ? wp_unslash($post_data[$field]) : $post_data[$field]; if (in_array($field, $url_fields, true)) { $value = esc_url_raw((string) $raw_value); } elseif ($field === 'booking_option') { // Connector: fixed WordPress booking form mode (no admin selector). $value = 'booking_form'; } elseif (in_array($field, $int_fields, true)) { $value = absint($raw_value); } elseif (in_array($field, $float_fields, true)) { $value = is_numeric($raw_value) ? (string) (float) $raw_value : ''; } else { $value = sanitize_text_field((string) $raw_value); } update_post_meta($post_id, bizdex_uplisting_meta_key($field), $value); } if (isset($post_data['booking_form_embed_code'])) { $embed_code = is_string($post_data['booking_form_embed_code']) ? $post_data['booking_form_embed_code'] : ''; update_post_meta($post_id, bizdex_uplisting_meta_key('booking_form_embed_code'), self::sanitize_booking_form_embed_code($embed_code, $post_id)); } // Save booking buttons if (isset($post_data['booking_buttons']) && is_array($post_data['booking_buttons'])) { $booking_buttons = []; foreach ($post_data['booking_buttons'] as $button) { if (!is_array($button)) { continue; } $text = isset($button['text']) ? sanitize_text_field(wp_unslash((string) $button['text'])) : ''; $url = isset($button['url']) ? esc_url_raw(wp_unslash((string) $button['url'])) : ''; if ($text !== '' && $url !== '') { $booking_buttons[] = [ 'text' => $text, 'url' => $url, ]; } } update_post_meta($post_id, bizdex_uplisting_meta_key('booking_buttons'), $booking_buttons); } else { delete_post_meta($post_id, bizdex_uplisting_meta_key('booking_buttons')); } update_post_meta($post_id, bizdex_uplisting_meta_key('apply_extra_charges_at_checkout'), !empty($post_data['apply_extra_charges_at_checkout']) ? '1' : '0' ); self::save_extra_charges_meta($post_id, $post_data); // Save gallery images if (isset($post_data['property_gallery_images'])) { $gallery_images = sanitize_text_field(wp_unslash((string) $post_data['property_gallery_images'])); update_post_meta($post_id, bizdex_uplisting_meta_key('property_gallery_images'), $gallery_images); } else { delete_post_meta($post_id, bizdex_uplisting_meta_key('property_gallery_images')); } /** * Fires after property meta has been written. * * @param int $post_id * @param array $post_data */ do_action('bizdex_uplisting_after_save_property_meta', $post_id, $post_data); } /** * Persist Extra Charges enable/disable flags from the property admin meta box. * * Amounts, names, and types stay as imported Uplisting values. * * @param int $post_id * @param array $post_data */ public static function save_extra_charges_meta($post_id, array $post_data): void { $cleaning_on = !empty($post_data['cleaning_fee_apply_at_checkout']) ? '1' : '0'; $tax_on = !empty($post_data['per_booking_percentage_apply_at_checkout']) ? '1' : '0'; $deposit_on = !empty($post_data['security_deposit_apply_at_checkout']) ? '1' : '0'; update_post_meta($post_id, bizdex_uplisting_meta_key('cleaning_fee_apply_at_checkout'), $cleaning_on); update_post_meta($post_id, bizdex_uplisting_meta_key('per_booking_percentage_apply_at_checkout'), $tax_on); update_post_meta($post_id, bizdex_uplisting_meta_key('security_deposit_apply_at_checkout'), $deposit_on); $security_deposit = bizdex_uplisting_get_post_meta($post_id, 'uplisting_security_deposit', true); if (!is_array($security_deposit)) { $security_deposit = []; } $security_deposit['enabled'] = !empty($post_data['security_deposit_enabled']) ? 1 : 0; update_post_meta($post_id, bizdex_uplisting_meta_key('uplisting_security_deposit'), $security_deposit); self::save_charge_row_checkout_toggles( $post_id, 'uplisting_fees', $post_data['uplisting_fees'] ?? [], ['cleaning_fee', 'cleaning'] ); self::save_charge_row_checkout_toggles( $post_id, 'uplisting_taxes', $post_data['uplisting_taxes'] ?? [], ['per_booking_percentage'] ); self::save_charge_row_checkout_toggles( $post_id, 'uplisting_discounts', $post_data['uplisting_discounts'] ?? [], [] ); self::set_charge_row_checkout_toggle_by_label( $post_id, 'uplisting_fees', ['cleaning_fee', 'cleaning'], $cleaning_on === '1' ); self::set_charge_row_checkout_toggle_by_label( $post_id, 'uplisting_taxes', ['per_booking_percentage'], $tax_on === '1' ); } /** * Apply checkout toggles to existing fee/tax/discount rows without changing amounts. * * @param int $post_id * @param string $meta_key Unprefixed meta key. * @param mixed $posted Posted row toggles keyed by original index. * @param array $skip_labels Rows controlled by a dedicated checkbox. */ private static function save_charge_row_checkout_toggles(int $post_id, string $meta_key, $posted, array $skip_labels): void { $rows = bizdex_uplisting_get_post_meta($post_id, $meta_key, true); if (!is_array($rows)) { return; } $posted = is_array($posted) ? $posted : []; foreach ($rows as $index => $row) { if (!is_array($row)) { continue; } $label = strtolower((string) ($row['label'] ?? '')); if (in_array($label, $skip_labels, true)) { continue; } $posted_row = []; if (isset($posted[$index]) && is_array($posted[$index])) { $posted_row = $posted[$index]; } elseif (isset($posted[(string) $index]) && is_array($posted[(string) $index])) { $posted_row = $posted[(string) $index]; } $rows[$index]['apply_at_checkout'] = !empty($posted_row['apply_at_checkout']) ? 1 : 0; } update_post_meta($post_id, bizdex_uplisting_meta_key($meta_key), $rows); } /** * Set apply_at_checkout on stored rows that match a label, without changing amounts. * * @param int $post_id * @param string $meta_key Unprefixed meta key. * @param array $labels * @param bool $enabled */ private static function set_charge_row_checkout_toggle_by_label(int $post_id, string $meta_key, array $labels, bool $enabled): void { $rows = bizdex_uplisting_get_post_meta($post_id, $meta_key, true); if (!is_array($rows)) { return; } $changed = false; foreach ($rows as $index => $row) { if (!is_array($row)) { continue; } $label = strtolower((string) ($row['label'] ?? '')); if (!in_array($label, $labels, true)) { continue; } $rows[$index]['apply_at_checkout'] = $enabled ? 1 : 0; $changed = true; } if ($changed) { update_post_meta($post_id, bizdex_uplisting_meta_key($meta_key), $rows); } } /** * Sanitize fee, tax, or discount rows from settings or meta input. * * @param mixed $rows * @param string $kind fee|tax|discount * @return array> */ public static function sanitize_charge_rows($rows, string $kind): array { if (!is_array($rows)) { return []; } $sanitized = []; foreach ($rows as $row) { if (!is_array($row)) { continue; } $name = sanitize_text_field(wp_unslash((string) ($row['name'] ?? ''))); $label = sanitize_text_field(wp_unslash((string) ($row['label'] ?? ''))); $amount = isset($row['amount']) ? floatval(wp_unslash((string) $row['amount'])) : 0.0; $type = sanitize_text_field(wp_unslash((string) ($row['type'] ?? 'fixed'))); $type = strtolower($type) === 'percentage' ? 'percentage' : 'fixed'; if ($name === '' && $label === '' && $amount <= 0) { continue; } $item = [ 'name' => $name, 'label' => $label, 'amount' => $amount, 'type' => $type, 'apply_at_checkout' => !empty($row['apply_at_checkout']) ? 1 : 0, ]; if ($kind === 'discount') { $item['days'] = isset($row['days']) ? absint(wp_unslash((string) $row['days'])) : 0; } $sanitized[] = $item; } return $sanitized; } /** * Keep the cleaning fee row in `uplisting_fees` meta in sync with property settings. */ private static function sync_cleaning_fee_in_fees_meta(int $post_id, float $cleaning_fee): void { $fees = bizdex_uplisting_get_post_meta($post_id, 'uplisting_fees', true); if (!is_array($fees)) { $fees = []; } $found = false; foreach ($fees as $index => $fee) { if (!is_array($fee)) { continue; } $label = strtolower((string) ($fee['label'] ?? '')); if (!in_array($label, ['cleaning_fee', 'cleaning'], true)) { continue; } $fees[$index]['amount'] = $cleaning_fee; $fees[$index]['name'] = $fees[$index]['name'] ?? __('Cleaning fee', 'advanced-connector-for-uplisting'); $fees[$index]['label'] = 'cleaning_fee'; $found = true; break; } if (!$found && $cleaning_fee > 0) { $fees[] = [ 'name' => __('Cleaning fee', 'advanced-connector-for-uplisting'), 'label' => 'cleaning_fee', 'amount' => $cleaning_fee, 'type' => 'fixed', 'apply_at_checkout' => bizdex_uplisting_get_post_meta($post_id, 'cleaning_fee_apply_at_checkout', true) !== '0' ? 1 : 0, ]; } update_post_meta($post_id, bizdex_uplisting_meta_key('uplisting_fees'), $fees); } /** * Keep the per-booking tax row in `uplisting_taxes` meta in sync with property settings. */ private static function sync_per_booking_tax_in_taxes_meta(int $post_id, float $percentage): void { $taxes = bizdex_uplisting_get_post_meta($post_id, 'uplisting_taxes', true); if (!is_array($taxes)) { $taxes = []; } $found = false; foreach ($taxes as $index => $tax) { if (!is_array($tax)) { continue; } $label = strtolower((string) ($tax['label'] ?? '')); if ($label !== 'per_booking_percentage') { continue; } $taxes[$index]['amount'] = $percentage; $taxes[$index]['type'] = 'percentage'; $found = true; break; } if (!$found && $percentage > 0) { $taxes[] = [ 'name' => __('Per booking percentage', 'advanced-connector-for-uplisting'), 'label' => 'per_booking_percentage', 'amount' => $percentage, 'type' => 'percentage', 'apply_at_checkout' => bizdex_uplisting_get_post_meta($post_id, 'per_booking_percentage_apply_at_checkout', true) !== '0' ? 1 : 0, ]; } update_post_meta($post_id, bizdex_uplisting_meta_key('uplisting_taxes'), $taxes); } /** * Preserve per-item checkout toggles when re-importing from Uplisting. * * @param array> $incoming * @param array> $existing * @return array> */ public static function merge_charge_rows_apply_flags(array $incoming, array $existing): array { if (empty($existing)) { foreach ($incoming as $index => $row) { if (is_array($row) && !array_key_exists('apply_at_checkout', $row)) { $incoming[$index]['apply_at_checkout'] = 1; } } return $incoming; } $existing_map = []; foreach ($existing as $row) { if (!is_array($row)) { continue; } $key = strtolower((string) ($row['label'] ?? $row['name'] ?? '')); if ($key !== '') { $existing_map[$key] = $row; } } foreach ($incoming as $index => $row) { if (!is_array($row)) { continue; } $key = strtolower((string) ($row['label'] ?? $row['name'] ?? '')); if ($key !== '' && isset($existing_map[$key]['apply_at_checkout'])) { $incoming[$index]['apply_at_checkout'] = $existing_map[$key]['apply_at_checkout']; } elseif (!array_key_exists('apply_at_checkout', $row)) { $incoming[$index]['apply_at_checkout'] = 1; } } return $incoming; } /** * Mirror Uplisting import charge data into checkout pricing meta. * * Import stores `cleaning_fee`, `tax_percentage`, fee/tax/discount rows, and * enables checkout extra charges when the API payload includes any of them. * * @param int $post_id * @param array $property_data Normalized Uplisting property payload. */ public static function sync_imported_pricing_charges(int $post_id, array $property_data): void { $cleaning_fee = isset($property_data['cleaning_fee']) && is_numeric($property_data['cleaning_fee']) ? (float) $property_data['cleaning_fee'] : 0.0; $tax_percentage = isset($property_data['tax_percentage']) && is_numeric($property_data['tax_percentage']) ? (float) $property_data['tax_percentage'] : 0.0; update_post_meta($post_id, bizdex_uplisting_meta_key('cleaning_fee'), $cleaning_fee); update_post_meta($post_id, bizdex_uplisting_meta_key('uplisting_cleaning_fee'), $cleaning_fee); update_post_meta($post_id, bizdex_uplisting_meta_key('per_booking_percentage'), $tax_percentage); update_post_meta($post_id, bizdex_uplisting_meta_key('uplisting_tax_percentage'), $tax_percentage); if ($cleaning_fee > 0) { update_post_meta($post_id, bizdex_uplisting_meta_key('cleaning_fee_apply_at_checkout'), '1'); self::sync_cleaning_fee_in_fees_meta($post_id, $cleaning_fee); } if ($tax_percentage > 0) { update_post_meta($post_id, bizdex_uplisting_meta_key('per_booking_percentage_apply_at_checkout'), '1'); self::sync_per_booking_tax_in_taxes_meta($post_id, $tax_percentage); } $has_charges = $cleaning_fee > 0 || $tax_percentage > 0 || !empty($property_data['fees']) || !empty($property_data['taxes']) || !empty($property_data['discounts']); if ($has_charges) { update_post_meta($post_id, bizdex_uplisting_meta_key('apply_extra_charges_at_checkout'), '1'); } } }