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/subscriptions.php
<?php
/**
 * @package Polylang-WC
 */

/**
 * Manages the compatibility with WooCommerce subscriptions.
 * Version tested: 2.2.19.
 *
 * @since 0.4
 */
class PLLWC_Subscriptions {

	/**
	 * Constructor.
	 * Setups actions and filters.
	 *
	 * @since 0.4
	 */
	public function __construct() {
		add_filter( 'pllwc_copy_post_metas', array( $this, 'copy_post_metas' ) );

		// Add languages to the subscriptions, similar to orders.
		add_filter( 'pllwc_get_order_types', array( $this, 'translate_types' ) );

		// Renewal and Resubscribe.
		add_filter( 'wcs_new_order_created', array( $this, 'new_order_created' ), 10, 2 );

		// After `Polylang_Woocommerce::custom_order_tables_init()`, to make sure `PLLWC()->admin_orders` is set.
		add_action( 'pllwc_declare_compatibility_custom_order_tables', array( $this, 'custom_order_tables_init' ), 20 );

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

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

		// Strings translations.
		add_filter( 'pll_sanitize_string_translation', array( $this, 'sanitize_strings' ), 10, 3 );
		add_action( 'init', array( $this, 'register_strings' ) );

		// Endpoints.
		add_filter( 'pll_translation_url', array( $this, 'pll_translation_url' ), 10, 2 );

		// Check if a user has a subscription.
		add_filter( 'wcs_user_has_subscription', array( $this, 'user_has_subscription' ), 10, 4 );
		add_filter( 'woocommerce_get_subscriptions_query_args', array( $this, 'get_subscriptions_query_args' ), 10, 2 );

		// Variable subscription products.
		add_action( 'wp_trash_post', array( $this, 'delete_variation' ) );
		add_action( 'before_delete_post', array( $this, 'delete_variation' ) );

		// Work around endpoints options added in wpml-config.xml :/.
		remove_filter( 'option_woocommerce_myaccount_subscriptions_endpoint', array( PLL_WPML_Config::instance(), 'translate_strings' ) );
		remove_filter( 'option_woocommerce_myaccount_view_subscription_endpoint', array( PLL_WPML_Config::instance(), 'translate_strings' ) );

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

	/**
	 * Add Subscription 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 subscription.
				'cancelled_subscription_notification',
				// Customer completed order.
				'woocommerce_order_status_completed_renewal_notification',
				// Customer Completed Switch Order.
				'woocommerce_order_status_completed_switch_notification',
				// Customer renewal order.
				'woocommerce_order_status_pending_to_processing_renewal_notification',
				'woocommerce_order_status_pending_to_on-hold_renewal_notification',
				// Customer renewal invoice.
				'woocommerce_generated_manual_renewal_order_renewal_notification',
				'woocommerce_order_status_failed_renewal_notification',
				// Expired subscription.
				'expired_subscription_notification', // Since WCS 2.1.
				// New order (to the shop).
				'woocommerce_order_status_pending_to_processing_renewal_notification',
				'woocommerce_order_status_pending_to_completed_renewal_notification',
				'woocommerce_order_status_pending_to_on-hold_renewal_notification',
				'woocommerce_order_status_failed_to_processing_renewal_notification',
				'woocommerce_order_status_failed_to_completed_renewal_notification',
				'woocommerce_order_status_failed_to_on-hold_renewal_notification',
				// Switch order (to the shop).
				'woocommerce_order_status_pending_to_processing_switch_notification',
				'woocommerce_order_status_pending_to_completed_switch_notification',
				'woocommerce_order_status_pending_to_on-hold_switch_notification',
				'woocommerce_order_status_failed_to_processing_switch_notification',
				'woocommerce_order_status_failed_to_completed_switch_notification',
				'woocommerce_order_status_failed_to_on-hold_switch_notification',
				// Suspended Subscription.
				'on-hold_subscription_notification', // Since WCS 2.1.
			)
		);
	}

	/**
	 * Copies or synchronizes metas.
	 * Hooked to the filter 'pllwc_copy_post_metas'.
	 *
	 * @since 0.4
	 *
	 * @param array $keys List of custom fields names.
	 * @return array
	 */
	public function copy_post_metas( $keys ) {
		$wcs_keys = array(
			'_subscription_payment_sync_date',
			'_subscription_length',
			'_subscription_limit',
			'_subscription_period',
			'_subscription_period_interval',
			'_subscription_price',
			'_subscription_sign_up_fee',
			'_subscription_trial_length',
			'_subscription_trial_period',
		);
		return array_merge( $keys, $wcs_keys );
	}

	/**
	 * Language and translation management for the subscriptions post type.
	 * Hooked to the filter 'pllwc_get_order_types'.
	 *
	 * @since 0.4
	 *
	 * @param array $types List of post type names for which Polylang manages language and translations.
	 * @return array List of post type names for which Polylang manages language and translations.
	 */
	public function translate_types( $types ) {
		return array_merge( $types, array( 'shop_subscription' ) );
	}

	/**
	 * Assigns the order language when it is created from a subscription.
	 * Hooked to the filter 'wcs_new_order_created'.
	 *
	 * @since 0.4.4
	 *
	 * @param object $new_order    New order.
	 * @param object $subscription Parent subscription.
	 * @return object Unmodified order
	 */
	public function new_order_created( $new_order, $subscription ) {
		if ( $lang = pll_get_post_language( $subscription->get_id() ) ) {
			$data_store = PLLWC_Data_Store::load( 'order_language' );
			$data_store->set_language( $new_order->get_id(), $lang );
		}
		return $new_order;
	}

	/**
	 * Reloads Subscription translations in emails.
	 * Hooked to the action 'change_locale'.
	 *
	 * @since 1.0
	 *
	 * @return void
	 */
	public function change_locale() {
		if ( class_exists( 'WC_Subscriptions_Core_Plugin' ) && method_exists( WC_Subscriptions_Core_Plugin::instance(), 'load_plugin_textdomain' ) ) {
			WC_Subscriptions_Core_Plugin::instance()->load_plugin_textdomain();
		}
	}

	/**
	 * Registers string translations.
	 *
	 * @since 2.1.4
	 *
	 * @return void
	 */
	public function register_strings(): void {
		$options = array(
			'add_to_cart_button_text' => __( 'Add to Cart Button Text', 'polylang-wc' ),
			'order_button_text'       => __( 'Place Order Button Text', 'polylang-wc' ),
			'switch_button_text'      => __( 'Switch Button Text', 'polylang-wc' ),
		);

		foreach ( $options as $option => $name ) {
			if ( PLL() instanceof PLL_Frontend ) {
				add_filter( 'option_woocommerce_subscriptions_' . $option, 'pll__' );
				continue;
			}

			if ( ! PLL() instanceof PLL_Admin_Base ) {
				continue;
			}

			$string = get_option( 'woocommerce_subscriptions_' . $option );

			if ( empty( $string ) ) {
				continue;
			}

			pll_register_string( $name, $string, 'WooCommerce Subscriptions' );
		}
	}

	/**
	 * Translated strings must be sanitized the same way WooCommerce Subscriptions does before they are saved.
	 * Hooked to the filter 'pll_sanitize_string_translation'.
	 *
	 * @since 0.4
	 *
	 * @param string $translation A string translation.
	 * @param string $name        The string name.
	 * @param string $context     The group the string belongs to.
	 * @return string Sanitized translation
	 */
	public function sanitize_strings( $translation, $name, $context ) {
		if ( 'WooCommerce Subscriptions' === $context ) {
			$translation = wp_kses_post( trim( $translation ) );
		}
		return $translation;
	}

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

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

	/**
	 * Returns the translation of the current url.
	 * Handles the translations of the Subscriptions endpoints slugs.
	 * Hooked to the filter 'pll_translation_url'.
	 *
	 * @since 0.4
	 *
	 * @param string $url  URL of the translation, to modify.
	 * @param string $lang Language slug.
	 * @return string
	 */
	public function pll_translation_url( $url, $lang ) {
		if ( $url && defined( 'POLYLANG_PRO' ) && POLYLANG_PRO && get_option( 'permalink_structure' ) ) {
			$wcs_query = pll_get_anonymous_object_from_filter( 'init', array( 'WCS_Query', 'add_endpoints' ) );

			if ( is_object( $wcs_query ) ) {
				$endpoint = $wcs_query->get_current_endpoint();

				if ( $endpoint && isset( $wcs_query->query_vars[ $endpoint ] ) ) {
					$language = PLL()->model->get_language( $lang );
					$url      = PLL()->translate_slugs->slugs_model->switch_translated_slug( $url, $language, 'wc_' . $wcs_query->query_vars[ $endpoint ] );
				}
			}
		}

		return $url;
	}

	/**
	 * Checks if a user has a subscription to a translated product.
	 * Hooked to the filter 'wcs_user_has_subscription'.
	 *
	 * @since 0.9.2
	 *
	 * @param bool  $has_subscription Whether WooCommerce Subscriptions found a subscription.
	 * @param int   $user_id          The ID of a user in the store.
	 * @param int   $product_id       The ID of a product in the store.
	 * @param mixed $status           Subscription status.
	 * @return bool
	 */
	public function user_has_subscription( $has_subscription, $user_id, $product_id, $status ) {
		if ( false === $has_subscription && ! empty( $product_id ) ) {
			$data_store = PLLWC_Data_Store::load( 'product_language' );
			foreach ( wcs_get_users_subscriptions( $user_id ) as $subscription ) {
				if ( empty( $status ) || 'any' === $status || $subscription->has_status( $status ) ) {
					foreach ( $data_store->get_translations( $product_id ) as $tr_id ) {
						if ( $subscription->has_product( $tr_id ) ) {
							$has_subscription = true;
							break 2;
						}
					}
				}
			}
		}
		return $has_subscription;
	}

	/**
	 * When querying subscriptions and no subscriptions have been found for the current product,
	 * checks if there are subscriptions for the translated products.
	 * Hooked to the filter 'woocommerce_get_subscriptions_query_args'.
	 *
	 * @since 1.2
	 *
	 * @param array $query_args WP_Query() arguments.
	 * @param array $args       Arguments of wcs_get_subscriptions().
	 * @return array
	 */
	public function get_subscriptions_query_args( $query_args, $args ) {
		if ( isset( $query_args['post__in'] ) && array( 0 ) === $query_args['post__in'] ) { // Where `array( 0 ) correspond to `WCS_Admin_Post_Types::$post__in_none`, telling no result should be returned.
			$data_store = PLLWC_Data_Store::load( 'product_language' );
			$found_subs_from_translations = wcs_get_subscriptions_for_product(
				array_merge(
					$data_store->get_translations( $args['product_id'] ),
					$data_store->get_translations( $args['variation_id'] )
				)
			);

			if ( ! empty( $found_subs_from_translations ) ) {
				$query_args['post__in'] = $found_subs_from_translations;
			}
		}
		return $query_args;
	}

	/**
	 * Synchronizes the subscription variations deletion.
	 * The case is handled specifically in WC Subscriptions because
	 * subscription variations are trashed and not deleted permanently.
	 * Hooked to the actions 'wp_trash_post' and 'before_delete_post'.
	 *
	 * @since 1.3.3
	 *
	 * @param int $variation_id Subscription variation id.
	 * @return void
	 */
	public function delete_variation( $variation_id ) {
		static $avoid_delete = array();

		$post_type = get_post_type( $variation_id );

		if ( 'product_variation' === $post_type && ! in_array( $variation_id, $avoid_delete ) ) {
			$variation_product = wc_get_product( $variation_id );

			if ( $variation_product && $variation_product->is_type( 'subscription_variation' ) ) {
				$data_store = PLLWC_Data_Store::load( 'product_language' );
				$tr_ids = $data_store->get_translations( $variation_id );
				$avoid_delete = array_merge( $avoid_delete, array_values( $tr_ids ) ); // To avoid deleting a variation two times.
				foreach ( $tr_ids as $tr_id ) {
					wp_trash_post( $tr_id );
				}
			}
		}
	}

	/**
	 * "Safe" init after the plugin is declared compatible with the WC's HPOS feature.
	 * At this point, `pll_init` has been triggered we're in `before_woocommerce_init`.
	 *
	 * @since 2.1.2
	 *
	 * @return void
	 */
	public function custom_order_tables_init(): void {
		if ( PLL() instanceof PLL_Admin && PLLWC()->admin_orders instanceof PLLWC_Admin_Orders_HPOS ) {
			add_action( 'woocommerce_after_subscription_object_save', array( PLLWC()->admin_orders, 'save_order_language' ) );
		}
	}
}