', $tip, esc_url( WC()->plugin_url() ) ); } return $help_tip; } /** * Access an object's property in a way that is compatible with CRUD and non-CRUD APIs for different versions of WooCommerce. * * We don't want to force the use of a custom legacy class for orders, similar to WC_Subscription_Legacy, because 3rd party * code may expect the object type to be WC_Order with strict type checks. * * A note on dates: in WC 3.0+, dates are returned a timestamps in the site's timezone :upside_down_face:. In WC < 3.0, they were * returned as MySQL strings in the site's timezone. We return them from here as MySQL strings in UTC timezone because that's how * dates are used in Subscriptions in almost all cases, for sanity's sake. * * @param WC_Order|WC_Product|WC_Subscription $object The object whose property we want to access. * @param string $property The property name. * @param string $single Whether to return just the first piece of meta data with the given property key, or all meta data. * @param mixed $default (optional) The value to return if no value is found - defaults to single -> null, multiple -> array(). * * @since 2.2.0 * @return mixed */ function wcs_get_objects_property( $object, $property, $single = 'single', $default = null ) { $value = ! is_null( $default ) ? $default : ( ( 'single' === $single ) ? null : array() ); if ( ! is_object( $object ) ) { return $value; } $prefixed_key = wcs_maybe_prefix_key( $property ); $property_function_map = array( 'order_version' => 'version', 'order_currency' => 'currency', 'order_date' => 'date_created', 'date' => 'date_created', 'cart_discount' => 'total_discount', ); if ( isset( $property_function_map[ $property ] ) ) { $property = $property_function_map[ $property ]; } switch ( $property ) { case 'post' : // In order to keep backwards compatibility it's required to use the parent data for variations. if ( method_exists( $object, 'is_type' ) && $object->is_type( 'variation' ) ) { $value = get_post( wcs_get_objects_property( $object, 'parent_id' ) ); } else { $value = get_post( wcs_get_objects_property( $object, 'id' ) ); } break; case 'post_status' : $value = wcs_get_objects_property( $object, 'post' )->post_status; break; case 'variation_data' : $value = wc_get_product_variation_attributes( wcs_get_objects_property( $object, 'id' ) ); break; default : $function_name = 'get_' . $property; if ( is_callable( array( $object, $function_name ) ) ) { $value = $object->$function_name(); } else { // If we don't have a method for this specific property, but we are using WC 3.0, it may be set as meta data on the object so check if we can use that. if ( $object->meta_exists( $prefixed_key ) ) { if ( 'single' === $single ) { $value = $object->get_meta( $prefixed_key, true ); } else { // WC_Data::get_meta() returns an array of stdClass objects with id, key & value properties when meta is available. $value = wp_list_pluck( $object->get_meta( $prefixed_key, false ), 'value' ); } } elseif ( 'single' === $single && isset( $object->$property ) ) { // WC < 3.0. $value = $object->$property; } elseif ( strtolower( $property ) !== 'id' && metadata_exists( 'post', wcs_get_objects_property( $object, 'id' ), $prefixed_key ) ) { // If we couldn't find a property or function, fallback to using post meta as that's what many __get() methods in WC < 3.0 did. if ( 'single' === $single ) { $value = get_post_meta( wcs_get_objects_property( $object, 'id' ), $prefixed_key, true ); } else { // Get all the meta values. $value = get_post_meta( wcs_get_objects_property( $object, 'id' ), $prefixed_key, false ); } } } break; } return $value; } /** * Set an object's property in a way that is compatible with CRUD and non-CRUD APIs for different versions of WooCommerce. * * @param WC_Order|WC_Product|WC_Subscription $object The object whose property we want to access. * @param string $key The meta key name without '_' prefix * @param mixed $value The data to set as the value of the meta * @param string $save Whether to write the data to the database or not. Use 'save' to write to the database, anything else to only update it in memory. * @param int $meta_id The meta ID of existing meta data if you wish to overwrite an existing piece of meta. * @param string $prefix_meta_key Whether the key should be prefixed with an '_' when stored in meta. Defaulted to 'prefix_meta_key', pass any other value to bypass automatic prefixing (optional) * @since 2.2.0 * @return mixed */ function wcs_set_objects_property( &$object, $key, $value, $save = 'save', $meta_id = '', $prefix_meta_key = 'prefix_meta_key' ) { $prefixed_key = wcs_maybe_prefix_key( $key ); // WC will automatically set/update these keys when a shipping/billing address attribute changes so we can ignore these keys if ( in_array( $prefixed_key, array( '_shipping_address_index', '_billing_address_index' ) ) ) { return; } // Special cases where properties with setters which don't map nicely to their function names $meta_setters_map = array( '_cart_discount' => 'set_discount_total', '_cart_discount_tax' => 'set_discount_tax', '_customer_user' => 'set_customer_id', '_order_tax' => 'set_cart_tax', '_order_shipping' => 'set_shipping_total', '_sale_price_dates_from' => 'set_date_on_sale_from', '_sale_price_dates_to' => 'set_date_on_sale_to', ); // If we have a 3.0 object with a predefined setter function, use it if ( isset( $meta_setters_map[ $prefixed_key ] ) && is_callable( array( $object, $meta_setters_map[ $prefixed_key ] ) ) ) { $function = $meta_setters_map[ $prefixed_key ]; $object->$function( $value ); // If we have a 3.0 object, use the setter if available. } elseif ( is_callable( array( $object, 'set' . $prefixed_key ) ) ) { // Prices include tax is stored as a boolean in props but saved in the database as a string yes/no, so we need to normalise it here to make sure if we have a string (which can be passed to it by things like wcs_copy_order_meta()) that it's converted to a boolean before being set if ( '_prices_include_tax' === $prefixed_key && ! is_bool( $value ) ) { $value = 'yes' === $value; } $object->{ "set$prefixed_key" }( $value ); // If there is a setter without the order prefix (eg set_order_total -> set_total) } elseif ( is_callable( array( $object, 'set' . str_replace( '_order', '', $prefixed_key ) ) ) ) { $function_name = 'set' . str_replace( '_order', '', $prefixed_key ); $object->$function_name( $value ); // If there is no setter, treat as meta within the 3.0.x object. } elseif ( is_callable( array( $object, 'update_meta_data' ) ) ) { $meta_key = ( 'prefix_meta_key' === $prefix_meta_key ) ? $prefixed_key : $key; $object->update_meta_data( $meta_key, $value, $meta_id ); // 2.6.x handling for name which is not meta. } elseif ( 'name' === $key ) { $object->post->post_title = $value; // 2.6.x handling for everything else. } else { $object->$key = $value; } // Save the data if ( 'save' === $save ) { if ( is_callable( array( $object, 'save' ) ) ) { // WC 3.0+ $object->save(); } elseif ( 'date_created' == $key ) { // WC < 3.0+ wp_update_post( array( 'ID' => wcs_get_objects_property( $object, 'id' ), 'post_date' => get_date_from_gmt( $value ), 'post_date_gmt' => $value ) ); } elseif ( 'name' === $key ) { // the replacement for post_title added in 3.0, need to update post_title not post meta wp_update_post( array( 'ID' => wcs_get_objects_property( $object, 'id' ), 'post_title' => $value ) ); } else { $meta_key = ( 'prefix_meta_key' === $prefix_meta_key ) ? $prefixed_key : $key; if ( ! empty( $meta_id ) ) { update_metadata_by_mid( 'post', $meta_id, $value, $meta_key ); } else { update_post_meta( wcs_get_objects_property( $object, 'id' ), $meta_key, $value ); } } } } /** * Delete an object's property in a way that is compatible with CRUD and non-CRUD APIs for different versions of WooCommerce. * * @param WC_Order|WC_Product|WC_Subscription $object The object whose property we want to access. * @param string $key The meta key name without '_' prefix * @param mixed $value The data to set as the value of the meta * @param string $save Whether to save the data or not, 'save' to save the data, otherwise it won't be saved. * @since 2.2.0 * @return mixed */ function wcs_delete_objects_property( &$object, $key, $save = 'save', $meta_id = '' ) { $prefixed_key = wcs_maybe_prefix_key( $key ); if ( ! empty( $meta_id ) && method_exists( $object, 'delete_meta_data_by_mid' ) ) { $object->delete_meta_data_by_mid( $meta_id ); } elseif ( method_exists( $object, 'delete_meta_data' ) ) { $object->delete_meta_data( $prefixed_key ); } elseif ( isset( $object->$key ) ) { unset( $object->$key ); } // Save the data if ( 'save' === $save ) { if ( method_exists( $object, 'save' ) ) { // WC 3.0+ $object->save(); } elseif ( ! empty( $meta_id ) ) { delete_metadata_by_mid( 'post', $meta_id ); } else { delete_post_meta( wcs_get_objects_property( $object, 'id' ), $prefixed_key ); } } } /** * Check whether an order is a standard order (i.e. not a refund or subscription) in version compatible way. * * WC 3.0 has the $order->get_type() API which returns 'shop_order', while WC < 3.0 provided the $order->order_type * property which returned 'simple', so we need to check for both. * * @param WC_Order $order * @since 2.2.0 * @return bool */ function wcs_is_order( $order ) { if ( method_exists( $order, 'get_type' ) ) { $is_order = ( 'shop_order' === $order->get_type() ); } else { $is_order = ( isset( $order->order_type ) && 'simple' === $order->order_type ); } return $is_order; } /** * Find and return the value for a deprecated property property. * * Product properties should not be accessed directly with WooCommerce 3.0+, because of that, a lot of properties * have been deprecated/removed in the subscription product type classes. This function centralises the handling * of deriving deprecated properties. This saves duplicating the __get() method in WC_Product_Subscription, * WC_Product_Variable_Subscription and WC_Product_Subscription_Variation. * * @param string $property * @param WC_Product $product * @since 2.2.0 * @return mixed */ function wcs_product_deprecated_property_handler( $property, $product ) { $message_prefix = 'Product properties should not be accessed directly with WooCommerce 3.0+.'; $function_name = 'get_' . str_replace( 'subscription_', '', str_replace( 'subscription_period_', '', $property ) ); $class_name = get_class( $product ); $value = null; if ( in_array( $property, array( 'product_type', 'parent_product_type', 'limit_subscriptions', 'subscription_limit', 'subscription_payment_sync_date', 'subscription_one_time_shipping' ) ) || ( is_callable( array( 'WC_Subscriptions_Product', $function_name ) ) && false !== strpos( $property, 'subscription' ) ) ) { switch ( $property ) { case 'product_type': $value = $product->get_type(); $alternative = $class_name . '::get_type()'; break; case 'parent_product_type': if ( $product->is_type( 'subscription_variation' ) ) { $value = 'variation'; $alternative = 'WC_Product_Variation::get_type()'; } else { $value = 'variable'; $alternative = 'WC_Product_Variable::get_type()'; } break; case 'limit_subscriptions': case 'subscription_limit': $value = wcs_get_product_limitation( $product ); $alternative = 'wcs_get_product_limitation( $product )'; break; case 'subscription_one_time_shipping': $value = WC_Subscriptions_Product::needs_one_time_shipping( $product ); $alternative = 'WC_Subscriptions_Product::needs_one_time_shipping( $product )'; break; case 'subscription_payment_sync_date': $value = WC_Subscriptions_Synchroniser::get_products_payment_day( $product ); $alternative = 'WC_Subscriptions_Synchroniser::get_products_payment_day( $product )'; break; case 'max_variation_period': case 'max_variation_period_interval': $meta_key = '_' . $property; if ( '' === $product->get_meta( $meta_key ) ) { WC_Product_Variable::sync( $product->get_id() ); } $value = $product->get_meta( $meta_key ); $alternative = $class_name . '::get_meta( ' . $meta_key . ' ) or wcs_get_min_max_variation_data( $product )'; break; default: $value = call_user_func( array( 'WC_Subscriptions_Product', $function_name ), $product ); $alternative = sprintf( 'WC_Subscriptions_Product::%s( $product )', $function_name ); break; } wcs_deprecated_argument( $class_name . '::$' . $property, '2.2.0', sprintf( '%s Use %s', $message_prefix, $alternative ) ); } return $value; } /** * Access a coupon's property in a way that is compatible with CRUD and non-CRUD APIs for different versions of WooCommerce. * * Similar to @see wcs_get_objects_property * * @param WC_Coupon $coupon The coupon whose property we want to access. * @param string $property The property name. * @since 2.2 * @return mixed */ function wcs_get_coupon_property( $coupon, $property ) { $value = ''; if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) { $value = $coupon->$property; } else { // Some coupon properties don't map nicely to their corresponding getter function. This array contains those exceptions. $property_to_getter_map = array( 'type' => 'get_discount_type', 'exclude_product_ids' => 'get_excluded_product_ids', 'expiry_date' => 'get_date_expires', 'exclude_product_categories' => 'get_excluded_product_categories', 'customer_email' => 'get_email_restrictions', 'enable_free_shipping' => 'get_free_shipping', 'coupon_amount' => 'get_amount', ); switch ( true ) { case 'exists' == $property: $value = $coupon->get_id() > 0; break; case isset( $property_to_getter_map[ $property ] ) && is_callable( array( $coupon, $property_to_getter_map[ $property ] ) ): $function = $property_to_getter_map[ $property ]; $value = $coupon->$function(); break; case is_callable( array( $coupon, 'get_' . $property ) ): $value = $coupon->{ "get_$property" }(); break; } } return $value; } /** * Set a coupon's property in a way that is compatible with CRUD and non-CRUD APIs for different versions of WooCommerce. * * Similar to @see wcs_set_objects_property * * @param WC_Coupon $coupon The coupon whose property we want to set. * @param string $property The property name. * @param mixed $value The data to set as the value * @since 2.2 */ function wcs_set_coupon_property( &$coupon, $property, $value ) { if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) { $coupon->$property = $value; } else { // Some coupon properties don't map nicely to their corresponding setter function. This array contains those exceptions. $property_to_setter_map = array( 'type' => 'set_discount_type', 'exclude_product_ids' => 'set_excluded_product_ids', 'expiry_date' => 'set_date_expires', 'exclude_product_categories' => 'set_excluded_product_categories', 'customer_email' => 'set_email_restrictions', 'enable_free_shipping' => 'set_free_shipping', 'coupon_amount' => 'set_amount', ); switch ( true ) { case 'individual_use' == $property: // set_individual_use expects a boolean, the individual_use property use to be either 'yes' or 'no' so we need to accept both types if ( ! is_bool( $value ) ) { $value = 'yes' === $value; } $coupon->set_individual_use( $value ); break; case isset( $property_to_setter_map[ $property ] ) && is_callable( array( $coupon, $property_to_setter_map[ $property ] ) ): $function = $property_to_setter_map[ $property ]; $coupon->$function( $value ); break; case is_callable( array( $coupon, 'set_' . $property ) ): $coupon->{ "set_$property" }( $value ); break; } } } /** * Generate an order/subscription key. * * This is a compatibility wrapper for @see wc_generate_order_key() which was introduced in WC 3.5.4. * * @return string $order_key. * @since 2.5.0 */ function wcs_generate_order_key() { if ( function_exists( 'wc_generate_order_key' ) ) { $order_key = wc_generate_order_key(); } else { $order_key = 'wc_' . apply_filters( 'woocommerce_generate_order_key', 'order_' . wp_generate_password( 13, false ) ); } return $order_key; } /** * Update a single option for a WC_Settings_API object. * * This is a compatibility wrapper for @see WC_Settings_API::update_option() which was introduced in WC 3.4.0. * * @param WC_Settings_API $settings_api The object to update the option for. * @param string $key Option key. * @param mixed $value Value to set. * @since 2.5.1 */ function wcs_update_settings_option( $settings_api, $key, $value ) { // WooCommerce 3.4+ if ( is_callable( array( $settings_api, 'update_option' ) ) ) { $settings_api->update_option( $key, $value ); } else { if ( empty( $settings_api->settings ) ) { $settings_api->init_settings(); } $settings_api->settings[ $key ] = $value; return update_option( $settings_api->get_option_key(), apply_filters( 'woocommerce_settings_api_sanitized_fields_' . $settings_api->id, $settings_api->settings ), 'yes' ); } } /** * Determines if the request is a non-legacy REST API request. * * This function is a compatibility wrapper for WC()->is_rest_api_request() which was introduced in WC 3.6. * * @since 2.5.7 * * @return bool True if it's a REST API request, false otherwise. */ function wcs_is_rest_api_request() { if ( function_exists( 'WC' ) && is_callable( array( WC(), 'is_rest_api_request' ) ) ) { return WC()->is_rest_api_request(); } if ( empty( $_SERVER['REQUEST_URI'] ) ) { return false; } $rest_prefix = trailingslashit( rest_get_url_prefix() ); $is_rest_api_request = ( false !== strpos( $_SERVER['REQUEST_URI'], $rest_prefix ) ); return apply_filters( 'woocommerce_is_rest_api_request', $is_rest_api_request ); } /** * Determines whether the current request is a WordPress cron request. * * This function is a compatibility wrapper for wp_doing_cron() which was introduced in WP 4.8. * * @since 2.5.7 * * @return bool True if it's a WordPress cron request, false otherwise. */ function wcs_doing_cron() { return function_exists( 'wp_doing_cron' ) ? wp_doing_cron() : defined( 'DOING_CRON' ) && DOING_CRON; } /** * Determines whether the current request is a WordPress Ajax request. * * @since 2.5.7 * * @return bool True if it's a WordPress Ajax request, false otherwise. */ function wcs_doing_ajax() { return function_exists( 'wp_doing_ajax' ) ? wp_doing_ajax() : defined( 'DOING_AJAX' ) && DOING_AJAX; } /** * A wrapper function for getting an order's used coupon codes. * * WC 3.7 deprecated @see WC_Abstract_Order::get_used_coupons() in favour of WC_Abstract_Order::get_coupon_codes(). * * @since 2.6.0 * * @param WC_Abstract_Order $order An order or subscription object to get the coupon codes for. * @return array The coupon codes applied to the $order. */ function wcs_get_used_coupon_codes( $order ) { return is_callable( array( $order, 'get_coupon_codes' ) ) ? $order->get_coupon_codes() : $order->get_used_coupons(); } /** * Attach a function callback for a certain WooCommerce versions. * * Enables attaching a callback if WooCommerce is before, after, equal or not equal to a given version. * This function is a wrapper for @see WCS_Dependent_Hook_Manager::add_woocommerce_dependent_action(). * * @since 2.6.0 * * @param string $tag The action or filter tag to attach the callback too. * @param string|array $function The callable function to attach to the hook. * @param string $woocommerce_version The WooCommerce version to do a compare on. For example '3.0.0'. * @param string $operator The version compare operator to use. @see https://www.php.net/manual/en/function.version-compare.php * @param integer $priority The priority to attach this callback to. * @param integer $number_of_args The number of arguments to pass to the callback function */ function wcs_add_woocommerce_dependent_action( $tag, $function, $woocommerce_version, $operator, $priority = 10, $number_of_args = 1 ) { WCS_Dependent_Hook_Manager::add_woocommerce_dependent_action( $tag, $function, $woocommerce_version, $operator, $priority, $number_of_args ); }