HEX
Server: Apache/2.4.65 (Debian)
System: Linux kubikelcreative 5.10.0-35-amd64 #1 SMP Debian 5.10.237-1 (2025-05-19) x86_64
User: www-data (33)
PHP: 8.4.13
Disabled: NONE
Upload Files
File: /var/www/indoadvisory/wp/wp-content/plugins/polylang-wc/plugins/bookings.php
<?php
/**
 * @package Polylang-WC
 */

/**
 * Manages the compatibility with WooCommerce Bookings.
 * Version tested: 1.10.11.
 *
 * @since 0.6
 */
class PLLWC_Bookings {
	/**
	 * Stores if the locale has been switched.
	 *
	 * @var bool
	 */
	private $switched_locale;

	/**
	 * Constructor.
	 * Setups actions and filters.
	 *
	 * @since 0.6
	 */
	public function __construct() {
		// Post types.
		add_filter( 'pll_get_post_types', array( $this, 'translate_types' ), 10, 2 );

		if ( PLL() instanceof PLL_Admin ) {
			add_action( 'wp_loaded', array( $this, 'custom_columns' ), 20 );
			add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ), 20 );
		}

		// Bookings.
		$statuses = array(
			'unpaid',
			'pending-confirmation',
			'confirmed',
			'paid',
			'complete',
			'in-cart',
			'cancelled',
		);

		foreach ( $statuses as $status ) {
			add_action( 'woocommerce_booking_' . $status, array( $this, 'before_booking_metabox_save' ) );
		}
		add_action( 'woocommerce_booking_process_meta', array( $this, 'after_booking_metabox_save' ) );

		// Create booking.
		add_action( 'woocommerce_new_booking', array( $this, 'new_booking' ), 1 );

		// Hooked just before WC_Booking_Cart_Manager::order_item_meta.
		add_action( 'woocommerce_new_order_item', array( $this, 'set_booking_language_at_checkout' ), 49, 2 );

		// Products.
		add_action( 'pllwc_copy_product', array( $this, 'copy_resources' ), 10, 3 );
		add_action( 'pllwc_copy_product', array( $this, 'copy_persons' ), 10, 3 );
		add_action( 'wp_ajax_woocommerce_remove_bookable_resource', array( $this, 'remove_bookable_resource' ), 5 ); // Before WooCommerce Bookings.
		add_action( 'wp_ajax_woocommerce_unlink_bookable_person', array( $this, 'unlink_bookable_person' ), 5 ); // Before WooCommerce Bookings.

		add_action( 'pll_save_post', array( $this, 'save_post' ), 10, 3 );
		add_filter( 'update_post_metadata', array( $this, 'update_post_metadata' ), 99, 4 ); // After Yoast SEO which returns null at priority 10. See https://github.com/Yoast/wordpress-seo/pull/6902.
		add_filter( 'get_post_metadata', array( $this, 'get_post_metadata' ), 10, 4 );
		add_filter( 'pll_copy_post_metas', array( $this, 'copy_post_metas' ) );
		add_filter( 'pll_translate_post_meta', array( $this, 'translate_post_meta' ), 10, 3 );
		add_filter( 'pll_post_metas_to_export', array( $this, 'get_metas_to_translate' ), 10 );

		// Cart.
		add_filter( 'pllwc_translate_cart_item', array( $this, 'translate_cart_item' ), 10, 2 );
		add_filter( 'pllwc_add_cart_item_data', array( $this, 'add_cart_item_data' ), 10, 2 );


		// Add e-mails for translation.
		add_filter( 'pllwc_order_email_actions', array( $this, 'filter_order_email_actions' ) );

		if ( version_compare( $GLOBALS['wp_version'], '6.7-beta' ) < 0 ) {
			// Backward compatibility with WP < 6.7.
			add_action( 'change_locale', array( $this, 'change_locale' ) );
		}

		add_action( 'parse_query', array( $this, 'filter_bookings_notifications' ) );

		// Endpoints in emails.
		if ( isset( PLL()->translate_slugs ) ) {
			add_action( 'pllwc_email_language', array( PLL()->translate_slugs->slugs_model, 'init_translated_slugs' ) );
		}

		// Bookings endpoint.
		add_filter( 'pll_translation_url', array( $this, 'pll_translation_url' ), 10, 2 );
		add_filter( 'woocommerce_get_query_vars', array( $this, 'get_endpoints_query_vars' ) );

		if ( PLL() instanceof PLL_Frontend ) {
			add_action( 'parse_query', array( $this, 'parse_query' ), 3 ); // Before Polylang (for orders).
		}
	}

	/**
	 * Add Bookings e-mails in the translation mechanism.
	 *
	 * @since 1.6
	 *
	 * @param string[] $actions Array of actions used to send emails.
	 * @return string[]
	 */
	public function filter_order_email_actions( $actions ) {
		return array_merge(
			$actions,
			array(
				// Cancelled booking.
				'woocommerce_booking_pending-confirmation_to_cancelled_notification',
				'woocommerce_booking_confirmed_to_cancelled_notification',
				'woocommerce_booking_paid_to_cancelled_notification',
				// Booking confirmed.
				'woocommerce_booking_confirmed_notification',
				// Pending confirmation.
				'woocommerce_booking_pending-confirmation_notification',
				// Reminder.
				'wc-booking-reminder',
				// New booking.
				'woocommerce_new_booking_notification',
				'woocommerce_admin_new_booking_notification',
			)
		);
	}

	/**
	 * Language and translation management for custom post types.
	 * Hooked to the filter 'pll_get_post_types'.
	 *
	 * @since 0.6
	 *
	 * @param array $types List of post type names for which Polylang manages language and translations.
	 * @param bool  $hide  True when displaying the list in Polylang settings.
	 * @return array List of post type names for which Polylang manages language and translations.
	 */
	public function translate_types( $types, $hide ) {
		$wc_bookings_types = array( 'bookable_resource', 'bookable_person', 'wc_booking' );
		return $hide ? array_diff( $types, $wc_bookings_types ) : array_merge( $types, $wc_bookings_types );
	}

	/**
	 * Removes the standard languages columns for bookings
	 * and replaces them with one unique column as for orders.
	 * Hooked to the action 'wp_loaded'.
	 *
	 * @since 0.6
	 *
	 * @return void
	 */
	public function custom_columns() {
		remove_filter( 'manage_edit-wc_booking_columns', array( PLL()->filters_columns, 'add_post_column' ), 100 );
		remove_action( 'manage_wc_booking_posts_custom_column', array( PLL()->filters_columns, 'post_column' ), 10, 2 );

		add_filter( 'manage_edit-wc_booking_columns', array( PLLWC()->admin_orders, 'add_order_column' ), 100 );
		add_action( 'manage_wc_booking_posts_custom_column', array( PLLWC()->admin_orders, 'order_column' ), 10, 2 );

		// FIXME add a filter in PLLWC for the position of the column?
	}

	/**
	 * Removes the language metabox for bookings.
	 * Hooked to the action 'add_meta_boxes'.
	 *
	 * @since 0.6
	 *
	 * @param string $post_type Post type.
	 * @return void
	 */
	public function add_meta_boxes( $post_type ) {
		if ( 'wc_booking' === $post_type ) {
			remove_meta_box( 'ml_box', $post_type, 'side' ); // Removes the Polylang metabox.
		}
	}

	/**
	 * Reloads Bookings translations.
	 * Used for emails and the workaround for localized bookings meta keys.
	 * Hooked to the action 'change_locale'.
	 *
	 * @since 1.0
	 *
	 * @return void
	 */
	public function change_locale() {
		load_plugin_textdomain( 'woocommerce-bookings', false, plugin_basename( __DIR__ ) . '/languages' );
	}

	/**
	 * Reloads the WooCommerce Bookings and WP text domains to work around localized bookings meta.
	 * Hooked to the actions 'woocommerce_booking_{$status}'.
	 *
	 * @since 0.6
	 *
	 * @param int $post_id Booking ID.
	 * @return void
	 */
	public function before_booking_metabox_save( $post_id ) {
		if ( isset( $_POST['post_type'], $_POST['wc_bookings_details_meta_box_nonce'] ) && 'wc_booking' === $_POST['post_type'] ) {  // phpcs:ignore WordPress.Security.NonceVerification
			$booking_locale = pll_get_post_language( $post_id, 'locale' );
			$this->switched_locale = switch_to_locale( $booking_locale );
		}
	}

	/**
	 * Reloads the WooCommerce Bookings and WP text domains to work around localized bookings meta.
	 * Part of the workaround for localized bookings meta keys.
	 * Hooked to the action 'woocommerce_booking_process_meta'.
	 *
	 * @since 0.6
	 *
	 * @return void
	 */
	public function after_booking_metabox_save() {
		if ( $this->switched_locale ) {
			unset( $this->switched_locale );
			restore_previous_locale();
		}
	}

	/**
	 * Assigns the booking and order languages when creating a new booking from the backend.
	 * Hooked to the action 'woocommerce_new_booking'.
	 *
	 * @since 0.6
	 *
	 * @param int $booking_id Booking ID.
	 * @return void
	 */
	public function new_booking( $booking_id ) {
		$data_store = PLLWC_Data_Store::load( 'product_language' );

		$booking = get_wc_booking( $booking_id );
		$lang    = $data_store->get_language( $booking->product_id );
		pll_set_post_language( $booking->id, $lang );

		if ( ! empty( $booking->order_id ) ) {
			$data_store = PLLWC_Data_Store::load( 'order_language' );
			$data_store->set_language( $booking->order_id, $lang );
		}
	}

	/**
	 * Assigns the right booking language.
	 * In case a visitor adds the product to cart in a language
	 * and then switches the language before he completes the checkout.
	 * Hooked to the action 'woocommerce_new_order_item'.
	 *
	 * @since 1.9
	 *
	 * @param int                 $item_id     An order item ID.
	 * @param WC_Order_Item|false $order_item  Order item object.
	 * @return int
	 */
	public function set_booking_language_at_checkout( $item_id, $order_item ) {
		if ( empty( $order_item->legacy_values ) || ! is_array( $order_item->legacy_values ) || empty( $order_item->legacy_values['booking'] ) ) {
			return $item_id;
		}
		$booking_id = $order_item->legacy_values['booking']['_booking_id'];

		$lang = pll_current_language();
		if ( ! empty( $lang ) && pll_get_post_language( $booking_id ) !== $lang ) {
			pll_set_post_language( $booking_id, $lang );
		}

		return $item_id;
	}

	/**
	 * Copies or synchronizes bookable posts (resource, person).
	 *
	 * @since 0.6
	 *
	 * @param array  $post Bookable post to copy (person or resource).
	 * @param int    $to   ID of the product to which we paste information.
	 * @param string $lang Language slug.
	 * @return int Translated bookable post.
	 */
	protected function copy_bookable_post( $post, $to, $lang ) {
		$id = $post['ID'];
		$tr_id = pll_get_post( $id, $lang );

		if ( $tr_id ) {
			// If the translated bookable_person already exists, make sure it has the right post_parent.
			$post = get_post( $tr_id );
			if ( $post->post_parent !== $to ) {
				wp_update_post( array( 'ID' => $tr_id, 'post_parent' => $to ) );
			}
		} else {
			// Creates the bookable_resource if it does not exist yet.
			$post['ID']          = null;
			$post['post_parent'] = $to;
			$tr_id               = wp_insert_post( $post );
			pll_set_post_language( $tr_id, $lang );

			$translations = pll_get_post_translations( $id );
			$translations[ pll_get_post_language( $id ) ] = $id; // In case this is the first translation created.
			$translations[ $lang ] = $tr_id;
			pll_save_post_translations( $translations );
		}

		// Synchronize metas.
		PLL()->sync->post_metas->copy( $id, $tr_id, $lang );

		return $tr_id;
	}

	/**
	 * Copies or synchronizes resources.
	 * Hooked to the action 'pllwc_copy_product'.
	 *
	 * @since 0.6
	 *
	 * @param int    $from ID of the product from which we copy information.
	 * @param int    $to   ID of the product to which we paste information.
	 * @param string $lang Language slug.
	 * @return void
	 */
	public function copy_resources( $from, $to, $lang ) {
		global $wpdb;
		$relationships = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}wc_booking_relationships WHERE product_id = %d", $from ), ARRAY_A );

		foreach ( $relationships as $relationship ) {
			$resource       = get_post( $relationship['resource_id'], ARRAY_A ); // wp_insert_post() expects an array.
			$tr_resource_id = $this->copy_bookable_post( $resource, $to, $lang );
			if ( ! $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}wc_booking_relationships WHERE product_id = %d AND resource_id = %d", $to, $tr_resource_id ) ) ) {
				unset( $relationship['ID'] );
				$relationship['product_id']  = $to;
				$relationship['resource_id'] = $tr_resource_id;
				$wpdb->insert( "{$wpdb->prefix}wc_booking_relationships", $relationship );

				if ( class_exists( WC_Bookings_Cache::class ) ) {
					WC_Bookings_Cache::delete_booking_resources_transient( $to );
				}
			}
		}
	}

	/**
	 * Copies or synchronizes persons types.
	 * Hooked to the action 'pllwc_copy_product'.
	 *
	 * @since 0.6
	 *
	 * @param int    $from ID of the product from which we copy information.
	 * @param int    $to   ID of the product to which we paste information.
	 * @param string $lang Language slug.
	 * @return void
	 */
	public function copy_persons( $from, $to, $lang ) {
		// phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.get_posts_get_children
		$persons = get_children(
			array(
				'post_parent' => $from,
				'post_type'   => 'bookable_person',
				'lang'        => '',
			),
			ARRAY_A // wp_insert_post() expects an array.
		);

		foreach ( $persons as $post ) {
			$this->copy_bookable_post( $post, $to, $lang );
		}
	}

	/**
	 * Removes resources in translated products when a resource is removed in Ajax.
	 * Hooked to the action 'wp_ajax_woocommerce_remove_bookable_resource'.
	 *
	 * @since 0.6
	 *
	 * @return void
	 */
	public function remove_bookable_resource() {
		global $wpdb;

		check_ajax_referer( 'delete-bookable-resource', 'security' );

		if ( isset( $_POST['post_id'], $_POST['resource_id'] ) ) {
			$product_id  = absint( $_POST['post_id'] );
			$resource_id = absint( $_POST['resource_id'] );

			$data_store = PLLWC_Data_Store::load( 'product_language' );

			foreach ( $data_store->get_translations( $product_id ) as $lang => $tr_id ) {
				if ( $tr_id !== $product_id ) { // Let WooCommerce delete the current relationship.
					$tr_resource_id = pll_get_post( $resource_id, $lang );

					$wpdb->delete(
						"{$wpdb->prefix}wc_booking_relationships",
						array(
							'product_id'  => $tr_id,
							'resource_id' => $tr_resource_id,
						)
					);
				}
			}
		}
	}

	/**
	 * Unlinks the person type in translated products when a person type is unlink in Ajax.
	 * Hooked to the action 'wp_ajax_woocommerce_unlink_bookable_person'.
	 *
	 * @since 0.6
	 *
	 * @return void
	 */
	public function unlink_bookable_person() {
		check_ajax_referer( 'unlink-bookable-person', 'security' );

		if ( isset( $_POST['person_id'] ) ) {
			$person_type_id = intval( $_POST['person_id'] );
			$person_type    = get_post( $person_type_id );

			if ( $person_type && 'bookable_person' === $person_type->post_type ) {
				foreach ( pll_get_post_translations( $person_type_id ) as $tr_id ) {
					if ( $tr_id !== $person_type_id ) { // Let WooCommerce unlink the current person type.
						$tr_person_type = new WC_Product_Booking_Person_Type( $tr_id );
						$tr_person_type->set_parent_id( 0 );
						$tr_person_type->save();
					}
				}
			}
		}
	}

	/**
	 * Add bookings metas when creating a new product or resource.
	 *
	 * @since 0.9.3
	 *
	 * @param int    $post_id      New product or resource.
	 * @param array  $translations Existing product or resource translations.
	 * @param string $meta_key     Meta to add to the booking.
	 * @return void
	 */
	protected function add_metas_to_booking( $post_id, $translations, $meta_key ) {
		global $wpdb;

		if ( ! empty( $translations ) ) { // If there is no translation, the query returns all bookings!
			$query_translations = new WP_Query(
				array(
					'fields'      => 'ids',
					'post_type'   => 'wc_booking',
					'numberposts' => -1,
					'nopaging'    => true, // phpcs:ignore WordPressVIPMinimum.Performance.NoPaging.nopaging_nopaging
					'lang'        => '',
					'meta_query'  => array(
						array(
							'key'     => $meta_key,
							'value'   => $translations,
							'compare' => 'IN',
						),
					),
				)
			);

			$query_current = new WP_Query(
				array(
					'fields'      => 'ids',
					'post_type'   => 'wc_booking',
					'numberposts' => -1,
					'nopaging'    => true, // phpcs:ignore WordPressVIPMinimum.Performance.NoPaging.nopaging_nopaging
					'lang'        => '',
					'meta_query'  => array(
						array(
							'key'     => $meta_key,
							'value'   => array( $post_id ),
							'compare' => 'IN',
						),
					),
				)
			);

			$booking_ids = array_diff( $query_translations->posts, $query_current->posts );

			if ( ! empty( $booking_ids ) ) {
				$values = array();

				foreach ( $booking_ids as $booking ) {
					$values[] = $wpdb->prepare( '( %d, %s, %d )', $booking, $meta_key, $post_id );
				}

				$wpdb->query( "INSERT INTO {$wpdb->postmeta} ( post_id, meta_key, meta_value ) VALUES " . implode( ',', $values ) ); // // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
			}
		}
	}

	/**
	 * Updates the bookings associated to the translated products (or resource)
	 * when creating a new product (or resource translation).
	 * Hooked to the action 'pll_save_post'.
	 *
	 * @since 0.9.3
	 *
	 * @param int     $post_id      Post id.
	 * @param WP_Post $post         Post object.
	 * @param array   $translations Post translations.
	 * @return void
	 */
	public function save_post( $post_id, $post, $translations ) {
		$translations = array_diff( $translations, array( $post_id ) );

		if ( 'product' === $post->post_type ) {
			$this->add_metas_to_booking( $post_id, $translations, '_booking_product_id' );
		}

		if ( 'bookable_resource' === $post->post_type ) {
			$this->add_metas_to_booking( $post_id, $translations, '_booking_resource_id' );
		}
	}

	/**
	 * Allows to associate several products or resources to a booking.
	 * Hooked to the filter 'update_post_metadata'.
	 *
	 * @since 0.6
	 *
	 * @param null|bool  $r          Returned value (null by default).
	 * @param int        $post_id    Booking id.
	 * @param string     $meta_key   Meta key.
	 * @param int|string $meta_value Meta value.
	 * @return null|bool
	 */
	public function update_post_metadata( $r, $post_id, $meta_key, $meta_value ) {
		static $once = false;

		if ( in_array( $meta_key, array( '_booking_product_id', '_booking_resource_id' ) ) && ! empty( $meta_value ) && ! $once ) {
			$once = true;
			$r = $this->update_post_meta( $post_id, $meta_key, $meta_value );
		}

		$once = false;
		return $r;
	}

	/**
	 * Associates all products in a translation group to a booking.
	 *
	 * @since 0.6
	 *
	 * @param int    $post_id    Booking id.
	 * @param string $meta_key   Meta key.
	 * @param int    $meta_value Product id.
	 * @return bool
	 */
	protected function update_post_meta( $post_id, $meta_key, $meta_value ) {
		$values = get_post_meta( $post_id, $meta_key );

		if ( empty( $values ) ) {
			foreach ( pll_get_post_translations( $meta_value ) as $id ) {
				add_post_meta( $post_id, $meta_key, $id );
			}
		} else {
			$to_keep = array_intersect( $values, pll_get_post_translations( $meta_value ) );
			$olds    = array_values( array_diff( $values, $to_keep ) );
			$news    = array_values( array_diff( pll_get_post_translations( $meta_value ), $to_keep ) );
			foreach ( $olds as $k => $old ) {
				update_post_meta( $post_id, $meta_key, $news[ $k ], $old );
			}
		}

		return true;
	}

	/**
	 * Translates persons ids in _booking_persons meta.
	 *
	 * @since 0.6
	 *
	 * @param array  $persons  An array of persons.
	 * @param string $language Language slug.
	 * @return array
	 */
	protected function translate_booking_persons_meta( $persons, $language ) {
		if ( ! empty( $persons ) ) {
			$_persons = array();

			foreach ( $persons as $person => $n ) {
				$_persons[ pll_get_post( $person, $language ) ] = $n;
			}
			$persons = $_persons;
		}
		return $persons;
	}

	/**
	 * Allows to get the booking's associated product and resource in the current language.
	 * Hooked to the filter 'get_post_metadata'.
	 *
	 * @since 0.6
	 *
	 * @param null|bool $r        Returned value (null by default).
	 * @param int       $post_id  Booking id.
	 * @param string    $meta_key Meta key.
	 * @param bool      $single   Whether a single meta value has been requested.
	 * @return mixed
	 */
	public function get_post_metadata( $r, $post_id, $meta_key, $single ) {
		static $once = false;

		if ( ! $once && $single ) {
			switch ( $meta_key ) {
				case '_booking_product_id':
				case '_booking_resource_id':
					$once     = true;
					$value    = get_post_meta( $post_id, $meta_key, true );
					$language = PLL() instanceof PLL_Frontend ? pll_current_language() : pll_get_post_language( $post_id );
					$once     = false;
					return pll_get_post( $value, $language );
				case '_booking_persons':
					$once  = true;
					$value = get_post_meta( $post_id, $meta_key, true );
					$once  = false;
					return array( $this->translate_booking_persons_meta( $value, pll_get_post_language( $post_id ) ) );
			}
		}

		if ( ! $once && empty( $meta_key ) && 'wc_booking' === get_post_type( $post_id ) ) {
			$once     = true;
			$value    = get_post_meta( $post_id );
			$language = PLL() instanceof PLL_Frontend ? pll_current_language() : pll_get_post_language( $post_id );

			foreach ( array( '_booking_product_id', '_booking_resource_id' ) as $key ) {
				if ( ! empty( $value[ $key ] ) ) {
					$value[ $key ] = array( pll_get_post( reset( $value[ $key ] ), $language ) );
				}
			}

			if ( isset( $value['_booking_persons'] ) && is_array( $value['_booking_persons'] ) ) {
				$value['_booking_persons'] = array( serialize( $this->translate_booking_persons_meta( maybe_unserialize( reset( $value['_booking_persons'] ) ), pll_get_post_language( $post_id ) ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions
			}

			$once = false;
			return $value;
		}

		return $r;
	}

	/**
	 * Adds metas to synchronize when saving a product or resource.
	 * Hooked to the filter 'pll_copy_post_metas'.
	 *
	 * @since 0.6
	 *
	 * @param string[] $metas List of custom fields names.
	 * @return string[]
	 */
	public function copy_post_metas( $metas ) {
		$to_sync = array(
			'_wc_display_cost',
			'_wc_booking_base_cost', // Renamed to _wc_booking_block_cost, maybe in v1.10.9.
			'_wc_booking_block_cost',
			'_wc_booking_cost',
			'_wc_booking_min_duration',
			'_wc_booking_max_duration',
			'_wc_booking_enable_range_picker',
			'_wc_booking_calendar_display_mode',
			'_wc_booking_qty',
			'_wc_booking_has_persons',
			'_wc_booking_person_qty_multiplier',
			'_wc_booking_person_cost_multiplier',
			'_wc_booking_min_persons_group',
			'_wc_booking_max_persons_group',
			'_wc_booking_has_person_types',
			'_wc_booking_has_resources',
			'_wc_booking_resources_assignment',
			'_wc_booking_duration_type',
			'_wc_booking_duration',
			'_wc_booking_duration_unit',
			'_wc_booking_user_can_cancel',
			'_wc_booking_cancel_limit',
			'_wc_booking_cancel_limit_unit',
			'_wc_booking_max_date',
			'_wc_booking_max_date_unit',
			'_wc_booking_min_date',
			'_wc_booking_min_date_unit',
			'_wc_booking_buffer_period',
			'_wc_booking_first_block_time',
			'_wc_booking_requires_confirmation',
			'_wc_booking_default_date_availability',
			'_wc_booking_check_availability_against',
			'_wc_booking_apply_adjacent_buffer',
			'_wc_booking_availability',
			'_wc_booking_pricing',
			'_wc_booking_has_restricted_days', // Since 1.10.7.
			'_wc_booking_restricted_days', // Since 1.10.7.
			'_resource_base_costs', // To translate.
			'_resource_block_costs', // To translate.
		);

		// wc_booking_resource_label is automatically copied, not synced as public meta.
		return array_merge( $metas, $to_sync );
	}

	/**
	 * Translates a product meta before it is copied or synchronized.
	 * Hooked to the filter 'pll_translate_post_meta'.
	 *
	 * @since 1.0
	 *
	 * @param mixed  $value Meta value.
	 * @param string $key   Meta key.
	 * @param string $lang  Language of target.
	 * @return mixed
	 */
	public function translate_post_meta( $value, $key, $lang ) {
		if ( in_array( $key, array( '_resource_base_costs', '_resource_block_costs' ) ) ) {
			$tr_value = array();
			foreach ( $value as $post_id => $cost ) {
				if ( $tr_id = pll_get_post( $post_id, $lang ) ) {
					$tr_value[ $tr_id ] = $cost;
				}
			}
			$value = $tr_value;
		}
		return $value;
	}

	/**
	 * Adds the bookings metas to export.
	 *
	 * @since 1.8
	 *
	 * @param array $metas An array of post metas to export.
	 * @return array
	 */
	public function get_metas_to_translate( $metas ) {
		return array_merge( $metas, array( 'wc_booking_resource_label' => 1 ) );
	}

	/**
	 * Translates bookings items in cart.
	 * See WC_Booking_Form::get_posted_data().
	 * Hooked to the filter 'pllwc_translate_cart_item'.
	 *
	 * @since 0.6
	 *
	 * @param array  $item Cart item.
	 * @param string $lang Language code.
	 * @return array
	 */
	public function translate_cart_item( $item, $lang ) {
		if ( ! empty( $item['booking'] ) ) {
			$persons = array();
			$booking = &$item['booking'];

			// Translate persons types.
			if ( ! empty( $booking['_persons'] ) ) {
				foreach ( $booking['_persons'] as $id => $n ) {
					$tr_id = pll_get_post( $id, $lang );
					$persons[ $tr_id ] = $n;
					unset( $booking[ get_the_title( $id ) ] );
					$booking[ get_the_title( $tr_id ) ] = $n;
				}

				$booking['_persons'] = $persons;
			}

			// Translate resource.
			if ( ! empty( $booking['_resource_id'] ) ) {
				$tr_id                   = pll_get_post( $booking['_resource_id'], $lang );
				$booking['_resource_id'] = $tr_id;
				$booking['type']         = get_the_title( $tr_id );
			}

			// Translate date.
			if ( ! empty( $booking['date'] ) && ! empty( $booking['_date'] ) ) {
				$booking['date'] = date_i18n( wc_date_format(), strtotime( $booking['_date'] ) );
			}

			// Translate time.
			if ( ! empty( $booking['time'] ) && ! empty( $booking['_time'] ) ) {
				$booking['time'] = date_i18n( get_option( 'time_format' ), strtotime( "{$booking['_year']}-{$booking['_month']}-{$booking['_day']} {$booking['_time']}" ) );
			}

			// Translate Duration.
			if ( ! empty( $booking['duration'] ) && ! empty( $booking['_duration'] ) && $product = wc_get_product( $item['product_id'] ) ) {
				$total_duration = $booking['_duration'] * $product->get_duration();

				switch ( $booking['_duration_unit'] ) {
					case 'month':
						$booking['duration'] = $total_duration . ' ' . _n( 'month', 'months', $total_duration, 'polylang-wc' );
						break;
					case 'day':
						if ( $total_duration % 7 ) {
							$booking['duration'] = $total_duration . ' ' . _n( 'day', 'days', $total_duration, 'polylang-wc' );
						} else {
							$booking['duration'] = ( $total_duration / 7 ) . ' ' . _n( 'week', 'weeks', $total_duration, 'polylang-wc' );
						}
						break;
					case 'hour':
						$booking['duration'] = $total_duration . ' ' . _n( 'hour', 'hours', $total_duration, 'polylang-wc' );
						break;
					case 'minute':
						$booking['duration'] = $total_duration . ' ' . _n( 'minute', 'minutes', $total_duration, 'polylang-wc' );
						break;
					case 'night':
						$booking['duration'] = $total_duration . ' ' . _n( 'night', 'nights', $total_duration, 'polylang-wc' );
						break;
					default:
						$booking['duration'] = $total_duration;
						break;
				}
			}

			// We need to set the price.
			if ( ! empty( $item['data'] ) && ! empty( $booking['_cost'] ) ) {
				$item['data']->set_price( $booking['_cost'] );
			}
		}

		return $item;
	}

	/**
	 * Adds the booking to the cart item data when translating the cart.
	 * Hooked to the filter 'pllwc_add_cart_item_data'.
	 *
	 * @since 0.7.4
	 *
	 * @param array $cart_item_data Cart item data.
	 * @param array $item           Cart item.
	 * @return array
	 */
	public function add_cart_item_data( $cart_item_data, $item ) {
		if ( isset( $item['booking'] ) ) {
			$cart_item_data['booking'] = $item['booking'];
		}
		return $cart_item_data;
	}

	/**
	 * Filters bookings when sending notifications to get only bookings in the same language as the chosen product.
	 * Hooked to the action 'parse_query'.
	 *
	 * @since 0.6
	 *
	 * @param WP_Query $query WP_Query object.
	 * @return void
	 */
	public function filter_bookings_notifications( $query ) {
		$qvars = &$query->query_vars;

		if ( function_exists( 'get_current_screen' ) && ( $screen = get_current_screen() ) && 'wc_booking_page_booking_notification' === $screen->id && 'wc_booking' === $qvars['post_type'] ) {
			$meta_query = reset( $qvars['meta_query'] );
			$query->set( 'lang', pll_get_post_language( $meta_query['value'] ) );
		}
	}

	/**
	 * Returns the translation of the bookings endpoint url.
	 * Hooked to the filter 'pll_translation_url'.
	 *
	 * @since 0.6
	 *
	 * @param string $url  URL of the translation, to modify.
	 * @param string $lang Language slug.
	 * @return string
	 */
	public function pll_translation_url( $url, $lang ) {
		global $wp;

		$endpoint = apply_filters( 'woocommerce_bookings_account_endpoint', 'bookings' );

		if ( isset( PLL()->translate_slugs->slugs_model, $wp->query_vars[ $endpoint ] ) ) {
			$language = PLL()->model->get_language( $lang );
			$url      = wc_get_endpoint_url( $endpoint, '', $url );
			$url      = PLL()->translate_slugs->slugs_model->switch_translated_slug( $url, $language, 'wc_bookings' );
		}

		return $url;
	}

	/**
	 * Adds the bookings endpoint to the list of endpoints to translate.
	 * Hooked to the filter 'woocommerce_get_query_vars'.
	 * Copies `WC_Booking_Order_Manager::get_endpoint()` as the class is instantiated anonymously.
	 *
	 * @since 0.6
	 *
	 * @param array $slugs Endpoints slugs.
	 * @return array
	 */
	public function get_endpoints_query_vars( $slugs ) {
		$slugs[] = apply_filters( 'woocommerce_bookings_account_endpoint', 'bookings' );
		return $slugs;
	}

	/**
	 * Disables the languages filter for a customer to see all bookings whatever the languages.
	 * Hooked to the action 'parse_query'.
	 *
	 * @since 0.6
	 *
	 * @param WP_Query $query WP_Query object.
	 * @return void
	 */
	public function parse_query( $query ) {
		$qvars = $query->query_vars;

		// Customers should see all their orders whatever the language.
		if ( isset( $qvars['post_type'] ) && ( 'wc_booking' === $qvars['post_type'] || ( is_array( $qvars['post_type'] ) && in_array( 'wc_booking', $qvars['post_type'] ) ) ) ) {
			$query->set( 'lang', 0 );
		}
	}
}