initial commit

This commit is contained in:
2021-12-10 12:03:04 +00:00
commit c46c7ddbf0
3643 changed files with 582794 additions and 0 deletions

View File

@ -0,0 +1,438 @@
<?php
/**
* Abstract_WC_Order_Data_Store_CPT class file.
*
* @package WooCommerce\Classes
*/
use Automattic\Jetpack\Constants;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Abstract Order Data Store: Stored in CPT.
*
* @version 3.0.0
*/
abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Data_Store_Interface, WC_Abstract_Order_Data_Store_Interface {
/**
* Internal meta type used to store order data.
*
* @var string
*/
protected $meta_type = 'post';
/**
* Data stored in meta keys, but not considered "meta" for an order.
*
* @since 3.0.0
* @var array
*/
protected $internal_meta_keys = array(
'_order_currency',
'_cart_discount',
'_cart_discount_tax',
'_order_shipping',
'_order_shipping_tax',
'_order_tax',
'_order_total',
'_order_version',
'_prices_include_tax',
'_payment_tokens',
);
/*
|--------------------------------------------------------------------------
| CRUD Methods
|--------------------------------------------------------------------------
*/
/**
* Method to create a new order in the database.
*
* @param WC_Order $order Order object.
*/
public function create( &$order ) {
$order->set_version( Constants::get_constant( 'WC_VERSION' ) );
$order->set_currency( $order->get_currency() ? $order->get_currency() : get_woocommerce_currency() );
if ( ! $order->get_date_created( 'edit' ) ) {
$order->set_date_created( time() );
}
$id = wp_insert_post(
apply_filters(
'woocommerce_new_order_data',
array(
'post_date' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getOffsetTimestamp() ),
'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
'post_type' => $order->get_type( 'edit' ),
'post_status' => $this->get_post_status( $order ),
'ping_status' => 'closed',
'post_author' => 1,
'post_title' => $this->get_post_title(),
'post_password' => $this->get_order_key( $order ),
'post_parent' => $order->get_parent_id( 'edit' ),
'post_excerpt' => $this->get_post_excerpt( $order ),
)
),
true
);
if ( $id && ! is_wp_error( $id ) ) {
$order->set_id( $id );
$this->update_post_meta( $order );
$order->save_meta_data();
$order->apply_changes();
$this->clear_caches( $order );
}
}
/**
* Method to read an order from the database.
*
* @param WC_Order $order Order object.
*
* @throws Exception If passed order is invalid.
*/
public function read( &$order ) {
$order->set_defaults();
$post_object = get_post( $order->get_id() );
if ( ! $order->get_id() || ! $post_object || ! in_array( $post_object->post_type, wc_get_order_types(), true ) ) {
throw new Exception( __( 'Invalid order.', 'woocommerce' ) );
}
$order->set_props(
array(
'parent_id' => $post_object->post_parent,
'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ),
'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ),
'status' => $post_object->post_status,
)
);
$this->read_order_data( $order, $post_object );
$order->read_meta_data();
$order->set_object_read( true );
/**
* In older versions, discounts may have been stored differently.
* Update them now so if the object is saved, the correct values are
* stored. @todo When meta is flattened, handle this during migration.
*/
if ( version_compare( $order->get_version( 'edit' ), '2.3.7', '<' ) && $order->get_prices_include_tax( 'edit' ) ) {
$order->set_discount_total( (float) get_post_meta( $order->get_id(), '_cart_discount', true ) - (float) get_post_meta( $order->get_id(), '_cart_discount_tax', true ) );
}
}
/**
* Method to update an order in the database.
*
* @param WC_Order $order Order object.
*/
public function update( &$order ) {
$order->save_meta_data();
$order->set_version( Constants::get_constant( 'WC_VERSION' ) );
if ( null === $order->get_date_created( 'edit' ) ) {
$order->set_date_created( time() );
}
$changes = $order->get_changes();
// Only update the post when the post data changes.
if ( array_intersect( array( 'date_created', 'date_modified', 'status', 'parent_id', 'post_excerpt' ), array_keys( $changes ) ) ) {
$post_data = array(
'post_date' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getOffsetTimestamp() ),
'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
'post_status' => $this->get_post_status( $order ),
'post_parent' => $order->get_parent_id(),
'post_excerpt' => $this->get_post_excerpt( $order ),
'post_modified' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $order->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ),
'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $order->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ),
);
/**
* When updating this object, to prevent infinite loops, use $wpdb
* to update data, since wp_update_post spawns more calls to the
* save_post action.
*
* This ensures hooks are fired by either WP itself (admin screen save),
* or an update purely from CRUD.
*/
if ( doing_action( 'save_post' ) ) {
$GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $order->get_id() ) );
clean_post_cache( $order->get_id() );
} else {
wp_update_post( array_merge( array( 'ID' => $order->get_id() ), $post_data ) );
}
$order->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook.
}
$this->update_post_meta( $order );
$order->apply_changes();
$this->clear_caches( $order );
}
/**
* Method to delete an order from the database.
*
* @param WC_Order $order Order object.
* @param array $args Array of args to pass to the delete method.
*
* @return void
*/
public function delete( &$order, $args = array() ) {
$id = $order->get_id();
$args = wp_parse_args(
$args,
array(
'force_delete' => false,
)
);
if ( ! $id ) {
return;
}
if ( $args['force_delete'] ) {
wp_delete_post( $id );
$order->set_id( 0 );
do_action( 'woocommerce_delete_order', $id );
} else {
wp_trash_post( $id );
$order->set_status( 'trash' );
do_action( 'woocommerce_trash_order', $id );
}
}
/*
|--------------------------------------------------------------------------
| Additional Methods
|--------------------------------------------------------------------------
*/
/**
* Get the status to save to the post object.
*
* Plugins extending the order classes can override this to change the stored status/add prefixes etc.
*
* @since 3.6.0
* @param WC_order $order Order object.
* @return string
*/
protected function get_post_status( $order ) {
$order_status = $order->get_status( 'edit' );
if ( ! $order_status ) {
$order_status = apply_filters( 'woocommerce_default_order_status', 'pending' );
}
$post_status = $order_status;
$valid_statuses = get_post_stati();
// Add a wc- prefix to the status, but exclude some core statuses which should not be prefixed.
// @todo In the future this should only happen based on `wc_is_order_status`, but in order to
// preserve back-compatibility this happens to all statuses except a select few. A doing_it_wrong
// Notice will be needed here, followed by future removal.
if ( ! in_array( $post_status, array( 'auto-draft', 'draft', 'trash' ), true ) && in_array( 'wc-' . $post_status, $valid_statuses, true ) ) {
$post_status = 'wc-' . $post_status;
}
return $post_status;
}
/**
* Excerpt for post.
*
* @param WC_order $order Order object.
* @return string
*/
protected function get_post_excerpt( $order ) {
return '';
}
/**
* Get a title for the new post type.
*
* @return string
*/
protected function get_post_title() {
// @codingStandardsIgnoreStart
/* translators: %s: Order date */
return sprintf( __( 'Order &ndash; %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Order date parsed by strftime', 'woocommerce' ) ) );
// @codingStandardsIgnoreEnd
}
/**
* Get order key.
*
* @since 4.3.0
* @param WC_order $order Order object.
* @return string
*/
protected function get_order_key( $order ) {
return wc_generate_order_key();
}
/**
* Read order data. Can be overridden by child classes to load other props.
*
* @param WC_Order $order Order object.
* @param object $post_object Post object.
* @since 3.0.0
*/
protected function read_order_data( &$order, $post_object ) {
$id = $order->get_id();
$order->set_props(
array(
'currency' => get_post_meta( $id, '_order_currency', true ),
'discount_total' => get_post_meta( $id, '_cart_discount', true ),
'discount_tax' => get_post_meta( $id, '_cart_discount_tax', true ),
'shipping_total' => get_post_meta( $id, '_order_shipping', true ),
'shipping_tax' => get_post_meta( $id, '_order_shipping_tax', true ),
'cart_tax' => get_post_meta( $id, '_order_tax', true ),
'total' => get_post_meta( $id, '_order_total', true ),
'version' => get_post_meta( $id, '_order_version', true ),
'prices_include_tax' => metadata_exists( 'post', $id, '_prices_include_tax' ) ? 'yes' === get_post_meta( $id, '_prices_include_tax', true ) : 'yes' === get_option( 'woocommerce_prices_include_tax' ),
)
);
// Gets extra data associated with the order if needed.
foreach ( $order->get_extra_data_keys() as $key ) {
$function = 'set_' . $key;
if ( is_callable( array( $order, $function ) ) ) {
$order->{$function}( get_post_meta( $order->get_id(), '_' . $key, true ) );
}
}
}
/**
* Helper method that updates all the post meta for an order based on it's settings in the WC_Order class.
*
* @param WC_Order $order Order object.
* @since 3.0.0
*/
protected function update_post_meta( &$order ) {
$updated_props = array();
$meta_key_to_props = array(
'_order_currency' => 'currency',
'_cart_discount' => 'discount_total',
'_cart_discount_tax' => 'discount_tax',
'_order_shipping' => 'shipping_total',
'_order_shipping_tax' => 'shipping_tax',
'_order_tax' => 'cart_tax',
'_order_total' => 'total',
'_order_version' => 'version',
'_prices_include_tax' => 'prices_include_tax',
);
$props_to_update = $this->get_props_to_update( $order, $meta_key_to_props );
foreach ( $props_to_update as $meta_key => $prop ) {
$value = $order->{"get_$prop"}( 'edit' );
$value = is_string( $value ) ? wp_slash( $value ) : $value;
if ( 'prices_include_tax' === $prop ) {
$value = $value ? 'yes' : 'no';
}
$updated = $this->update_or_delete_post_meta( $order, $meta_key, $value );
if ( $updated ) {
$updated_props[] = $prop;
}
}
do_action( 'woocommerce_order_object_updated_props', $order, $updated_props );
}
/**
* Clear any caches.
*
* @param WC_Order $order Order object.
* @since 3.0.0
*/
protected function clear_caches( &$order ) {
clean_post_cache( $order->get_id() );
wc_delete_shop_order_transients( $order );
wp_cache_delete( 'order-items-' . $order->get_id(), 'orders' );
}
/**
* Read order items of a specific type from the database for this order.
*
* @param WC_Order $order Order object.
* @param string $type Order item type.
* @return array
*/
public function read_items( $order, $type ) {
global $wpdb;
// Get from cache if available.
$items = 0 < $order->get_id() ? wp_cache_get( 'order-items-' . $order->get_id(), 'orders' ) : false;
if ( false === $items ) {
$items = $wpdb->get_results(
$wpdb->prepare( "SELECT order_item_type, order_item_id, order_id, order_item_name FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d ORDER BY order_item_id;", $order->get_id() )
);
foreach ( $items as $item ) {
wp_cache_set( 'item-' . $item->order_item_id, $item, 'order-items' );
}
if ( 0 < $order->get_id() ) {
wp_cache_set( 'order-items-' . $order->get_id(), $items, 'orders' );
}
}
$items = wp_list_filter( $items, array( 'order_item_type' => $type ) );
if ( ! empty( $items ) ) {
$items = array_map( array( 'WC_Order_Factory', 'get_order_item' ), array_combine( wp_list_pluck( $items, 'order_item_id' ), $items ) );
} else {
$items = array();
}
return $items;
}
/**
* Remove all line items (products, coupons, shipping, taxes) from the order.
*
* @param WC_Order $order Order object.
* @param string $type Order item type. Default null.
*/
public function delete_items( $order, $type = null ) {
global $wpdb;
if ( ! empty( $type ) ) {
$wpdb->query( $wpdb->prepare( "DELETE FROM itemmeta USING {$wpdb->prefix}woocommerce_order_itemmeta itemmeta INNER JOIN {$wpdb->prefix}woocommerce_order_items items WHERE itemmeta.order_item_id = items.order_item_id AND items.order_id = %d AND items.order_item_type = %s", $order->get_id(), $type ) );
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d AND order_item_type = %s", $order->get_id(), $type ) );
} else {
$wpdb->query( $wpdb->prepare( "DELETE FROM itemmeta USING {$wpdb->prefix}woocommerce_order_itemmeta itemmeta INNER JOIN {$wpdb->prefix}woocommerce_order_items items WHERE itemmeta.order_item_id = items.order_item_id and items.order_id = %d", $order->get_id() ) );
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d", $order->get_id() ) );
}
$this->clear_caches( $order );
}
/**
* Get token ids for an order.
*
* @param WC_Order $order Order object.
* @return array
*/
public function get_payment_token_ids( $order ) {
$token_ids = array_filter( (array) get_post_meta( $order->get_id(), '_payment_tokens', true ) );
return $token_ids;
}
/**
* Update token ids for an order.
*
* @param WC_Order $order Order object.
* @param array $token_ids Payment token ids.
*/
public function update_payment_token_ids( $order, $token_ids ) {
update_post_meta( $order->get_id(), '_payment_tokens', $token_ids );
}
}

View File

@ -0,0 +1,166 @@
<?php
/**
* Class Abstract_WC_Order_Item_Type_Data_Store file.
*
* @package WooCommerce\DataStores
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC Order Item Data Store
*
* @version 3.0.0
*/
abstract class Abstract_WC_Order_Item_Type_Data_Store extends WC_Data_Store_WP implements WC_Object_Data_Store_Interface {
/**
* Meta type. This should match up with
* the types available at https://developer.wordpress.org/reference/functions/add_metadata/.
* WP defines 'post', 'user', 'comment', and 'term'.
*
* @var string
*/
protected $meta_type = 'order_item';
/**
* This only needs set if you are using a custom metadata type (for example payment tokens.
* This should be the name of the field your table uses for associating meta with objects.
* For example, in payment_tokenmeta, this would be payment_token_id.
*
* @var string
*/
protected $object_id_field_for_meta = 'order_item_id';
/**
* Create a new order item in the database.
*
* @since 3.0.0
* @param WC_Order_Item $item Order item object.
*/
public function create( &$item ) {
global $wpdb;
$wpdb->insert(
$wpdb->prefix . 'woocommerce_order_items',
array(
'order_item_name' => $item->get_name(),
'order_item_type' => $item->get_type(),
'order_id' => $item->get_order_id(),
)
);
$item->set_id( $wpdb->insert_id );
$this->save_item_data( $item );
$item->save_meta_data();
$item->apply_changes();
$this->clear_cache( $item );
do_action( 'woocommerce_new_order_item', $item->get_id(), $item, $item->get_order_id() );
}
/**
* Update a order item in the database.
*
* @since 3.0.0
* @param WC_Order_Item $item Order item object.
*/
public function update( &$item ) {
global $wpdb;
$changes = $item->get_changes();
if ( array_intersect( array( 'name', 'order_id' ), array_keys( $changes ) ) ) {
$wpdb->update(
$wpdb->prefix . 'woocommerce_order_items',
array(
'order_item_name' => $item->get_name(),
'order_item_type' => $item->get_type(),
'order_id' => $item->get_order_id(),
),
array( 'order_item_id' => $item->get_id() )
);
}
$this->save_item_data( $item );
$item->save_meta_data();
$item->apply_changes();
$this->clear_cache( $item );
do_action( 'woocommerce_update_order_item', $item->get_id(), $item, $item->get_order_id() );
}
/**
* Remove an order item from the database.
*
* @since 3.0.0
* @param WC_Order_Item $item Order item object.
* @param array $args Array of args to pass to the delete method.
*/
public function delete( &$item, $args = array() ) {
if ( $item->get_id() ) {
global $wpdb;
do_action( 'woocommerce_before_delete_order_item', $item->get_id() );
$wpdb->delete( $wpdb->prefix . 'woocommerce_order_items', array( 'order_item_id' => $item->get_id() ) );
$wpdb->delete( $wpdb->prefix . 'woocommerce_order_itemmeta', array( 'order_item_id' => $item->get_id() ) );
do_action( 'woocommerce_delete_order_item', $item->get_id() );
$this->clear_cache( $item );
}
}
/**
* Read a order item from the database.
*
* @since 3.0.0
*
* @param WC_Order_Item $item Order item object.
*
* @throws Exception If invalid order item.
*/
public function read( &$item ) {
global $wpdb;
$item->set_defaults();
// Get from cache if available.
$data = wp_cache_get( 'item-' . $item->get_id(), 'order-items' );
if ( false === $data ) {
$data = $wpdb->get_row( $wpdb->prepare( "SELECT order_id, order_item_name FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d LIMIT 1;", $item->get_id() ) );
wp_cache_set( 'item-' . $item->get_id(), $data, 'order-items' );
}
if ( ! $data ) {
throw new Exception( __( 'Invalid order item.', 'woocommerce' ) );
}
$item->set_props(
array(
'order_id' => $data->order_id,
'name' => $data->order_item_name,
)
);
$item->read_meta_data();
}
/**
* Saves an item's data to the database / item meta.
* Ran after both create and update, so $item->get_id() will be set.
*
* @since 3.0.0
* @param WC_Order_Item $item Order item object.
*/
public function save_item_data( &$item ) {}
/**
* Clear meta cache.
*
* @param WC_Order_Item $item Order item object.
*/
public function clear_cache( &$item ) {
wp_cache_delete( 'item-' . $item->get_id(), 'order-items' );
wp_cache_delete( 'order-items-' . $item->get_order_id(), 'orders' );
wp_cache_delete( $item->get_id(), $this->meta_type . '_meta' );
}
}

View File

@ -0,0 +1,729 @@
<?php
/**
* Class WC_Coupon_Data_Store_CPT file.
*
* @package WooCommerce\DataStores
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC Coupon Data Store: Custom Post Type.
*
* @version 3.0.0
*/
class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Data_Store_Interface, WC_Object_Data_Store_Interface {
/**
* Internal meta type used to store coupon data.
*
* @since 3.0.0
* @var string
*/
protected $meta_type = 'post';
/**
* Data stored in meta keys, but not considered "meta" for a coupon.
*
* @since 3.0.0
* @var array
*/
protected $internal_meta_keys = array(
'discount_type',
'coupon_amount',
'expiry_date',
'date_expires',
'usage_count',
'individual_use',
'product_ids',
'exclude_product_ids',
'usage_limit',
'usage_limit_per_user',
'limit_usage_to_x_items',
'free_shipping',
'product_categories',
'exclude_product_categories',
'exclude_sale_items',
'minimum_amount',
'maximum_amount',
'customer_email',
'_used_by',
'_edit_lock',
'_edit_last',
);
/**
* The updated coupon properties
*
* @since 4.1.0
* @var array
*/
protected $updated_props = array();
/**
* Method to create a new coupon in the database.
*
* @since 3.0.0
* @param WC_Coupon $coupon Coupon object.
*/
public function create( &$coupon ) {
if ( ! $coupon->get_date_created( 'edit' ) ) {
$coupon->set_date_created( time() );
}
$coupon_id = wp_insert_post(
apply_filters(
'woocommerce_new_coupon_data',
array(
'post_type' => 'shop_coupon',
'post_status' => 'publish',
'post_author' => get_current_user_id(),
'post_title' => $coupon->get_code( 'edit' ),
'post_content' => '',
'post_excerpt' => $coupon->get_description( 'edit' ),
'post_date' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getOffsetTimestamp() ),
'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getTimestamp() ),
)
),
true
);
if ( $coupon_id ) {
$coupon->set_id( $coupon_id );
$this->update_post_meta( $coupon );
$coupon->save_meta_data();
$coupon->apply_changes();
delete_transient( 'rest_api_coupons_type_count' );
do_action( 'woocommerce_new_coupon', $coupon_id, $coupon );
}
}
/**
* Method to read a coupon.
*
* @since 3.0.0
*
* @param WC_Coupon $coupon Coupon object.
*
* @throws Exception If invalid coupon.
*/
public function read( &$coupon ) {
$coupon->set_defaults();
$post_object = get_post( $coupon->get_id() );
if ( ! $coupon->get_id() || ! $post_object || 'shop_coupon' !== $post_object->post_type ) {
throw new Exception( __( 'Invalid coupon.', 'woocommerce' ) );
}
$coupon_id = $coupon->get_id();
$coupon->set_props(
array(
'code' => $post_object->post_title,
'description' => $post_object->post_excerpt,
'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ),
'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ),
'date_expires' => metadata_exists( 'post', $coupon_id, 'date_expires' ) ? get_post_meta( $coupon_id, 'date_expires', true ) : get_post_meta( $coupon_id, 'expiry_date', true ), // @todo: Migrate expiry_date meta to date_expires in upgrade routine.
'discount_type' => get_post_meta( $coupon_id, 'discount_type', true ),
'amount' => get_post_meta( $coupon_id, 'coupon_amount', true ),
'usage_count' => get_post_meta( $coupon_id, 'usage_count', true ),
'individual_use' => 'yes' === get_post_meta( $coupon_id, 'individual_use', true ),
'product_ids' => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'product_ids', true ) ) ),
'excluded_product_ids' => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'exclude_product_ids', true ) ) ),
'usage_limit' => get_post_meta( $coupon_id, 'usage_limit', true ),
'usage_limit_per_user' => get_post_meta( $coupon_id, 'usage_limit_per_user', true ),
'limit_usage_to_x_items' => 0 < get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ) ? get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ) : null,
'free_shipping' => 'yes' === get_post_meta( $coupon_id, 'free_shipping', true ),
'product_categories' => array_filter( (array) get_post_meta( $coupon_id, 'product_categories', true ) ),
'excluded_product_categories' => array_filter( (array) get_post_meta( $coupon_id, 'exclude_product_categories', true ) ),
'exclude_sale_items' => 'yes' === get_post_meta( $coupon_id, 'exclude_sale_items', true ),
'minimum_amount' => get_post_meta( $coupon_id, 'minimum_amount', true ),
'maximum_amount' => get_post_meta( $coupon_id, 'maximum_amount', true ),
'email_restrictions' => array_filter( (array) get_post_meta( $coupon_id, 'customer_email', true ) ),
'used_by' => array_filter( (array) get_post_meta( $coupon_id, '_used_by' ) ),
)
);
$coupon->read_meta_data();
$coupon->set_object_read( true );
do_action( 'woocommerce_coupon_loaded', $coupon );
}
/**
* Updates a coupon in the database.
*
* @since 3.0.0
* @param WC_Coupon $coupon Coupon object.
*/
public function update( &$coupon ) {
$coupon->save_meta_data();
$changes = $coupon->get_changes();
if ( array_intersect( array( 'code', 'description', 'date_created', 'date_modified' ), array_keys( $changes ) ) ) {
$post_data = array(
'post_title' => $coupon->get_code( 'edit' ),
'post_excerpt' => $coupon->get_description( 'edit' ),
'post_date' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created( 'edit' )->getOffsetTimestamp() ),
'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created( 'edit' )->getTimestamp() ),
'post_modified' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $coupon->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ),
'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $coupon->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ),
);
/**
* When updating this object, to prevent infinite loops, use $wpdb
* to update data, since wp_update_post spawns more calls to the
* save_post action.
*
* This ensures hooks are fired by either WP itself (admin screen save),
* or an update purely from CRUD.
*/
if ( doing_action( 'save_post' ) ) {
$GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $coupon->get_id() ) );
clean_post_cache( $coupon->get_id() );
} else {
wp_update_post( array_merge( array( 'ID' => $coupon->get_id() ), $post_data ) );
}
$coupon->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook.
}
$this->update_post_meta( $coupon );
$coupon->apply_changes();
delete_transient( 'rest_api_coupons_type_count' );
do_action( 'woocommerce_update_coupon', $coupon->get_id(), $coupon );
}
/**
* Deletes a coupon from the database.
*
* @since 3.0.0
*
* @param WC_Coupon $coupon Coupon object.
* @param array $args Array of args to pass to the delete method.
*/
public function delete( &$coupon, $args = array() ) {
$args = wp_parse_args(
$args,
array(
'force_delete' => false,
)
);
$id = $coupon->get_id();
if ( ! $id ) {
return;
}
if ( $args['force_delete'] ) {
wp_delete_post( $id );
wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $coupon->get_code(), 'coupons' );
$coupon->set_id( 0 );
do_action( 'woocommerce_delete_coupon', $id );
} else {
wp_trash_post( $id );
do_action( 'woocommerce_trash_coupon', $id );
}
}
/**
* Helper method that updates all the post meta for a coupon based on it's settings in the WC_Coupon class.
*
* @param WC_Coupon $coupon Coupon object.
* @since 3.0.0
*/
private function update_post_meta( &$coupon ) {
$meta_key_to_props = array(
'discount_type' => 'discount_type',
'coupon_amount' => 'amount',
'individual_use' => 'individual_use',
'product_ids' => 'product_ids',
'exclude_product_ids' => 'excluded_product_ids',
'usage_limit' => 'usage_limit',
'usage_limit_per_user' => 'usage_limit_per_user',
'limit_usage_to_x_items' => 'limit_usage_to_x_items',
'usage_count' => 'usage_count',
'date_expires' => 'date_expires',
'free_shipping' => 'free_shipping',
'product_categories' => 'product_categories',
'exclude_product_categories' => 'excluded_product_categories',
'exclude_sale_items' => 'exclude_sale_items',
'minimum_amount' => 'minimum_amount',
'maximum_amount' => 'maximum_amount',
'customer_email' => 'email_restrictions',
);
$props_to_update = $this->get_props_to_update( $coupon, $meta_key_to_props );
foreach ( $props_to_update as $meta_key => $prop ) {
$value = $coupon->{"get_$prop"}( 'edit' );
$value = is_string( $value ) ? wp_slash( $value ) : $value;
switch ( $prop ) {
case 'individual_use':
case 'free_shipping':
case 'exclude_sale_items':
$value = wc_bool_to_string( $value );
break;
case 'product_ids':
case 'excluded_product_ids':
$value = implode( ',', array_filter( array_map( 'intval', $value ) ) );
break;
case 'product_categories':
case 'excluded_product_categories':
$value = array_filter( array_map( 'intval', $value ) );
break;
case 'email_restrictions':
$value = array_filter( array_map( 'sanitize_email', $value ) );
break;
case 'date_expires':
$value = $value ? $value->getTimestamp() : null;
break;
}
$updated = $this->update_or_delete_post_meta( $coupon, $meta_key, $value );
if ( $updated ) {
$this->updated_props[] = $prop;
}
}
do_action( 'woocommerce_coupon_object_updated_props', $coupon, $this->updated_props );
}
/**
* Increase usage count for current coupon.
*
* @since 3.0.0
* @param WC_Coupon $coupon Coupon object.
* @param string $used_by Either user ID or billing email.
* @param WC_Order $order (Optional) If passed, clears the hold record associated with order.
* @return int New usage count.
*/
public function increase_usage_count( &$coupon, $used_by = '', $order = null ) {
$coupon_held_key_for_user = '';
if ( $order instanceof WC_Order ) {
$coupon_held_key_for_user = $order->get_data_store()->get_coupon_held_keys_for_users( $order, $coupon->get_id() );
}
$new_count = $this->update_usage_count_meta( $coupon, 'increase' );
if ( $used_by ) {
$this->add_coupon_used_by( $coupon, $used_by, $coupon_held_key_for_user );
$coupon->set_used_by( (array) get_post_meta( $coupon->get_id(), '_used_by' ) );
}
do_action( 'woocommerce_increase_coupon_usage_count', $coupon, $new_count, $used_by );
return $new_count;
}
/**
* Helper function to add a `_used_by` record to track coupons used by the user.
*
* @param WC_Coupon $coupon Coupon object.
* @param string $used_by Either user ID or billing email.
* @param string $coupon_held_key (Optional) Update meta key to `_used_by` instead of adding a new record.
*/
private function add_coupon_used_by( $coupon, $used_by, $coupon_held_key ) {
global $wpdb;
if ( $coupon_held_key && '' !== $coupon_held_key ) {
// Looks like we added a tentative record for this coupon getting used.
// Lets change the tentative record to a permanent one.
$result = $wpdb->query(
$wpdb->prepare(
"
UPDATE $wpdb->postmeta SET meta_key = %s, meta_value = %s WHERE meta_key = %s LIMIT 1",
'_used_by',
$used_by,
$coupon_held_key
)
);
if ( ! $result ) {
// If no rows were updated, then insert a `_used_by` row manually to maintain consistency.
add_post_meta( $coupon->get_id(), '_used_by', strtolower( $used_by ) );
}
} else {
add_post_meta( $coupon->get_id(), '_used_by', strtolower( $used_by ) );
}
}
/**
* Decrease usage count for current coupon.
*
* @since 3.0.0
* @param WC_Coupon $coupon Coupon object.
* @param string $used_by Either user ID or billing email.
* @return int New usage count.
*/
public function decrease_usage_count( &$coupon, $used_by = '' ) {
global $wpdb;
$new_count = $this->update_usage_count_meta( $coupon, 'decrease' );
if ( $used_by ) {
/**
* We're doing this the long way because `delete_post_meta( $id, $key, $value )` deletes.
* all instances where the key and value match, and we only want to delete one.
*/
$meta_id = $wpdb->get_var(
$wpdb->prepare(
"SELECT meta_id FROM $wpdb->postmeta WHERE meta_key = '_used_by' AND meta_value = %s AND post_id = %d LIMIT 1;",
$used_by,
$coupon->get_id()
)
);
if ( $meta_id ) {
delete_metadata_by_mid( 'post', $meta_id );
$coupon->set_used_by( (array) get_post_meta( $coupon->get_id(), '_used_by' ) );
}
}
do_action( 'woocommerce_decrease_coupon_usage_count', $coupon, $new_count, $used_by );
return $new_count;
}
/**
* Increase or decrease the usage count for a coupon by 1.
*
* @since 3.0.0
* @param WC_Coupon $coupon Coupon object.
* @param string $operation 'increase' or 'decrease'.
* @return int New usage count
*/
private function update_usage_count_meta( &$coupon, $operation = 'increase' ) {
global $wpdb;
$id = $coupon->get_id();
$operator = ( 'increase' === $operation ) ? '+' : '-';
add_post_meta( $id, 'usage_count', $coupon->get_usage_count( 'edit' ), true );
$wpdb->query(
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
"UPDATE $wpdb->postmeta SET meta_value = meta_value {$operator} 1 WHERE meta_key = 'usage_count' AND post_id = %d;",
$id
)
);
// Get the latest value direct from the DB, instead of possibly the WP meta cache.
return (int) $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = 'usage_count' AND post_id = %d;", $id ) );
}
/**
* Returns tentative usage count for coupon.
*
* @param int $coupon_id Coupon ID.
*
* @return int Tentative usage count.
*/
public function get_tentative_usage_count( $coupon_id ) {
global $wpdb;
return $wpdb->get_var(
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$this->get_tentative_usage_query( $coupon_id )
);
}
/**
* Get the number of uses for a coupon by user ID.
*
* @since 3.0.0
* @param WC_Coupon $coupon Coupon object.
* @param int $user_id User ID.
* @return int
*/
public function get_usage_by_user_id( &$coupon, $user_id ) {
global $wpdb;
$usage_count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %d;",
$coupon->get_id(),
$user_id
)
);
$tentative_usage_count = $this->get_tentative_usages_for_user( $coupon->get_id(), array( $user_id ) );
return $tentative_usage_count + $usage_count;
}
/**
* Get the number of uses for a coupon by email address
*
* @since 3.6.4
* @param WC_Coupon $coupon Coupon object.
* @param string $email Email address.
* @return int
*/
public function get_usage_by_email( &$coupon, $email ) {
global $wpdb;
$usage_count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %s;",
$coupon->get_id(),
$email
)
);
$tentative_usage_count = $this->get_tentative_usages_for_user( $coupon->get_id(), array( $email ) );
return $tentative_usage_count + $usage_count;
}
/**
* Get tentative coupon usages for user.
*
* @param int $coupon_id Coupon ID.
* @param array $user_aliases Array of user aliases to check tentative usages for.
*
* @return string|null
*/
public function get_tentative_usages_for_user( $coupon_id, $user_aliases ) {
global $wpdb;
return $wpdb->get_var(
$this->get_tentative_usage_query_for_user( $coupon_id, $user_aliases )
); // WPCS: unprepared SQL ok.
}
/**
* Get held time for resources before cancelling the order. Use 60 minutes as sane default.
* Note that the filter `woocommerce_coupon_hold_minutes` only support minutes because it's getting used elsewhere as well, however this function returns in seconds.
*
* @return int
*/
private function get_tentative_held_time() {
return apply_filters( 'woocommerce_coupon_hold_minutes', ( (int) get_option( 'woocommerce_hold_stock_minutes', 60 ) ) ) * 60;
}
/**
* Check and records coupon usage tentatively for short period of time so that counts validation is correct. Returns early if there is no limit defined for the coupon.
*
* @param WC_Coupon $coupon Coupon object.
*
* @return bool|int|string|null Returns meta key if coupon was held, null if returned early.
*/
public function check_and_hold_coupon( $coupon ) {
global $wpdb;
$usage_limit = $coupon->get_usage_limit();
$held_time = $this->get_tentative_held_time();
if ( 0 >= $usage_limit || 0 >= $held_time ) {
return null;
}
if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', true ) ) {
return null;
}
// Make sure we have usage_count meta key for this coupon because its required for `$query_for_usages`.
// We are not directly modifying `$query_for_usages` to allow for `usage_count` not present only keep that query simple.
if ( ! metadata_exists( 'post', $coupon->get_id(), 'usage_count' ) ) {
$coupon->set_usage_count( $coupon->get_usage_count() ); // Use `get_usage_count` here to write default value, which may changed by a filter.
$coupon->save();
}
$query_for_usages = $wpdb->prepare(
"
SELECT meta_value from $wpdb->postmeta
WHERE {$wpdb->postmeta}.meta_key = 'usage_count'
AND {$wpdb->postmeta}.post_id = %d
LIMIT 1
FOR UPDATE
",
$coupon->get_id()
);
$query_for_tentative_usages = $this->get_tentative_usage_query( $coupon->get_id() );
$db_timestamp = $wpdb->get_var( 'SELECT UNIX_TIMESTAMP() FROM DUAL' );
$coupon_usage_key = '_coupon_held_' . ( (int) $db_timestamp + $held_time ) . '_' . wp_generate_password( 6, false );
$insert_statement = $wpdb->prepare(
"
INSERT INTO $wpdb->postmeta ( post_id, meta_key, meta_value )
SELECT %d, %s, %s FROM DUAL
WHERE ( $query_for_usages ) + ( $query_for_tentative_usages ) < %d
",
$coupon->get_id(),
$coupon_usage_key,
'',
$usage_limit
); // WPCS: unprepared SQL ok.
/**
* In some cases, specifically when there is a combined index on post_id,meta_key, the insert statement above could end up in a deadlock.
* We will try to insert 3 times before giving up to recover from deadlock.
*/
for ( $count = 0; $count < 3; $count++ ) {
$result = $wpdb->query( $insert_statement ); // WPCS: unprepared SQL ok.
if ( false !== $result ) {
break;
}
}
return $result > 0 ? $coupon_usage_key : $result;
}
/**
* Generate query to calculate tentative usages for the coupon.
*
* @param int $coupon_id Coupon ID to get tentative usage query for.
*
* @return string Query for tentative usages.
*/
private function get_tentative_usage_query( $coupon_id ) {
global $wpdb;
return $wpdb->prepare(
"
SELECT COUNT(meta_id) FROM $wpdb->postmeta
WHERE {$wpdb->postmeta}.meta_key like %s
AND {$wpdb->postmeta}.meta_key > %s
AND {$wpdb->postmeta}.post_id = %d
FOR UPDATE
",
array(
'_coupon_held_%',
'_coupon_held_' . time(),
$coupon_id,
)
); // WPCS: unprepared SQL ok.
}
/**
* Check and records coupon usage tentatively for passed user aliases for short period of time so that counts validation is correct. Returns early if there is no limit per user for the coupon.
*
* @param WC_Coupon $coupon Coupon object.
* @param array $user_aliases Emails or Ids to check for user.
* @param string $user_alias Email/ID to use as `used_by` value.
*
* @return null|false|int
*/
public function check_and_hold_coupon_for_user( $coupon, $user_aliases, $user_alias ) {
global $wpdb;
$limit_per_user = $coupon->get_usage_limit_per_user();
$held_time = $this->get_tentative_held_time();
if ( 0 >= $limit_per_user || 0 >= $held_time ) {
// This coupon do not have any restriction for usage per customer. No need to check further, lets bail.
return null;
}
if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', true ) ) {
return null;
}
$format = implode( "','", array_fill( 0, count( $user_aliases ), '%s' ) );
$query_for_usages = $wpdb->prepare(
"
SELECT COUNT(*) FROM $wpdb->postmeta
WHERE {$wpdb->postmeta}.meta_key = '_used_by'
AND {$wpdb->postmeta}.meta_value IN ('$format')
AND {$wpdb->postmeta}.post_id = %d
FOR UPDATE
",
array_merge(
$user_aliases,
array( $coupon->get_id() )
)
); // WPCS: unprepared SQL ok.
$query_for_tentative_usages = $this->get_tentative_usage_query_for_user( $coupon->get_id(), $user_aliases );
$db_timestamp = $wpdb->get_var( 'SELECT UNIX_TIMESTAMP() FROM DUAL' );
$coupon_used_by_meta_key = '_maybe_used_by_' . ( (int) $db_timestamp + $held_time ) . '_' . wp_generate_password( 6, false );
$insert_statement = $wpdb->prepare(
"
INSERT INTO $wpdb->postmeta ( post_id, meta_key, meta_value )
SELECT %d, %s, %s FROM DUAL
WHERE ( $query_for_usages ) + ( $query_for_tentative_usages ) < %d
",
$coupon->get_id(),
$coupon_used_by_meta_key,
$user_alias,
$limit_per_user
); // WPCS: unprepared SQL ok.
// This query can potentially be deadlocked if a combined index on post_id and meta_key is present and there is
// high concurrency, in which case DB will abort the query which has done less work to resolve deadlock.
// We will try up to 3 times before giving up.
for ( $count = 0; $count < 3; $count++ ) {
$result = $wpdb->query( $insert_statement ); // WPCS: unprepared SQL ok.
if ( false !== $result ) {
break;
}
}
return $result > 0 ? $coupon_used_by_meta_key : $result;
}
/**
* Generate query to calculate tentative usages for the coupon by the user.
*
* @param int $coupon_id Coupon ID.
* @param array $user_aliases List of user aliases to check for usages.
*
* @return string Tentative usages query.
*/
private function get_tentative_usage_query_for_user( $coupon_id, $user_aliases ) {
global $wpdb;
$format = implode( "','", array_fill( 0, count( $user_aliases ), '%s' ) );
// Note that if you are debugging, `_maybe_used_by_%` will be converted to `_maybe_used_by_{...very long str...}` to very long string. This is expected, and is automatically corrected while running the insert query.
return $wpdb->prepare(
"
SELECT COUNT( meta_id ) FROM $wpdb->postmeta
WHERE {$wpdb->postmeta}.meta_key like %s
AND {$wpdb->postmeta}.meta_key > %s
AND {$wpdb->postmeta}.post_id = %d
AND {$wpdb->postmeta}.meta_value IN ('$format')
FOR UPDATE
",
array_merge(
array(
'_maybe_used_by_%',
'_maybe_used_by_' . time(),
$coupon_id,
),
$user_aliases
)
); // WPCS: unprepared SQL ok.
}
/**
* Return a coupon code for a specific ID.
*
* @since 3.0.0
* @param int $id Coupon ID.
* @return string Coupon Code
*/
public function get_code_by_id( $id ) {
global $wpdb;
return $wpdb->get_var(
$wpdb->prepare(
"SELECT post_title
FROM $wpdb->posts
WHERE ID = %d
AND post_type = 'shop_coupon'
AND post_status = 'publish'",
$id
)
);
}
/**
* Return an array of IDs for for a specific coupon code.
* Can return multiple to check for existence.
*
* @since 3.0.0
* @param string $code Coupon code.
* @return array Array of IDs.
*/
public function get_ids_by_code( $code ) {
global $wpdb;
return $wpdb->get_col(
$wpdb->prepare(
"SELECT ID FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC",
wc_sanitize_coupon_code( $code )
)
);
}
}

View File

@ -0,0 +1,199 @@
<?php
/**
* Class WC_Customer_Data_Store_Session file.
*
* @package WooCommerce\DataStores
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC Customer Data Store which stores the data in session.
*
* @version 3.0.0
*/
class WC_Customer_Data_Store_Session extends WC_Data_Store_WP implements WC_Customer_Data_Store_Interface, WC_Object_Data_Store_Interface {
/**
* Keys which are also stored in a session (so we can make sure they get updated...)
*
* @var array
*/
protected $session_keys = array(
'id',
'date_modified',
'billing_postcode',
'billing_city',
'billing_address_1',
'billing_address',
'billing_address_2',
'billing_state',
'billing_country',
'shipping_postcode',
'shipping_city',
'shipping_address_1',
'shipping_address',
'shipping_address_2',
'shipping_state',
'shipping_country',
'is_vat_exempt',
'calculated_shipping',
'billing_first_name',
'billing_last_name',
'billing_company',
'billing_phone',
'billing_email',
'shipping_first_name',
'shipping_last_name',
'shipping_company',
'shipping_phone',
);
/**
* Simply update the session.
*
* @param WC_Customer $customer Customer object.
*/
public function create( &$customer ) {
$this->save_to_session( $customer );
}
/**
* Simply update the session.
*
* @param WC_Customer $customer Customer object.
*/
public function update( &$customer ) {
$this->save_to_session( $customer );
}
/**
* Saves all customer data to the session.
*
* @param WC_Customer $customer Customer object.
*/
public function save_to_session( $customer ) {
$data = array();
foreach ( $this->session_keys as $session_key ) {
$function_key = $session_key;
if ( 'billing_' === substr( $session_key, 0, 8 ) ) {
$session_key = str_replace( 'billing_', '', $session_key );
}
$data[ $session_key ] = (string) $customer->{"get_$function_key"}( 'edit' );
}
WC()->session->set( 'customer', $data );
}
/**
* Read customer data from the session unless the user has logged in, in
* which case the stored ID will differ from the actual ID.
*
* @since 3.0.0
* @param WC_Customer $customer Customer object.
*/
public function read( &$customer ) {
$data = (array) WC()->session->get( 'customer' );
/**
* There is a valid session if $data is not empty, and the ID matches the logged in user ID.
*
* If the user object has been updated since the session was created (based on date_modified) we should not load the session - data should be reloaded.
*/
if ( isset( $data['id'], $data['date_modified'] ) && $data['id'] === (string) $customer->get_id() && $data['date_modified'] === (string) $customer->get_date_modified( 'edit' ) ) {
foreach ( $this->session_keys as $session_key ) {
if ( in_array( $session_key, array( 'id', 'date_modified' ), true ) ) {
continue;
}
$function_key = $session_key;
if ( 'billing_' === substr( $session_key, 0, 8 ) ) {
$session_key = str_replace( 'billing_', '', $session_key );
}
if ( isset( $data[ $session_key ] ) && is_callable( array( $customer, "set_{$function_key}" ) ) ) {
$customer->{"set_{$function_key}"}( wp_unslash( $data[ $session_key ] ) );
}
}
}
$this->set_defaults( $customer );
$customer->set_object_read( true );
}
/**
* Load default values if props are unset.
*
* @param WC_Customer $customer Customer object.
*/
protected function set_defaults( &$customer ) {
try {
$default = wc_get_customer_default_location();
$has_shipping_address = $customer->has_shipping_address();
if ( ! $customer->get_billing_country() ) {
$customer->set_billing_country( $default['country'] );
}
if ( ! $customer->get_shipping_country() && ! $has_shipping_address ) {
$customer->set_shipping_country( $customer->get_billing_country() );
}
if ( ! $customer->get_billing_state() ) {
$customer->set_billing_state( $default['state'] );
}
if ( ! $customer->get_shipping_state() && ! $has_shipping_address ) {
$customer->set_shipping_state( $customer->get_billing_state() );
}
if ( ! $customer->get_billing_email() && is_user_logged_in() ) {
$current_user = wp_get_current_user();
$customer->set_billing_email( $current_user->user_email );
}
} catch ( WC_Data_Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
}
}
/**
* Deletes a customer from the database.
*
* @since 3.0.0
* @param WC_Customer $customer Customer object.
* @param array $args Array of args to pass to the delete method.
*/
public function delete( &$customer, $args = array() ) {
WC()->session->set( 'customer', null );
}
/**
* Gets the customers last order.
*
* @since 3.0.0
* @param WC_Customer $customer Customer object.
* @return WC_Order|false
*/
public function get_last_order( &$customer ) {
return false;
}
/**
* Return the number of orders this customer has.
*
* @since 3.0.0
* @param WC_Customer $customer Customer object.
* @return integer
*/
public function get_order_count( &$customer ) {
return 0;
}
/**
* Return how much money this customer has spent.
*
* @since 3.0.0
* @param WC_Customer $customer Customer object.
* @return float
*/
public function get_total_spent( &$customer ) {
return 0;
}
}

View File

@ -0,0 +1,529 @@
<?php
/**
* Class WC_Customer_Data_Store file.
*
* @package WooCommerce\DataStores
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC Customer Data Store.
*
* @version 3.0.0
*/
class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Data_Store_Interface, WC_Object_Data_Store_Interface {
/**
* Data stored in meta keys, but not considered "meta".
*
* @since 3.0.0
* @var array
*/
protected $internal_meta_keys = array(
'locale',
'billing_postcode',
'billing_city',
'billing_address_1',
'billing_address_2',
'billing_state',
'billing_country',
'shipping_postcode',
'shipping_city',
'shipping_address_1',
'shipping_address_2',
'shipping_state',
'shipping_country',
'paying_customer',
'last_update',
'first_name',
'last_name',
'display_name',
'show_admin_bar_front',
'use_ssl',
'admin_color',
'rich_editing',
'comment_shortcuts',
'dismissed_wp_pointers',
'show_welcome_panel',
'session_tokens',
'nickname',
'description',
'billing_first_name',
'billing_last_name',
'billing_company',
'billing_phone',
'billing_email',
'shipping_first_name',
'shipping_last_name',
'shipping_company',
'shipping_phone',
'wptests_capabilities',
'wptests_user_level',
'syntax_highlighting',
'_order_count',
'_money_spent',
'_last_order',
'_woocommerce_tracks_anon_id',
);
/**
* Internal meta type used to store user data.
*
* @var string
*/
protected $meta_type = 'user';
/**
* Callback to remove unwanted meta data.
*
* @param object $meta Meta object.
* @return bool
*/
protected function exclude_internal_meta_keys( $meta ) {
global $wpdb;
$table_prefix = $wpdb->prefix ? $wpdb->prefix : 'wp_';
return ! in_array( $meta->meta_key, $this->internal_meta_keys, true )
&& 0 !== strpos( $meta->meta_key, '_woocommerce_persistent_cart' )
&& 0 !== strpos( $meta->meta_key, 'closedpostboxes_' )
&& 0 !== strpos( $meta->meta_key, 'metaboxhidden_' )
&& 0 !== strpos( $meta->meta_key, 'manageedit-' )
&& ! strstr( $meta->meta_key, $table_prefix )
&& 0 !== stripos( $meta->meta_key, 'wp_' );
}
/**
* Method to create a new customer in the database.
*
* @since 3.0.0
*
* @param WC_Customer $customer Customer object.
*
* @throws WC_Data_Exception If unable to create new customer.
*/
public function create( &$customer ) {
$id = wc_create_new_customer( $customer->get_email(), $customer->get_username(), $customer->get_password() );
if ( is_wp_error( $id ) ) {
throw new WC_Data_Exception( $id->get_error_code(), $id->get_error_message() );
}
$customer->set_id( $id );
$this->update_user_meta( $customer );
// Prevent wp_update_user calls in the same request and customer trigger the 'Notice of Password Changed' email.
$customer->set_password( '' );
wp_update_user(
apply_filters(
'woocommerce_update_customer_args',
array(
'ID' => $customer->get_id(),
'role' => $customer->get_role(),
'display_name' => $customer->get_display_name(),
),
$customer
)
);
$wp_user = new WP_User( $customer->get_id() );
$customer->set_date_created( $wp_user->user_registered );
$customer->set_date_modified( get_user_meta( $customer->get_id(), 'last_update', true ) );
$customer->save_meta_data();
$customer->apply_changes();
do_action( 'woocommerce_new_customer', $customer->get_id(), $customer );
}
/**
* Method to read a customer object.
*
* @since 3.0.0
* @param WC_Customer $customer Customer object.
* @throws Exception If invalid customer.
*/
public function read( &$customer ) {
$user_object = $customer->get_id() ? get_user_by( 'id', $customer->get_id() ) : false;
// User object is required.
if ( ! $user_object || empty( $user_object->ID ) ) {
throw new Exception( __( 'Invalid customer.', 'woocommerce' ) );
}
$customer_id = $customer->get_id();
// Load meta but exclude deprecated props and parent keys.
$user_meta = array_diff_key(
array_change_key_case( array_map( 'wc_flatten_meta_callback', get_user_meta( $customer_id ) ) ),
array_flip( array( 'country', 'state', 'postcode', 'city', 'address', 'address_2', 'default', 'location' ) ),
array_change_key_case( (array) $user_object->data )
);
$customer->set_props( $user_meta );
$customer->set_props(
array(
'is_paying_customer' => get_user_meta( $customer_id, 'paying_customer', true ),
'email' => $user_object->user_email,
'username' => $user_object->user_login,
'display_name' => $user_object->display_name,
'date_created' => $user_object->user_registered, // Mysql string in local format.
'date_modified' => get_user_meta( $customer_id, 'last_update', true ),
'role' => ! empty( $user_object->roles[0] ) ? $user_object->roles[0] : 'customer',
)
);
$customer->read_meta_data();
$customer->set_object_read( true );
do_action( 'woocommerce_customer_loaded', $customer );
}
/**
* Updates a customer in the database.
*
* @since 3.0.0
* @param WC_Customer $customer Customer object.
*/
public function update( &$customer ) {
wp_update_user(
apply_filters(
'woocommerce_update_customer_args',
array(
'ID' => $customer->get_id(),
'user_email' => $customer->get_email(),
'display_name' => $customer->get_display_name(),
),
$customer
)
);
// Only update password if a new one was set with set_password.
if ( $customer->get_password() ) {
wp_update_user(
array(
'ID' => $customer->get_id(),
'user_pass' => $customer->get_password(),
)
);
$customer->set_password( '' );
}
$this->update_user_meta( $customer );
$customer->set_date_modified( get_user_meta( $customer->get_id(), 'last_update', true ) );
$customer->save_meta_data();
$customer->apply_changes();
do_action( 'woocommerce_update_customer', $customer->get_id(), $customer );
}
/**
* Deletes a customer from the database.
*
* @since 3.0.0
* @param WC_Customer $customer Customer object.
* @param array $args Array of args to pass to the delete method.
*/
public function delete( &$customer, $args = array() ) {
if ( ! $customer->get_id() ) {
return;
}
$args = wp_parse_args(
$args,
array(
'reassign' => 0,
)
);
$id = $customer->get_id();
wp_delete_user( $id, $args['reassign'] );
do_action( 'woocommerce_delete_customer', $id );
}
/**
* Helper method that updates all the meta for a customer. Used for update & create.
*
* @since 3.0.0
* @param WC_Customer $customer Customer object.
*/
private function update_user_meta( $customer ) {
$updated_props = array();
$changed_props = $customer->get_changes();
$meta_key_to_props = array(
'paying_customer' => 'is_paying_customer',
'first_name' => 'first_name',
'last_name' => 'last_name',
);
foreach ( $meta_key_to_props as $meta_key => $prop ) {
if ( ! array_key_exists( $prop, $changed_props ) ) {
continue;
}
if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) {
$updated_props[] = $prop;
}
}
$billing_address_props = array(
'billing_first_name' => 'billing_first_name',
'billing_last_name' => 'billing_last_name',
'billing_company' => 'billing_company',
'billing_address_1' => 'billing_address_1',
'billing_address_2' => 'billing_address_2',
'billing_city' => 'billing_city',
'billing_state' => 'billing_state',
'billing_postcode' => 'billing_postcode',
'billing_country' => 'billing_country',
'billing_email' => 'billing_email',
'billing_phone' => 'billing_phone',
);
foreach ( $billing_address_props as $meta_key => $prop ) {
$prop_key = substr( $prop, 8 );
if ( ! isset( $changed_props['billing'] ) || ! array_key_exists( $prop_key, $changed_props['billing'] ) ) {
continue;
}
if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) {
$updated_props[] = $prop;
}
}
$shipping_address_props = array(
'shipping_first_name' => 'shipping_first_name',
'shipping_last_name' => 'shipping_last_name',
'shipping_company' => 'shipping_company',
'shipping_address_1' => 'shipping_address_1',
'shipping_address_2' => 'shipping_address_2',
'shipping_city' => 'shipping_city',
'shipping_state' => 'shipping_state',
'shipping_postcode' => 'shipping_postcode',
'shipping_country' => 'shipping_country',
'shipping_phone' => 'shipping_phone',
);
foreach ( $shipping_address_props as $meta_key => $prop ) {
$prop_key = substr( $prop, 9 );
if ( ! isset( $changed_props['shipping'] ) || ! array_key_exists( $prop_key, $changed_props['shipping'] ) ) {
continue;
}
if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) {
$updated_props[] = $prop;
}
}
do_action( 'woocommerce_customer_object_updated_props', $customer, $updated_props );
}
/**
* Gets the customers last order.
*
* @since 3.0.0
* @param WC_Customer $customer Customer object.
* @return WC_Order|false
*/
public function get_last_order( &$customer ) {
$last_order = apply_filters(
'woocommerce_customer_get_last_order',
get_user_meta( $customer->get_id(), '_last_order', true ),
$customer
);
if ( '' === $last_order ) {
global $wpdb;
$last_order = $wpdb->get_var(
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
"SELECT posts.ID
FROM $wpdb->posts AS posts
LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id
WHERE meta.meta_key = '_customer_user'
AND meta.meta_value = '" . esc_sql( $customer->get_id() ) . "'
AND posts.post_type = 'shop_order'
AND posts.post_status IN ( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' )
ORDER BY posts.ID DESC"
// phpcs:enable
);
update_user_meta( $customer->get_id(), '_last_order', $last_order );
}
if ( ! $last_order ) {
return false;
}
return wc_get_order( absint( $last_order ) );
}
/**
* Return the number of orders this customer has.
*
* @since 3.0.0
* @param WC_Customer $customer Customer object.
* @return integer
*/
public function get_order_count( &$customer ) {
$count = apply_filters(
'woocommerce_customer_get_order_count',
get_user_meta( $customer->get_id(), '_order_count', true ),
$customer
);
if ( '' === $count ) {
global $wpdb;
$count = $wpdb->get_var(
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
"SELECT COUNT(*)
FROM $wpdb->posts as posts
LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id
WHERE meta.meta_key = '_customer_user'
AND posts.post_type = 'shop_order'
AND posts.post_status IN ( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' )
AND meta_value = '" . esc_sql( $customer->get_id() ) . "'"
// phpcs:enable
);
update_user_meta( $customer->get_id(), '_order_count', $count );
}
return absint( $count );
}
/**
* Return how much money this customer has spent.
*
* @since 3.0.0
* @param WC_Customer $customer Customer object.
* @return float
*/
public function get_total_spent( &$customer ) {
$spent = apply_filters(
'woocommerce_customer_get_total_spent',
get_user_meta( $customer->get_id(), '_money_spent', true ),
$customer
);
if ( '' === $spent ) {
global $wpdb;
$statuses = array_map( 'esc_sql', wc_get_is_paid_statuses() );
$spent = $wpdb->get_var(
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
apply_filters(
'woocommerce_customer_get_total_spent_query',
"SELECT SUM(meta2.meta_value)
FROM $wpdb->posts as posts
LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id
LEFT JOIN {$wpdb->postmeta} AS meta2 ON posts.ID = meta2.post_id
WHERE meta.meta_key = '_customer_user'
AND meta.meta_value = '" . esc_sql( $customer->get_id() ) . "'
AND posts.post_type = 'shop_order'
AND posts.post_status IN ( 'wc-" . implode( "','wc-", $statuses ) . "' )
AND meta2.meta_key = '_order_total'",
$customer
)
// phpcs:enable
);
if ( ! $spent ) {
$spent = 0;
}
update_user_meta( $customer->get_id(), '_money_spent', $spent );
}
return wc_format_decimal( $spent, 2 );
}
/**
* Search customers and return customer IDs.
*
* @param string $term Search term.
* @param int|string $limit Limit search results.
* @since 3.0.7
*
* @return array
*/
public function search_customers( $term, $limit = '' ) {
$results = apply_filters( 'woocommerce_customer_pre_search_customers', false, $term, $limit );
if ( is_array( $results ) ) {
return $results;
}
$query = new WP_User_Query(
apply_filters(
'woocommerce_customer_search_customers',
array(
'search' => '*' . esc_attr( $term ) . '*',
'search_columns' => array( 'user_login', 'user_url', 'user_email', 'user_nicename', 'display_name' ),
'fields' => 'ID',
'number' => $limit,
),
$term,
$limit,
'main_query'
)
);
$query2 = new WP_User_Query(
apply_filters(
'woocommerce_customer_search_customers',
array(
'fields' => 'ID',
'number' => $limit,
'meta_query' => array(
'relation' => 'OR',
array(
'key' => 'first_name',
'value' => $term,
'compare' => 'LIKE',
),
array(
'key' => 'last_name',
'value' => $term,
'compare' => 'LIKE',
),
),
),
$term,
$limit,
'meta_query'
)
);
$results = wp_parse_id_list( array_merge( (array) $query->get_results(), (array) $query2->get_results() ) );
if ( $limit && count( $results ) > $limit ) {
$results = array_slice( $results, 0, $limit );
}
return $results;
}
/**
* Get all user ids who have `billing_email` set to any of the email passed in array.
*
* @param array $emails List of emails to check against.
*
* @return array
*/
public function get_user_ids_for_billing_email( $emails ) {
$emails = array_unique( array_map( 'strtolower', array_map( 'sanitize_email', $emails ) ) );
$users_query = new WP_User_Query(
array(
'fields' => 'ID',
'meta_query' => array(
array(
'key' => 'billing_email',
'value' => $emails,
'compare' => 'IN',
),
),
)
);
return array_unique( $users_query->get_results() );
}
}

View File

@ -0,0 +1,521 @@
<?php
/**
* WC_Customer_Download_Data_Store class file.
*
* @package WooCommerce\Classes
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC Customer Download Data Store.
*
* @version 3.0.0
*/
class WC_Customer_Download_Data_Store implements WC_Customer_Download_Data_Store_Interface {
/**
* Names of the database fields for the download permissions table.
*/
const DOWNLOAD_PERMISSION_DB_FIELDS = array(
'download_id',
'product_id',
'user_id',
'user_email',
'order_id',
'order_key',
'downloads_remaining',
'access_granted',
'download_count',
'access_expires',
);
/**
* Create download permission for a user, from an array of data.
*
* @param array $data Data to create the permission for.
* @returns int The database id of the created permission, or false if the permission creation failed.
*/
public function create_from_data( $data ) {
$data = array_intersect_key( $data, array_flip( self::DOWNLOAD_PERMISSION_DB_FIELDS ) );
$id = $this->insert_new_download_permission( $data );
do_action( 'woocommerce_grant_product_download_access', $data );
return $id;
}
/**
* Create download permission for a user.
*
* @param WC_Customer_Download $download WC_Customer_Download object.
*/
public function create( &$download ) {
global $wpdb;
// Always set a access granted date.
if ( is_null( $download->get_access_granted( 'edit' ) ) ) {
$download->set_access_granted( time() );
}
$data = array();
foreach ( self::DOWNLOAD_PERMISSION_DB_FIELDS as $db_field_name ) {
$value = call_user_func( array( $download, 'get_' . $db_field_name ), 'edit' );
$data[ $db_field_name ] = $value;
}
$inserted_id = $this->insert_new_download_permission( $data );
if ( $inserted_id ) {
$download->set_id( $inserted_id );
$download->apply_changes();
}
do_action( 'woocommerce_grant_product_download_access', $data );
}
/**
* Create download permission for a user, from an array of data.
* Assumes that all the keys in the passed data are valid.
*
* @param array $data Data to create the permission for.
* @return int The database id of the created permission, or false if the permission creation failed.
*/
private function insert_new_download_permission( $data ) {
global $wpdb;
// Always set a access granted date.
if ( ! isset( $data['access_granted'] ) ) {
$data['access_granted'] = time();
}
$data['access_granted'] = $this->adjust_date_for_db( $data['access_granted'] );
if ( isset( $data['access_expires'] ) ) {
$data['access_expires'] = $this->adjust_date_for_db( $data['access_expires'] );
}
$format = array(
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
'%d',
'%s',
);
$result = $wpdb->insert(
$wpdb->prefix . 'woocommerce_downloadable_product_permissions',
apply_filters( 'woocommerce_downloadable_file_permission_data', $data ),
apply_filters( 'woocommerce_downloadable_file_permission_format', $format, $data )
);
return $result ? $wpdb->insert_id : false;
}
/**
* Adjust a date value to be inserted in the database.
*
* @param mixed $date The date value. Can be a WC_DateTime, a timestamp, or anything else that "date" recognizes.
* @return string The date converted to 'Y-m-d' format.
* @throws Exception The passed value can't be converted to a date.
*/
private function adjust_date_for_db( $date ) {
if ( 'WC_DateTime' === get_class( $date ) ) {
$date = $date->getTimestamp();
}
$adjusted_date = date( 'Y-m-d', $date ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
if ( $adjusted_date ) {
return $adjusted_date;
}
$msg = sprintf( __( "I don't know how to get a date from a %s", 'woocommerce' ), is_object( $date ) ? get_class( $date ) : gettype( $date ) );
throw new Exception( $msg );
}
/**
* Method to read a download permission from the database.
*
* @param WC_Customer_Download $download WC_Customer_Download object.
*
* @throws Exception Throw exception if invalid download is passed.
*/
public function read( &$download ) {
global $wpdb;
if ( ! $download->get_id() ) {
throw new Exception( __( 'Invalid download.', 'woocommerce' ) );
}
$download->set_defaults();
$raw_download = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE permission_id = %d",
$download->get_id()
)
);
if ( ! $raw_download ) {
throw new Exception( __( 'Invalid download.', 'woocommerce' ) );
}
$download->set_props(
array(
'download_id' => $raw_download->download_id,
'product_id' => $raw_download->product_id,
'user_id' => $raw_download->user_id,
'user_email' => $raw_download->user_email,
'order_id' => $raw_download->order_id,
'order_key' => $raw_download->order_key,
'downloads_remaining' => $raw_download->downloads_remaining,
'access_granted' => strtotime( $raw_download->access_granted ),
'download_count' => $raw_download->download_count,
'access_expires' => is_null( $raw_download->access_expires ) ? null : strtotime( $raw_download->access_expires ),
)
);
$download->set_object_read( true );
}
/**
* Method to update a download in the database.
*
* @param WC_Customer_Download $download WC_Customer_Download object.
*/
public function update( &$download ) {
global $wpdb;
$data = array(
'download_id' => $download->get_download_id( 'edit' ),
'product_id' => $download->get_product_id( 'edit' ),
'user_id' => $download->get_user_id( 'edit' ),
'user_email' => $download->get_user_email( 'edit' ),
'order_id' => $download->get_order_id( 'edit' ),
'order_key' => $download->get_order_key( 'edit' ),
'downloads_remaining' => $download->get_downloads_remaining( 'edit' ),
// phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
'access_granted' => date( 'Y-m-d', $download->get_access_granted( 'edit' )->getTimestamp() ),
'download_count' => $download->get_download_count( 'edit' ),
// phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
'access_expires' => ! is_null( $download->get_access_expires( 'edit' ) ) ? date( 'Y-m-d', $download->get_access_expires( 'edit' )->getTimestamp() ) : null,
);
$format = array(
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
'%d',
'%s',
);
$wpdb->update(
$wpdb->prefix . 'woocommerce_downloadable_product_permissions',
$data,
array(
'permission_id' => $download->get_id(),
),
$format
);
$download->apply_changes();
}
/**
* Method to delete a download permission from the database.
*
* @param WC_Customer_Download $download WC_Customer_Download object.
* @param array $args Array of args to pass to the delete method.
*/
public function delete( &$download, $args = array() ) {
global $wpdb;
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
WHERE permission_id = %d",
$download->get_id()
)
);
$download->set_id( 0 );
}
/**
* Method to delete a download permission from the database by ID.
*
* @param int $id permission_id of the download to be deleted.
*/
public function delete_by_id( $id ) {
global $wpdb;
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
WHERE permission_id = %d",
$id
)
);
}
/**
* Method to delete a download permission from the database by order ID.
*
* @param int $id Order ID of the downloads that will be deleted.
*/
public function delete_by_order_id( $id ) {
global $wpdb;
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
WHERE order_id = %d",
$id
)
);
}
/**
* Method to delete a download permission from the database by download ID.
*
* @param int $id download_id of the downloads that will be deleted.
*/
public function delete_by_download_id( $id ) {
global $wpdb;
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
WHERE download_id = %s",
$id
)
);
}
/**
* Method to delete a download permission from the database by user ID.
*
* @since 3.4.0
* @param int $id user ID of the downloads that will be deleted.
* @return bool True if deleted rows.
*/
public function delete_by_user_id( $id ) {
global $wpdb;
return (bool) $wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
WHERE user_id = %d",
$id
)
);
}
/**
* Method to delete a download permission from the database by user email.
*
* @since 3.4.0
* @param string $email email of the downloads that will be deleted.
* @return bool True if deleted rows.
*/
public function delete_by_user_email( $email ) {
global $wpdb;
return (bool) $wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
WHERE user_email = %s",
$email
)
);
}
/**
* Get a download object.
*
* @param array $data From the DB.
* @return WC_Customer_Download
*/
private function get_download( $data ) {
return new WC_Customer_Download( $data );
}
/**
* Get array of download ids by specified args.
*
* @param array $args Arguments to filter downloads. $args['return'] accepts the following values: 'objects' (default), 'ids' or a comma separeted list of fields (for example: 'order_id,user_id,user_email').
* @return array Can be an array of permission_ids, an array of WC_Customer_Download objects or an array of arrays containing specified fields depending on the value of $args['return'].
*/
public function get_downloads( $args = array() ) {
global $wpdb;
$args = wp_parse_args(
$args,
array(
'user_email' => '',
'user_id' => '',
'order_id' => '',
'order_key' => '',
'product_id' => '',
'download_id' => '',
'orderby' => 'permission_id',
'order' => 'ASC',
'limit' => -1,
'page' => 1,
'return' => 'objects',
)
);
$valid_fields = array( 'permission_id', 'download_id', 'product_id', 'order_id', 'order_key', 'user_email', 'user_id', 'downloads_remaining', 'access_granted', 'access_expires', 'download_count' );
$get_results_output = ARRAY_A;
if ( 'ids' === $args['return'] ) {
$fields = 'permission_id';
} elseif ( 'objects' === $args['return'] ) {
$fields = '*';
$get_results_output = OBJECT;
} else {
$fields = explode( ',', (string) $args['return'] );
$fields = implode( ', ', array_intersect( $fields, $valid_fields ) );
}
$query = array();
$query[] = "SELECT {$fields} FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE 1=1";
if ( $args['user_email'] ) {
$query[] = $wpdb->prepare( 'AND user_email = %s', sanitize_email( $args['user_email'] ) );
}
if ( $args['user_id'] ) {
$query[] = $wpdb->prepare( 'AND user_id = %d', absint( $args['user_id'] ) );
}
if ( $args['order_id'] ) {
$query[] = $wpdb->prepare( 'AND order_id = %d', $args['order_id'] );
}
if ( $args['order_key'] ) {
$query[] = $wpdb->prepare( 'AND order_key = %s', $args['order_key'] );
}
if ( $args['product_id'] ) {
$query[] = $wpdb->prepare( 'AND product_id = %d', $args['product_id'] );
}
if ( $args['download_id'] ) {
$query[] = $wpdb->prepare( 'AND download_id = %s', $args['download_id'] );
}
$orderby = in_array( $args['orderby'], $valid_fields, true ) ? $args['orderby'] : 'permission_id';
$order = 'DESC' === strtoupper( $args['order'] ) ? 'DESC' : 'ASC';
$orderby_sql = sanitize_sql_orderby( "{$orderby} {$order}" );
$query[] = "ORDER BY {$orderby_sql}";
if ( 0 < $args['limit'] ) {
$query[] = $wpdb->prepare( 'LIMIT %d, %d', absint( $args['limit'] ) * absint( $args['page'] - 1 ), absint( $args['limit'] ) );
}
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$results = $wpdb->get_results( implode( ' ', $query ), $get_results_output );
switch ( $args['return'] ) {
case 'ids':
return wp_list_pluck( $results, 'permission_id' );
case 'objects':
return array_map( array( $this, 'get_download' ), $results );
default:
return $results;
}
}
/**
* Update download ids if the hash changes.
*
* @deprecated 3.3.0 Download id is now a static UUID and should not be changed based on file hash.
*
* @param int $product_id Product ID.
* @param string $old_id Old download_id.
* @param string $new_id New download_id.
*/
public function update_download_id( $product_id, $old_id, $new_id ) {
global $wpdb;
wc_deprecated_function( __METHOD__, '3.3' );
$wpdb->update(
$wpdb->prefix . 'woocommerce_downloadable_product_permissions',
array(
'download_id' => $new_id,
),
array(
'download_id' => $old_id,
'product_id' => $product_id,
)
);
}
/**
* Get a customers downloads.
*
* @param int $customer_id Customer ID.
* @return array
*/
public function get_downloads_for_customer( $customer_id ) {
global $wpdb;
return $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions as permissions
WHERE user_id = %d
AND permissions.order_id > 0
AND
(
permissions.downloads_remaining > 0
OR permissions.downloads_remaining = ''
)
AND
(
permissions.access_expires IS NULL
OR permissions.access_expires >= %s
OR permissions.access_expires = '0000-00-00 00:00:00'
)
ORDER BY permissions.order_id, permissions.product_id, permissions.permission_id;",
$customer_id,
date( 'Y-m-d', current_time( 'timestamp' ) ) // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
)
);
}
/**
* Update user prop for downloads based on order id.
*
* @param int $order_id Order ID.
* @param int $customer_id Customer ID.
* @param string $email Customer email address.
*/
public function update_user_by_order_id( $order_id, $customer_id, $email ) {
global $wpdb;
$wpdb->update(
$wpdb->prefix . 'woocommerce_downloadable_product_permissions',
array(
'user_id' => $customer_id,
'user_email' => $email,
),
array(
'order_id' => $order_id,
),
array(
'%d',
'%s',
),
array(
'%d',
)
);
}
}

View File

@ -0,0 +1,239 @@
<?php
/**
* Class WC_Customer_Download_Log_Data_Store file.
*
* @version 3.3.0
* @package WooCommerce\Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* WC_Customer_Download_Log_Data_Store class.
*/
class WC_Customer_Download_Log_Data_Store implements WC_Customer_Download_Log_Data_Store_Interface {
// Table name for download logs.
const WC_DOWNLOAD_LOG_TABLE = 'wc_download_log';
/**
* Get the table name for download logs.
*
* @return string
*/
public static function get_table_name() {
return self::WC_DOWNLOAD_LOG_TABLE;
}
/**
* Create download log entry.
*
* @param WC_Customer_Download_Log $download_log Customer download log object.
*/
public function create( WC_Customer_Download_Log &$download_log ) {
global $wpdb;
// Always set a timestamp.
if ( is_null( $download_log->get_timestamp( 'edit' ) ) ) {
$download_log->set_timestamp( time() );
}
$data = array(
'timestamp' => date( 'Y-m-d H:i:s', $download_log->get_timestamp( 'edit' )->getTimestamp() ),
'permission_id' => $download_log->get_permission_id( 'edit' ),
'user_id' => $download_log->get_user_id( 'edit' ),
'user_ip_address' => $download_log->get_user_ip_address( 'edit' ),
);
$format = array(
'%s',
'%s',
'%s',
'%s',
);
$result = $wpdb->insert(
$wpdb->prefix . self::get_table_name(),
apply_filters( 'woocommerce_downloadable_product_download_log_insert_data', $data ),
apply_filters( 'woocommerce_downloadable_product_download_log_insert_format', $format, $data )
);
do_action( 'woocommerce_downloadable_product_download_log_insert', $data );
if ( $result ) {
$download_log->set_id( $wpdb->insert_id );
$download_log->apply_changes();
} else {
wp_die( esc_html__( 'Unable to insert download log entry in database.', 'woocommerce' ) );
}
}
/**
* Method to read a download log from the database.
*
* @param WC_Customer_Download_Log $download_log Download log object.
* @throws Exception Exception when read is not possible.
*/
public function read( &$download_log ) {
global $wpdb;
$download_log->set_defaults();
// Ensure we have an id to pull from the DB.
if ( ! $download_log->get_id() ) {
throw new Exception( __( 'Invalid download log: no ID.', 'woocommerce' ) );
}
$table = $wpdb->prefix . self::get_table_name();
// Query the DB for the download log.
$raw_download_log = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE download_log_id = %d", $download_log->get_id() ) ); // WPCS: unprepared SQL ok.
if ( ! $raw_download_log ) {
throw new Exception( __( 'Invalid download log: not found.', 'woocommerce' ) );
}
$download_log->set_props(
array(
'timestamp' => strtotime( $raw_download_log->timestamp ),
'permission_id' => $raw_download_log->permission_id,
'user_id' => $raw_download_log->user_id,
'user_ip_address' => $raw_download_log->user_ip_address,
)
);
$download_log->set_object_read( true );
}
/**
* Method to update a download log in the database.
*
* @param WC_Customer_Download_Log $download_log Download log object.
*/
public function update( &$download_log ) {
global $wpdb;
$data = array(
'timestamp' => date( 'Y-m-d H:i:s', $download_log->get_timestamp( 'edit' )->getTimestamp() ),
'permission_id' => $download_log->get_permission_id( 'edit' ),
'user_id' => $download_log->get_user_id( 'edit' ),
'user_ip_address' => $download_log->get_user_ip_address( 'edit' ),
);
$format = array(
'%s',
'%s',
'%s',
'%s',
);
$wpdb->update(
$wpdb->prefix . self::get_table_name(),
$data,
array(
'download_log_id' => $download_log->get_id(),
),
$format
);
$download_log->apply_changes();
}
/**
* Get a download log object.
*
* @param array $data From the DB.
* @return WC_Customer_Download_Log
*/
private function get_download_log( $data ) {
return new WC_Customer_Download_Log( $data );
}
/**
* Get array of download log ids by specified args.
*
* @param array $args Arguments to define download logs to retrieve.
* @return array
*/
public function get_download_logs( $args = array() ) {
global $wpdb;
$args = wp_parse_args(
$args,
array(
'permission_id' => '',
'user_id' => '',
'user_ip_address' => '',
'orderby' => 'download_log_id',
'order' => 'ASC',
'limit' => -1,
'page' => 1,
'return' => 'objects',
)
);
$query = array();
$table = $wpdb->prefix . self::get_table_name();
$query[] = "SELECT * FROM {$table} WHERE 1=1";
if ( $args['permission_id'] ) {
$query[] = $wpdb->prepare( 'AND permission_id = %d', $args['permission_id'] );
}
if ( $args['user_id'] ) {
$query[] = $wpdb->prepare( 'AND user_id = %d', $args['user_id'] );
}
if ( $args['user_ip_address'] ) {
$query[] = $wpdb->prepare( 'AND user_ip_address = %s', $args['user_ip_address'] );
}
$allowed_orders = array( 'download_log_id', 'timestamp', 'permission_id', 'user_id' );
$orderby = in_array( $args['orderby'], $allowed_orders, true ) ? $args['orderby'] : 'download_log_id';
$order = 'DESC' === strtoupper( $args['order'] ) ? 'DESC' : 'ASC';
$orderby_sql = sanitize_sql_orderby( "{$orderby} {$order}" );
$query[] = "ORDER BY {$orderby_sql}";
if ( 0 < $args['limit'] ) {
$query[] = $wpdb->prepare( 'LIMIT %d, %d', absint( $args['limit'] ) * absint( $args['page'] - 1 ), absint( $args['limit'] ) );
}
$raw_download_logs = $wpdb->get_results( implode( ' ', $query ) ); // WPCS: unprepared SQL ok.
switch ( $args['return'] ) {
case 'ids':
return wp_list_pluck( $raw_download_logs, 'download_log_id' );
default:
return array_map( array( $this, 'get_download_log' ), $raw_download_logs );
}
}
/**
* Get download logs for a given download permission.
*
* @param int $permission_id Permission to get logs for.
* @return array
*/
public function get_download_logs_for_permission( $permission_id ) {
// If no permission_id is passed, return an empty array.
if ( empty( $permission_id ) ) {
return array();
}
return $this->get_download_logs(
array(
'permission_id' => $permission_id,
)
);
}
/**
* Method to delete download logs for a given permission ID.
*
* @since 3.4.0
* @param int $id download_id of the downloads that will be deleted.
*/
public function delete_by_permission_id( $id ) {
global $wpdb;
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE permission_id = %d", $id ) );
}
}

View File

@ -0,0 +1,658 @@
<?php
/**
* Shared logic for WP based data.
* Contains functions like meta handling for all default data stores.
* Your own data store doesn't need to use WC_Data_Store_WP -- you can write
* your own meta handling functions.
*
* @version 3.0.0
* @package WooCommerce\Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* WC_Data_Store_WP class.
*/
class WC_Data_Store_WP {
/**
* Meta type. This should match up with
* the types available at https://developer.wordpress.org/reference/functions/add_metadata/.
* WP defines 'post', 'user', 'comment', and 'term'.
*
* @var string
*/
protected $meta_type = 'post';
/**
* This only needs set if you are using a custom metadata type (for example payment tokens.
* This should be the name of the field your table uses for associating meta with objects.
* For example, in payment_tokenmeta, this would be payment_token_id.
*
* @var string
*/
protected $object_id_field_for_meta = '';
/**
* Data stored in meta keys, but not considered "meta" for an object.
*
* @since 3.0.0
*
* @var array
*/
protected $internal_meta_keys = array();
/**
* Meta data which should exist in the DB, even if empty.
*
* @since 3.6.0
*
* @var array
*/
protected $must_exist_meta_keys = array();
/**
* Get and store terms from a taxonomy.
*
* @since 3.0.0
* @param WC_Data|integer $object WC_Data object or object ID.
* @param string $taxonomy Taxonomy name e.g. product_cat.
* @return array of terms
*/
protected function get_term_ids( $object, $taxonomy ) {
if ( is_numeric( $object ) ) {
$object_id = $object;
} else {
$object_id = $object->get_id();
}
$terms = get_the_terms( $object_id, $taxonomy );
if ( false === $terms || is_wp_error( $terms ) ) {
return array();
}
return wp_list_pluck( $terms, 'term_id' );
}
/**
* Returns an array of meta for an object.
*
* @since 3.0.0
* @param WC_Data $object WC_Data object.
* @return array
*/
public function read_meta( &$object ) {
global $wpdb;
$db_info = $this->get_db_info();
$raw_meta_data = $wpdb->get_results(
$wpdb->prepare(
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
"SELECT {$db_info['meta_id_field']} as meta_id, meta_key, meta_value
FROM {$db_info['table']}
WHERE {$db_info['object_id_field']} = %d
ORDER BY {$db_info['meta_id_field']}",
// phpcs:enable
$object->get_id()
)
);
return $this->filter_raw_meta_data( $object, $raw_meta_data );
}
/**
* Helper method to filter internal meta keys from all meta data rows for the object.
*
* @since 4.7.0
*
* @param WC_Data $object WC_Data object.
* @param array $raw_meta_data Array of std object of meta data to be filtered.
*
* @return mixed|void
*/
public function filter_raw_meta_data( &$object, $raw_meta_data ) {
$this->internal_meta_keys = array_merge( array_map( array( $this, 'prefix_key' ), $object->get_data_keys() ), $this->internal_meta_keys );
$meta_data = array_filter( $raw_meta_data, array( $this, 'exclude_internal_meta_keys' ) );
return apply_filters( "woocommerce_data_store_wp_{$this->meta_type}_read_meta", $meta_data, $object, $this );
}
/**
* Deletes meta based on meta ID.
*
* @since 3.0.0
* @param WC_Data $object WC_Data object.
* @param stdClass $meta (containing at least ->id).
*/
public function delete_meta( &$object, $meta ) {
delete_metadata_by_mid( $this->meta_type, $meta->id );
}
/**
* Add new piece of meta.
*
* @since 3.0.0
* @param WC_Data $object WC_Data object.
* @param stdClass $meta (containing ->key and ->value).
* @return int meta ID
*/
public function add_meta( &$object, $meta ) {
return add_metadata( $this->meta_type, $object->get_id(), wp_slash( $meta->key ), is_string( $meta->value ) ? wp_slash( $meta->value ) : $meta->value, false );
}
/**
* Update meta.
*
* @since 3.0.0
* @param WC_Data $object WC_Data object.
* @param stdClass $meta (containing ->id, ->key and ->value).
*/
public function update_meta( &$object, $meta ) {
update_metadata_by_mid( $this->meta_type, $meta->id, $meta->value, $meta->key );
}
/**
* Table structure is slightly different between meta types, this function will return what we need to know.
*
* @since 3.0.0
* @return array Array elements: table, object_id_field, meta_id_field
*/
protected function get_db_info() {
global $wpdb;
$meta_id_field = 'meta_id'; // for some reason users calls this umeta_id so we need to track this as well.
$table = $wpdb->prefix;
// If we are dealing with a type of metadata that is not a core type, the table should be prefixed.
if ( ! in_array( $this->meta_type, array( 'post', 'user', 'comment', 'term' ), true ) ) {
$table .= 'woocommerce_';
}
$table .= $this->meta_type . 'meta';
$object_id_field = $this->meta_type . '_id';
// Figure out our field names.
if ( 'user' === $this->meta_type ) {
$meta_id_field = 'umeta_id';
$table = $wpdb->usermeta;
}
if ( ! empty( $this->object_id_field_for_meta ) ) {
$object_id_field = $this->object_id_field_for_meta;
}
return array(
'table' => $table,
'object_id_field' => $object_id_field,
'meta_id_field' => $meta_id_field,
);
}
/**
* Internal meta keys we don't want exposed as part of meta_data. This is in
* addition to all data props with _ prefix.
*
* @since 2.6.0
*
* @param string $key Prefix to be added to meta keys.
* @return string
*/
protected function prefix_key( $key ) {
return '_' === substr( $key, 0, 1 ) ? $key : '_' . $key;
}
/**
* Callback to remove unwanted meta data.
*
* @param object $meta Meta object to check if it should be excluded or not.
* @return bool
*/
protected function exclude_internal_meta_keys( $meta ) {
return ! in_array( $meta->meta_key, $this->internal_meta_keys, true ) && 0 !== stripos( $meta->meta_key, 'wp_' );
}
/**
* Gets a list of props and meta keys that need updated based on change state
* or if they are present in the database or not.
*
* @param WC_Data $object The WP_Data object (WC_Coupon for coupons, etc).
* @param array $meta_key_to_props A mapping of meta keys => prop names.
* @param string $meta_type The internal WP meta type (post, user, etc).
* @return array A mapping of meta keys => prop names, filtered by ones that should be updated.
*/
protected function get_props_to_update( $object, $meta_key_to_props, $meta_type = 'post' ) {
$props_to_update = array();
$changed_props = $object->get_changes();
// Props should be updated if they are a part of the $changed array or don't exist yet.
foreach ( $meta_key_to_props as $meta_key => $prop ) {
if ( array_key_exists( $prop, $changed_props ) || ! metadata_exists( $meta_type, $object->get_id(), $meta_key ) ) {
$props_to_update[ $meta_key ] = $prop;
}
}
return $props_to_update;
}
/**
* Update meta data in, or delete it from, the database.
*
* Avoids storing meta when it's either an empty string or empty array.
* Other empty values such as numeric 0 and null should still be stored.
* Data-stores can force meta to exist using `must_exist_meta_keys`.
*
* Note: WordPress `get_metadata` function returns an empty string when meta data does not exist.
*
* @param WC_Data $object The WP_Data object (WC_Coupon for coupons, etc).
* @param string $meta_key Meta key to update.
* @param mixed $meta_value Value to save.
*
* @since 3.6.0 Added to prevent empty meta being stored unless required.
*
* @return bool True if updated/deleted.
*/
protected function update_or_delete_post_meta( $object, $meta_key, $meta_value ) {
if ( in_array( $meta_value, array( array(), '' ), true ) && ! in_array( $meta_key, $this->must_exist_meta_keys, true ) ) {
$updated = delete_post_meta( $object->get_id(), $meta_key );
} else {
$updated = update_post_meta( $object->get_id(), $meta_key, $meta_value );
}
return (bool) $updated;
}
/**
* Get valid WP_Query args from a WC_Object_Query's query variables.
*
* @since 3.1.0
* @param array $query_vars query vars from a WC_Object_Query.
* @return array
*/
protected function get_wp_query_args( $query_vars ) {
$skipped_values = array( '', array(), null );
$wp_query_args = array(
'errors' => array(),
'meta_query' => array(), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
);
foreach ( $query_vars as $key => $value ) {
if ( in_array( $value, $skipped_values, true ) || 'meta_query' === $key ) {
continue;
}
// Build meta queries out of vars that are stored in internal meta keys.
if ( in_array( '_' . $key, $this->internal_meta_keys, true ) ) {
// Check for existing values if wildcard is used.
if ( '*' === $value ) {
$wp_query_args['meta_query'][] = array(
array(
'key' => '_' . $key,
'compare' => 'EXISTS',
),
array(
'key' => '_' . $key,
'value' => '',
'compare' => '!=',
),
);
} else {
$wp_query_args['meta_query'][] = array(
'key' => '_' . $key,
'value' => $value,
'compare' => is_array( $value ) ? 'IN' : '=',
);
}
} else { // Other vars get mapped to wp_query args or just left alone.
$key_mapping = array(
'parent' => 'post_parent',
'parent_exclude' => 'post_parent__not_in',
'exclude' => 'post__not_in',
'limit' => 'posts_per_page',
'type' => 'post_type',
'return' => 'fields',
);
if ( isset( $key_mapping[ $key ] ) ) {
$wp_query_args[ $key_mapping[ $key ] ] = $value;
} else {
$wp_query_args[ $key ] = $value;
}
}
}
return apply_filters( 'woocommerce_get_wp_query_args', $wp_query_args, $query_vars );
}
/**
* Map a valid date query var to WP_Query arguments.
* Valid date formats: YYYY-MM-DD or timestamp, possibly combined with an operator from $valid_operators.
* Also accepts a WC_DateTime object.
*
* @since 3.2.0
* @param mixed $query_var A valid date format.
* @param string $key meta or db column key.
* @param array $wp_query_args WP_Query args.
* @return array Modified $wp_query_args
*/
public function parse_date_for_wp_query( $query_var, $key, $wp_query_args = array() ) {
$query_parse_regex = '/([^.<>]*)(>=|<=|>|<|\.\.\.)([^.<>]+)/';
$valid_operators = array( '>', '>=', '=', '<=', '<', '...' );
// YYYY-MM-DD queries have 'day' precision. Timestamp/WC_DateTime queries have 'second' precision.
$precision = 'second';
$dates = array();
$operator = '=';
try {
// Specific time query with a WC_DateTime.
if ( is_a( $query_var, 'WC_DateTime' ) ) {
$dates[] = $query_var;
} elseif ( is_numeric( $query_var ) ) { // Specific time query with a timestamp.
$dates[] = new WC_DateTime( "@{$query_var}", new DateTimeZone( 'UTC' ) );
} elseif ( preg_match( $query_parse_regex, $query_var, $sections ) ) { // Query with operators and possible range of dates.
if ( ! empty( $sections[1] ) ) {
$dates[] = is_numeric( $sections[1] ) ? new WC_DateTime( "@{$sections[1]}", new DateTimeZone( 'UTC' ) ) : wc_string_to_datetime( $sections[1] );
}
$operator = in_array( $sections[2], $valid_operators, true ) ? $sections[2] : '';
$dates[] = is_numeric( $sections[3] ) ? new WC_DateTime( "@{$sections[3]}", new DateTimeZone( 'UTC' ) ) : wc_string_to_datetime( $sections[3] );
if ( ! is_numeric( $sections[1] ) && ! is_numeric( $sections[3] ) ) {
$precision = 'day';
}
} else { // Specific time query with a string.
$dates[] = wc_string_to_datetime( $query_var );
$precision = 'day';
}
} catch ( Exception $e ) {
return $wp_query_args;
}
// Check for valid inputs.
if ( ! $operator || empty( $dates ) || ( '...' === $operator && count( $dates ) < 2 ) ) {
return $wp_query_args;
}
// Build date query for 'post_date' or 'post_modified' keys.
if ( 'post_date' === $key || 'post_modified' === $key ) {
if ( ! isset( $wp_query_args['date_query'] ) ) {
$wp_query_args['date_query'] = array();
}
$query_arg = array(
'column' => 'day' === $precision ? $key : $key . '_gmt',
'inclusive' => '>' !== $operator && '<' !== $operator,
);
// Add 'before'/'after' query args.
$comparisons = array();
if ( '>' === $operator || '>=' === $operator || '...' === $operator ) {
$comparisons[] = 'after';
}
if ( '<' === $operator || '<=' === $operator || '...' === $operator ) {
$comparisons[] = 'before';
}
foreach ( $comparisons as $index => $comparison ) {
if ( 'day' === $precision ) {
/**
* WordPress doesn't generate the correct SQL for inclusive day queries with both a 'before' and
* 'after' string query, so we have to use the array format in 'day' precision.
*
* @see https://core.trac.wordpress.org/ticket/29908
*/
$query_arg[ $comparison ]['year'] = $dates[ $index ]->date( 'Y' );
$query_arg[ $comparison ]['month'] = $dates[ $index ]->date( 'n' );
$query_arg[ $comparison ]['day'] = $dates[ $index ]->date( 'j' );
} else {
/**
* WordPress doesn't support 'hour'/'second'/'minute' in array format 'before'/'after' queries,
* so we have to use a string query.
*/
$query_arg[ $comparison ] = gmdate( 'm/d/Y H:i:s', $dates[ $index ]->getTimestamp() );
}
}
if ( empty( $comparisons ) ) {
$query_arg['year'] = $dates[0]->date( 'Y' );
$query_arg['month'] = $dates[0]->date( 'n' );
$query_arg['day'] = $dates[0]->date( 'j' );
if ( 'second' === $precision ) {
$query_arg['hour'] = $dates[0]->date( 'H' );
$query_arg['minute'] = $dates[0]->date( 'i' );
$query_arg['second'] = $dates[0]->date( 's' );
}
}
$wp_query_args['date_query'][] = $query_arg;
return $wp_query_args;
}
// Build meta query for unrecognized keys.
if ( ! isset( $wp_query_args['meta_query'] ) ) {
$wp_query_args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
}
// Meta dates are stored as timestamps in the db.
// Check against beginning/end-of-day timestamps when using 'day' precision.
if ( 'day' === $precision ) {
$start_timestamp = strtotime( gmdate( 'm/d/Y 00:00:00', $dates[0]->getTimestamp() ) );
$end_timestamp = '...' !== $operator ? ( $start_timestamp + DAY_IN_SECONDS ) : strtotime( gmdate( 'm/d/Y 00:00:00', $dates[1]->getTimestamp() ) );
switch ( $operator ) {
case '>':
case '<=':
$wp_query_args['meta_query'][] = array(
'key' => $key,
'value' => $end_timestamp,
'compare' => $operator,
);
break;
case '<':
case '>=':
$wp_query_args['meta_query'][] = array(
'key' => $key,
'value' => $start_timestamp,
'compare' => $operator,
);
break;
default:
$wp_query_args['meta_query'][] = array(
'key' => $key,
'value' => $start_timestamp,
'compare' => '>=',
);
$wp_query_args['meta_query'][] = array(
'key' => $key,
'value' => $end_timestamp,
'compare' => '<=',
);
}
} else {
if ( '...' !== $operator ) {
$wp_query_args['meta_query'][] = array(
'key' => $key,
'value' => $dates[0]->getTimestamp(),
'compare' => $operator,
);
} else {
$wp_query_args['meta_query'][] = array(
'key' => $key,
'value' => $dates[0]->getTimestamp(),
'compare' => '>=',
);
$wp_query_args['meta_query'][] = array(
'key' => $key,
'value' => $dates[1]->getTimestamp(),
'compare' => '<=',
);
}
}
return $wp_query_args;
}
/**
* Return list of internal meta keys.
*
* @since 3.2.0
* @return array
*/
public function get_internal_meta_keys() {
return $this->internal_meta_keys;
}
/**
* Check if the terms are suitable for searching.
*
* Uses an array of stopwords (terms) that are excluded from the separate
* term matching when searching for posts. The list of English stopwords is
* the approximate search engines list, and is translatable.
*
* @since 3.4.0
* @param array $terms Terms to check.
* @return array Terms that are not stopwords.
*/
protected function get_valid_search_terms( $terms ) {
$valid_terms = array();
$stopwords = $this->get_search_stopwords();
foreach ( $terms as $term ) {
// keep before/after spaces when term is for exact match, otherwise trim quotes and spaces.
if ( preg_match( '/^".+"$/', $term ) ) {
$term = trim( $term, "\"'" );
} else {
$term = trim( $term, "\"' " );
}
// Avoid single A-Z and single dashes.
if ( empty( $term ) || ( 1 === strlen( $term ) && preg_match( '/^[a-z\-]$/i', $term ) ) ) {
continue;
}
if ( in_array( wc_strtolower( $term ), $stopwords, true ) ) {
continue;
}
$valid_terms[] = $term;
}
return $valid_terms;
}
/**
* Retrieve stopwords used when parsing search terms.
*
* @since 3.4.0
* @return array Stopwords.
*/
protected function get_search_stopwords() {
// Translators: This is a comma-separated list of very common words that should be excluded from a search, like a, an, and the. These are usually called "stopwords". You should not simply translate these individual words into your language. Instead, look for and provide commonly accepted stopwords in your language.
$stopwords = array_map(
'wc_strtolower',
array_map(
'trim',
explode(
',',
_x(
'about,an,are,as,at,be,by,com,for,from,how,in,is,it,of,on,or,that,the,this,to,was,what,when,where,who,will,with,www',
'Comma-separated list of search stopwords in your language',
'woocommerce'
)
)
)
);
return apply_filters( 'wp_search_stopwords', $stopwords );
}
/**
* Get data to save to a lookup table.
*
* @since 3.6.0
* @param int $id ID of object to update.
* @param string $table Lookup table name.
* @return array
*/
protected function get_data_for_lookup_table( $id, $table ) {
return array();
}
/**
* Get primary key name for lookup table.
*
* @since 3.6.0
* @param string $table Lookup table name.
* @return string
*/
protected function get_primary_key_for_lookup_table( $table ) {
return '';
}
/**
* Update a lookup table for an object.
*
* @since 3.6.0
* @param int $id ID of object to update.
* @param string $table Lookup table name.
*
* @return NULL
*/
protected function update_lookup_table( $id, $table ) {
global $wpdb;
$id = absint( $id );
$table = sanitize_key( $table );
if ( empty( $id ) || empty( $table ) ) {
return false;
}
$existing_data = wp_cache_get( 'lookup_table', 'object_' . $id );
$update_data = $this->get_data_for_lookup_table( $id, $table );
if ( ! empty( $update_data ) && $update_data !== $existing_data ) {
$wpdb->replace(
$wpdb->$table,
$update_data
);
wp_cache_set( 'lookup_table', $update_data, 'object_' . $id );
}
}
/**
* Delete lookup table data for an ID.
*
* @since 3.6.0
* @param int $id ID of object to update.
* @param string $table Lookup table name.
*/
public function delete_from_lookup_table( $id, $table ) {
global $wpdb;
$id = absint( $id );
$table = sanitize_key( $table );
if ( empty( $id ) || empty( $table ) ) {
return false;
}
$pk = $this->get_primary_key_for_lookup_table( $table );
$wpdb->delete(
$wpdb->$table,
array(
$pk => $id,
)
);
wp_cache_delete( 'lookup_table', 'object_' . $id );
}
/**
* Converts a WP post date string into a timestamp.
*
* @since 4.8.0
*
* @param string $time_string The WP post date string.
* @return int|null The date string converted to a timestamp or null.
*/
protected function string_to_timestamp( $time_string ) {
return '0000-00-00 00:00:00' !== $time_string ? wc_string_to_timestamp( $time_string ) : null;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,62 @@
<?php
/**
* Class WC_Order_Item_Coupon_Data_Store file.
*
* @package WooCommerce\DataStores
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC Order Item Coupon Data Store
*
* @version 3.0.0
*/
class WC_Order_Item_Coupon_Data_Store extends Abstract_WC_Order_Item_Type_Data_Store implements WC_Object_Data_Store_Interface, WC_Order_Item_Type_Data_Store_Interface {
/**
* Data stored in meta keys.
*
* @since 3.0.0
* @var array
*/
protected $internal_meta_keys = array( 'discount_amount', 'discount_amount_tax' );
/**
* Read/populate data properties specific to this order item.
*
* @since 3.0.0
* @param WC_Order_Item_Coupon $item Coupon order item.
*/
public function read( &$item ) {
parent::read( $item );
$id = $item->get_id();
$item->set_props(
array(
'discount' => get_metadata( 'order_item', $id, 'discount_amount', true ),
'discount_tax' => get_metadata( 'order_item', $id, 'discount_amount_tax', true ),
)
);
$item->set_object_read( true );
}
/**
* Saves an item's data to the database / item meta.
* Ran after both create and update, so $item->get_id() will be set.
*
* @since 3.0.0
* @param WC_Order_Item_Coupon $item Coupon order item.
*/
public function save_item_data( &$item ) {
$id = $item->get_id();
$save_values = array(
'discount_amount' => $item->get_discount( 'edit' ),
'discount_amount_tax' => $item->get_discount_tax( 'edit' ),
);
foreach ( $save_values as $key => $value ) {
update_metadata( 'order_item', $id, $key, $value );
}
}
}

View File

@ -0,0 +1,189 @@
<?php
/**
* Class WC_Order_Item_Data_Store file.
*
* @package WooCommerce\DataStores
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC Order Item Data Store: Misc Order Item Data functions.
*
* @version 3.0.0
*/
class WC_Order_Item_Data_Store implements WC_Order_Item_Data_Store_Interface {
/**
* Add an order item to an order.
*
* @since 3.0.0
* @param int $order_id Order ID.
* @param array $item order_item_name and order_item_type.
* @return int Order Item ID
*/
public function add_order_item( $order_id, $item ) {
global $wpdb;
$wpdb->insert(
$wpdb->prefix . 'woocommerce_order_items',
array(
'order_item_name' => $item['order_item_name'],
'order_item_type' => $item['order_item_type'],
'order_id' => $order_id,
),
array(
'%s',
'%s',
'%d',
)
);
$item_id = absint( $wpdb->insert_id );
$this->clear_caches( $item_id, $order_id );
return $item_id;
}
/**
* Update an order item.
*
* @since 3.0.0
* @param int $item_id Item ID.
* @param array $item order_item_name or order_item_type.
* @return boolean
*/
public function update_order_item( $item_id, $item ) {
global $wpdb;
$updated = $wpdb->update( $wpdb->prefix . 'woocommerce_order_items', $item, array( 'order_item_id' => $item_id ) );
$this->clear_caches( $item_id, null );
return $updated;
}
/**
* Delete an order item.
*
* @since 3.0.0
* @param int $item_id Item ID.
*/
public function delete_order_item( $item_id ) {
// Load the order ID before the deletion, since after, it won't exist in the database.
$order_id = $this->get_order_id_by_order_item_id( $item_id );
global $wpdb;
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d", $item_id ) );
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE order_item_id = %d", $item_id ) );
$this->clear_caches( $item_id, $order_id );
}
/**
* Update term meta.
*
* @since 3.0.0
* @param int $item_id Item ID.
* @param string $meta_key Meta key.
* @param mixed $meta_value Meta value.
* @param string $prev_value (default: '').
* @return bool
*/
public function update_metadata( $item_id, $meta_key, $meta_value, $prev_value = '' ) {
return update_metadata( 'order_item', $item_id, $meta_key, is_string( $meta_value ) ? wp_slash( $meta_value ) : $meta_value, $prev_value );
}
/**
* Add term meta.
*
* @since 3.0.0
* @param int $item_id Item ID.
* @param string $meta_key Meta key.
* @param mixed $meta_value Meta value.
* @param bool $unique (default: false).
* @return int New row ID or 0
*/
public function add_metadata( $item_id, $meta_key, $meta_value, $unique = false ) {
return add_metadata( 'order_item', $item_id, wp_slash( $meta_key ), is_string( $meta_value ) ? wp_slash( $meta_value ) : $meta_value, $unique );
}
/**
* Delete term meta.
*
* @since 3.0.0
* @param int $item_id Item ID.
* @param string $meta_key Meta key.
* @param string $meta_value (default: '').
* @param bool $delete_all (default: false).
* @return bool
*/
public function delete_metadata( $item_id, $meta_key, $meta_value = '', $delete_all = false ) {
return delete_metadata( 'order_item', $item_id, $meta_key, is_string( $meta_value ) ? wp_slash( $meta_value ) : $meta_value, $delete_all );
}
/**
* Get term meta.
*
* @since 3.0.0
* @param int $item_id Item ID.
* @param string $key Meta key.
* @param bool $single (default: true).
* @return mixed
*/
public function get_metadata( $item_id, $key, $single = true ) {
return get_metadata( 'order_item', $item_id, $key, $single );
}
/**
* Get order ID by order item ID.
*
* @since 3.0.0
* @param int $item_id Item ID.
* @return int
*/
public function get_order_id_by_order_item_id( $item_id ) {
global $wpdb;
return (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT order_id FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d",
$item_id
)
);
}
/**
* Get the order item type based on Item ID.
*
* @since 3.0.0
* @param int $item_id Item ID.
* @return string|null Order item type or null if no order item entry found.
*/
public function get_order_item_type( $item_id ) {
global $wpdb;
$order_item_type = $wpdb->get_var(
$wpdb->prepare(
"SELECT order_item_type FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d LIMIT 1;",
$item_id
)
);
return $order_item_type;
}
/**
* Clear meta cache.
*
* @param int $item_id Item ID.
* @param int|null $order_id Order ID. If not set, it will be loaded using the item ID.
*/
protected function clear_caches( $item_id, $order_id ) {
wp_cache_delete( 'item-' . $item_id, 'order-items' );
if ( ! $order_id ) {
$order_id = $this->get_order_id_by_order_item_id( $item_id );
}
if ( $order_id ) {
wp_cache_delete( 'order-items-' . $order_id, 'orders' );
}
}
}

View File

@ -0,0 +1,69 @@
<?php
/**
* Class WC_Order_Item_Fee_Data_Store file.
*
* @package WooCommerce\DataStores
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC Order Item Fee Data Store
*
* @version 3.0.0
*/
class WC_Order_Item_Fee_Data_Store extends Abstract_WC_Order_Item_Type_Data_Store implements WC_Object_Data_Store_Interface, WC_Order_Item_Type_Data_Store_Interface {
/**
* Data stored in meta keys.
*
* @since 3.0.0
* @var array
*/
protected $internal_meta_keys = array( '_fee_amount', '_tax_class', '_tax_status', '_line_subtotal', '_line_subtotal_tax', '_line_total', '_line_tax', '_line_tax_data' );
/**
* Read/populate data properties specific to this order item.
*
* @since 3.0.0
* @param WC_Order_Item_Fee $item Fee order item object.
*/
public function read( &$item ) {
parent::read( $item );
$id = $item->get_id();
$item->set_props(
array(
'amount' => get_metadata( 'order_item', $id, '_fee_amount', true ),
'tax_class' => get_metadata( 'order_item', $id, '_tax_class', true ),
'tax_status' => get_metadata( 'order_item', $id, '_tax_status', true ),
'total' => get_metadata( 'order_item', $id, '_line_total', true ),
'taxes' => get_metadata( 'order_item', $id, '_line_tax_data', true ),
)
);
$item->set_object_read( true );
}
/**
* Saves an item's data to the database / item meta.
* Ran after both create and update, so $id will be set.
*
* @since 3.0.0
* @param WC_Order_Item_Fee $item Fee order item object.
*/
public function save_item_data( &$item ) {
$id = $item->get_id();
$save_values = array(
'_fee_amount' => $item->get_amount( 'edit' ),
'_tax_class' => $item->get_tax_class( 'edit' ),
'_tax_status' => $item->get_tax_status( 'edit' ),
'_line_total' => $item->get_total( 'edit' ),
'_line_tax' => $item->get_total_tax( 'edit' ),
'_line_tax_data' => $item->get_taxes( 'edit' ),
);
foreach ( $save_values as $key => $value ) {
update_metadata( 'order_item', $id, $key, $value );
}
}
}

View File

@ -0,0 +1,97 @@
<?php
/**
* Class WC_Order_Item_Product_Data_Store file.
*
* @package WooCommerce\DataStores
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC Order Item Product Data Store
*
* @version 3.0.0
*/
class WC_Order_Item_Product_Data_Store extends Abstract_WC_Order_Item_Type_Data_Store implements WC_Object_Data_Store_Interface, WC_Order_Item_Type_Data_Store_Interface, WC_Order_Item_Product_Data_Store_Interface {
/**
* Data stored in meta keys.
*
* @since 3.0.0
* @var array
*/
protected $internal_meta_keys = array( '_product_id', '_variation_id', '_qty', '_tax_class', '_line_subtotal', '_line_subtotal_tax', '_line_total', '_line_tax', '_line_tax_data' );
/**
* Read/populate data properties specific to this order item.
*
* @since 3.0.0
* @param WC_Order_Item_Product $item Product order item object.
*/
public function read( &$item ) {
parent::read( $item );
$id = $item->get_id();
$item->set_props(
array(
'product_id' => get_metadata( 'order_item', $id, '_product_id', true ),
'variation_id' => get_metadata( 'order_item', $id, '_variation_id', true ),
'quantity' => get_metadata( 'order_item', $id, '_qty', true ),
'tax_class' => get_metadata( 'order_item', $id, '_tax_class', true ),
'subtotal' => get_metadata( 'order_item', $id, '_line_subtotal', true ),
'total' => get_metadata( 'order_item', $id, '_line_total', true ),
'taxes' => get_metadata( 'order_item', $id, '_line_tax_data', true ),
)
);
$item->set_object_read( true );
}
/**
* Saves an item's data to the database / item meta.
* Ran after both create and update, so $id will be set.
*
* @since 3.0.0
* @param WC_Order_Item_Product $item Product order item object.
*/
public function save_item_data( &$item ) {
$id = $item->get_id();
$changes = $item->get_changes();
$meta_key_to_props = array(
'_product_id' => 'product_id',
'_variation_id' => 'variation_id',
'_qty' => 'quantity',
'_tax_class' => 'tax_class',
'_line_subtotal' => 'subtotal',
'_line_subtotal_tax' => 'subtotal_tax',
'_line_total' => 'total',
'_line_tax' => 'total_tax',
'_line_tax_data' => 'taxes',
);
$props_to_update = $this->get_props_to_update( $item, $meta_key_to_props, 'order_item' );
foreach ( $props_to_update as $meta_key => $prop ) {
update_metadata( 'order_item', $id, $meta_key, $item->{"get_$prop"}( 'edit' ) );
}
}
/**
* Get a list of download IDs for a specific item from an order.
*
* @since 3.0.0
* @param WC_Order_Item_Product $item Product order item object.
* @param WC_Order $order Order object.
* @return array
*/
public function get_download_ids( $item, $order ) {
global $wpdb;
return $wpdb->get_col(
$wpdb->prepare(
"SELECT download_id FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE user_email = %s AND order_key = %s AND product_id = %d ORDER BY permission_id",
$order->get_billing_email(),
$order->get_order_key(),
$item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id()
)
);
}
}

View File

@ -0,0 +1,78 @@
<?php
/**
* WC Order Item Shipping Data Store
*
* @version 3.0.0
* @package WooCommerce\DataStores
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC_Order_Item_Shipping_Data_Store class.
*/
class WC_Order_Item_Shipping_Data_Store extends Abstract_WC_Order_Item_Type_Data_Store implements WC_Object_Data_Store_Interface, WC_Order_Item_Type_Data_Store_Interface {
/**
* Data stored in meta keys.
*
* @since 3.0.0
* @var array
*/
protected $internal_meta_keys = array( 'method_id', 'instance_id', 'cost', 'total_tax', 'taxes' );
/**
* Read/populate data properties specific to this order item.
*
* @since 3.0.0
* @param WC_Order_Item_Shipping $item Item to read to.
* @throws Exception If invalid shipping order item.
*/
public function read( &$item ) {
parent::read( $item );
$id = $item->get_id();
$item->set_props(
array(
'method_id' => get_metadata( 'order_item', $id, 'method_id', true ),
'instance_id' => get_metadata( 'order_item', $id, 'instance_id', true ),
'total' => get_metadata( 'order_item', $id, 'cost', true ),
'taxes' => get_metadata( 'order_item', $id, 'taxes', true ),
)
);
// BW compat.
if ( '' === $item->get_instance_id() && strstr( $item->get_method_id(), ':' ) ) {
$legacy_method_id = explode( ':', $item->get_method_id() );
$item->set_method_id( $legacy_method_id[0] );
$item->set_instance_id( $legacy_method_id[1] );
}
$item->set_object_read( true );
}
/**
* Saves an item's data to the database / item meta.
* Ran after both create and update, so $id will be set.
*
* @since 3.0.0
* @param WC_Order_Item_Shipping $item Item to save.
*/
public function save_item_data( &$item ) {
$id = $item->get_id();
$changes = $item->get_changes();
$meta_key_to_props = array(
'method_id' => 'method_id',
'instance_id' => 'instance_id',
'cost' => 'total',
'total_tax' => 'total_tax',
'taxes' => 'taxes',
);
$props_to_update = $this->get_props_to_update( $item, $meta_key_to_props, 'order_item' );
foreach ( $props_to_update as $meta_key => $prop ) {
update_metadata( 'order_item', $id, $meta_key, $item->{"get_$prop"}( 'edit' ) );
}
}
}

View File

@ -0,0 +1,74 @@
<?php
/**
* Class WC_Order_Item_Tax_Data_Store file.
*
* @package WooCommerce\DataStores
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC Order Item Tax Data Store
*
* @version 3.0.0
*/
class WC_Order_Item_Tax_Data_Store extends Abstract_WC_Order_Item_Type_Data_Store implements WC_Object_Data_Store_Interface, WC_Order_Item_Type_Data_Store_Interface {
/**
* Data stored in meta keys.
*
* @since 3.0.0
* @var array
*/
protected $internal_meta_keys = array( 'rate_id', 'label', 'compound', 'tax_amount', 'shipping_tax_amount', 'rate_percent' );
/**
* Read/populate data properties specific to this order item.
*
* @since 3.0.0
* @param WC_Order_Item_Tax $item Tax order item object.
* @throws Exception If invalid order item.
*/
public function read( &$item ) {
parent::read( $item );
$id = $item->get_id();
$item->set_props(
array(
'rate_id' => get_metadata( 'order_item', $id, 'rate_id', true ),
'label' => get_metadata( 'order_item', $id, 'label', true ),
'compound' => get_metadata( 'order_item', $id, 'compound', true ),
'tax_total' => get_metadata( 'order_item', $id, 'tax_amount', true ),
'shipping_tax_total' => get_metadata( 'order_item', $id, 'shipping_tax_amount', true ),
'rate_percent' => get_metadata( 'order_item', $id, 'rate_percent', true ),
)
);
$item->set_object_read( true );
}
/**
* Saves an item's data to the database / item meta.
* Ran after both create and update, so $id will be set.
*
* @since 3.0.0
* @param WC_Order_Item_Tax $item Tax order item object.
*/
public function save_item_data( &$item ) {
$id = $item->get_id();
$changes = $item->get_changes();
$meta_key_to_props = array(
'rate_id' => 'rate_id',
'label' => 'label',
'compound' => 'compound',
'tax_amount' => 'tax_total',
'shipping_tax_amount' => 'shipping_tax_total',
'rate_percent' => 'rate_percent',
);
$props_to_update = $this->get_props_to_update( $item, $meta_key_to_props, 'order_item' );
foreach ( $props_to_update as $meta_key => $prop ) {
update_metadata( 'order_item', $id, $meta_key, $item->{"get_$prop"}( 'edit' ) );
}
}
}

View File

@ -0,0 +1,122 @@
<?php
/**
* Class WC_Order_Refund_Data_Store_CPT file.
*
* @package WooCommerce\DataStores
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC Order Refund Data Store: Stored in CPT.
*
* @version 3.0.0
*/
class WC_Order_Refund_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implements WC_Object_Data_Store_Interface, WC_Order_Refund_Data_Store_Interface {
/**
* Data stored in meta keys, but not considered "meta" for an order.
*
* @since 3.0.0
* @var array
*/
protected $internal_meta_keys = array(
'_order_currency',
'_cart_discount',
'_refund_amount',
'_refunded_by',
'_refunded_payment',
'_refund_reason',
'_cart_discount_tax',
'_order_shipping',
'_order_shipping_tax',
'_order_tax',
'_order_total',
'_order_version',
'_prices_include_tax',
'_payment_tokens',
);
/**
* Delete a refund - no trash is supported.
*
* @param WC_Order $order Order object.
* @param array $args Array of args to pass to the delete method.
*/
public function delete( &$order, $args = array() ) {
$id = $order->get_id();
$parent_order_id = $order->get_parent_id();
$refund_cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $parent_order_id;
if ( ! $id ) {
return;
}
wp_delete_post( $id );
wp_cache_delete( $refund_cache_key, 'orders' );
$order->set_id( 0 );
do_action( 'woocommerce_delete_order_refund', $id );
}
/**
* Read refund data. Can be overridden by child classes to load other props.
*
* @param WC_Order_Refund $refund Refund object.
* @param object $post_object Post object.
* @since 3.0.0
*/
protected function read_order_data( &$refund, $post_object ) {
parent::read_order_data( $refund, $post_object );
$id = $refund->get_id();
$refund->set_props(
array(
'amount' => get_post_meta( $id, '_refund_amount', true ),
'refunded_by' => metadata_exists( 'post', $id, '_refunded_by' ) ? get_post_meta( $id, '_refunded_by', true ) : absint( $post_object->post_author ),
'refunded_payment' => wc_string_to_bool( get_post_meta( $id, '_refunded_payment', true ) ),
'reason' => metadata_exists( 'post', $id, '_refund_reason' ) ? get_post_meta( $id, '_refund_reason', true ) : $post_object->post_excerpt,
)
);
}
/**
* Helper method that updates all the post meta for an order based on it's settings in the WC_Order class.
*
* @param WC_Order_Refund $refund Refund object.
* @since 3.0.0
*/
protected function update_post_meta( &$refund ) {
parent::update_post_meta( $refund );
$updated_props = array();
$meta_key_to_props = array(
'_refund_amount' => 'amount',
'_refunded_by' => 'refunded_by',
'_refunded_payment' => 'refunded_payment',
'_refund_reason' => 'reason',
);
$props_to_update = $this->get_props_to_update( $refund, $meta_key_to_props );
foreach ( $props_to_update as $meta_key => $prop ) {
$value = $refund->{"get_$prop"}( 'edit' );
update_post_meta( $refund->get_id(), $meta_key, $value );
$updated_props[] = $prop;
}
do_action( 'woocommerce_order_refund_object_updated_props', $refund, $updated_props );
}
/**
* Get a title for the new post type.
*
* @return string
*/
protected function get_post_title() {
return sprintf(
/* translators: %s: Order date */
__( 'Refund &ndash; %s', 'woocommerce' ),
strftime( _x( '%b %d, %Y @ %I:%M %p', 'Order date parsed by strftime', 'woocommerce' ) ) // phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment, WordPress.WP.I18n.UnorderedPlaceholdersText
);
}
}

View File

@ -0,0 +1,372 @@
<?php
/**
* Class WC_Payment_Token_Data_Store file.
*
* @package WooCommerce\DataStores
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC Payment Token Data Store: Custom Table.
*
* @version 3.0.0
*/
class WC_Payment_Token_Data_Store extends WC_Data_Store_WP implements WC_Payment_Token_Data_Store_Interface, WC_Object_Data_Store_Interface {
/**
* Meta type. Payment tokens are a new object type.
*
* @var string
*/
protected $meta_type = 'payment_token';
/**
* If we have already saved our extra data, don't do automatic / default handling.
*
* @var bool
*/
protected $extra_data_saved = false;
/**
* Create a new payment token in the database.
*
* @since 3.0.0
*
* @param WC_Payment_Token $token Payment token object.
*
* @throws Exception Throw exception if invalid or missing payment token fields.
*/
public function create( &$token ) {
if ( false === $token->validate() ) {
throw new Exception( __( 'Invalid or missing payment token fields.', 'woocommerce' ) );
}
global $wpdb;
if ( ! $token->is_default() && $token->get_user_id() > 0 ) {
$default_token = WC_Payment_Tokens::get_customer_default_token( $token->get_user_id() );
if ( is_null( $default_token ) ) {
$token->set_default( true );
}
}
$payment_token_data = array(
'gateway_id' => $token->get_gateway_id( 'edit' ),
'token' => $token->get_token( 'edit' ),
'user_id' => $token->get_user_id( 'edit' ),
'type' => $token->get_type( 'edit' ),
);
$wpdb->insert( $wpdb->prefix . 'woocommerce_payment_tokens', $payment_token_data );
$token_id = $wpdb->insert_id;
$token->set_id( $token_id );
$this->save_extra_data( $token, true );
$token->save_meta_data();
$token->apply_changes();
// Make sure all other tokens are not set to default.
if ( $token->is_default() && $token->get_user_id() > 0 ) {
WC_Payment_Tokens::set_users_default( $token->get_user_id(), $token_id );
}
do_action( 'woocommerce_new_payment_token', $token_id, $token );
}
/**
* Update a payment token.
*
* @since 3.0.0
*
* @param WC_Payment_Token $token Payment token object.
*
* @throws Exception Throw exception if invalid or missing payment token fields.
*/
public function update( &$token ) {
if ( false === $token->validate() ) {
throw new Exception( __( 'Invalid or missing payment token fields.', 'woocommerce' ) );
}
global $wpdb;
$updated_props = array();
$core_props = array( 'gateway_id', 'token', 'user_id', 'type' );
$changed_props = array_keys( $token->get_changes() );
foreach ( $changed_props as $prop ) {
if ( ! in_array( $prop, $core_props, true ) ) {
continue;
}
$updated_props[] = $prop;
$payment_token_data[ $prop ] = $token->{'get_' . $prop}( 'edit' );
}
if ( ! empty( $payment_token_data ) ) {
$wpdb->update(
$wpdb->prefix . 'woocommerce_payment_tokens',
$payment_token_data,
array( 'token_id' => $token->get_id() )
);
}
$updated_extra_props = $this->save_extra_data( $token );
$updated_props = array_merge( $updated_props, $updated_extra_props );
$token->save_meta_data();
$token->apply_changes();
// Make sure all other tokens are not set to default.
if ( $token->is_default() && $token->get_user_id() > 0 ) {
WC_Payment_Tokens::set_users_default( $token->get_user_id(), $token->get_id() );
}
do_action( 'woocommerce_payment_token_object_updated_props', $token, $updated_props );
do_action( 'woocommerce_payment_token_updated', $token->get_id() );
}
/**
* Remove a payment token from the database.
*
* @since 3.0.0
* @param WC_Payment_Token $token Payment token object.
* @param bool $force_delete Unused param.
*/
public function delete( &$token, $force_delete = false ) {
global $wpdb;
$wpdb->delete( $wpdb->prefix . 'woocommerce_payment_tokens', array( 'token_id' => $token->get_id() ), array( '%d' ) );
$wpdb->delete( $wpdb->prefix . 'woocommerce_payment_tokenmeta', array( 'payment_token_id' => $token->get_id() ), array( '%d' ) );
do_action( 'woocommerce_payment_token_deleted', $token->get_id(), $token );
}
/**
* Read a token from the database.
*
* @since 3.0.0
*
* @param WC_Payment_Token $token Payment token object.
*
* @throws Exception Throw exception if invalid payment token.
*/
public function read( &$token ) {
global $wpdb;
$data = $wpdb->get_row(
$wpdb->prepare(
"SELECT token, user_id, gateway_id, is_default FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d LIMIT 1",
$token->get_id()
)
);
if ( $data ) {
$token->set_props(
array(
'token' => $data->token,
'user_id' => $data->user_id,
'gateway_id' => $data->gateway_id,
'default' => $data->is_default,
)
);
$this->read_extra_data( $token );
$token->read_meta_data();
$token->set_object_read( true );
do_action( 'woocommerce_payment_token_loaded', $token );
} else {
throw new Exception( __( 'Invalid payment token.', 'woocommerce' ) );
}
}
/**
* Read extra data associated with the token (like last4 digits of a card for expiry dates).
*
* @param WC_Payment_Token $token Payment token object.
* @since 3.0.0
*/
protected function read_extra_data( &$token ) {
foreach ( $token->get_extra_data_keys() as $key ) {
$function = 'set_' . $key;
if ( is_callable( array( $token, $function ) ) ) {
$token->{$function}( get_metadata( 'payment_token', $token->get_id(), $key, true ) );
}
}
}
/**
* Saves extra token data as meta.
*
* @since 3.0.0
* @param WC_Payment_Token $token Payment token object.
* @param bool $force By default, only changed props are updated. When this param is true all props are updated.
* @return array List of updated props.
*/
protected function save_extra_data( &$token, $force = false ) {
if ( $this->extra_data_saved ) {
return array();
}
$updated_props = array();
$extra_data_keys = $token->get_extra_data_keys();
$meta_key_to_props = ! empty( $extra_data_keys ) ? array_combine( $extra_data_keys, $extra_data_keys ) : array();
$props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $token, $meta_key_to_props );
foreach ( $extra_data_keys as $key ) {
if ( ! array_key_exists( $key, $props_to_update ) ) {
continue;
}
$function = 'get_' . $key;
if ( is_callable( array( $token, $function ) ) ) {
if ( update_metadata( 'payment_token', $token->get_id(), $key, $token->{$function}( 'edit' ) ) ) {
$updated_props[] = $key;
}
}
}
return $updated_props;
}
/**
* Returns an array of objects (stdObject) matching specific token criteria.
* Accepts token_id, user_id, gateway_id, and type.
* Each object should contain the fields token_id, gateway_id, token, user_id, type, is_default.
*
* @since 3.0.0
* @param array $args List of accepted args: token_id, gateway_id, user_id, type.
* @return array
*/
public function get_tokens( $args ) {
global $wpdb;
$args = wp_parse_args(
$args,
array(
'token_id' => '',
'user_id' => '',
'gateway_id' => '',
'type' => '',
)
);
$sql = "SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens";
$where = array( '1=1' );
if ( $args['token_id'] ) {
$token_ids = array_map( 'absint', is_array( $args['token_id'] ) ? $args['token_id'] : array( $args['token_id'] ) );
$where[] = "token_id IN ('" . implode( "','", array_map( 'esc_sql', $token_ids ) ) . "')";
}
if ( $args['user_id'] ) {
$where[] = $wpdb->prepare( 'user_id = %d', absint( $args['user_id'] ) );
}
if ( $args['gateway_id'] ) {
$gateway_ids = array( $args['gateway_id'] );
} else {
$gateways = WC_Payment_Gateways::instance();
$gateway_ids = $gateways->get_payment_gateway_ids();
}
$page = isset( $args['page'] ) ? absint( $args['page'] ) : 1;
$posts_per_page = isset( $args['limit'] ) ? absint( $args['limit'] ) : get_option( 'posts_per_page' );
$pgstrt = absint( ( $page - 1 ) * $posts_per_page ) . ', ';
$limits = 'LIMIT ' . $pgstrt . $posts_per_page;
$gateway_ids[] = '';
$where[] = "gateway_id IN ('" . implode( "','", array_map( 'esc_sql', $gateway_ids ) ) . "')";
if ( $args['type'] ) {
$where[] = $wpdb->prepare( 'type = %s', $args['type'] );
}
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$token_results = $wpdb->get_results( $sql . ' WHERE ' . implode( ' AND ', $where ) . ' ' . $limits );
return $token_results;
}
/**
* Returns an stdObject of a token for a user's default token.
* Should contain the fields token_id, gateway_id, token, user_id, type, is_default.
*
* @since 3.0.0
* @param int $user_id User ID.
* @return object
*/
public function get_users_default_token( $user_id ) {
global $wpdb;
return $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE user_id = %d AND is_default = 1",
$user_id
)
);
}
/**
* Returns an stdObject of a token.
* Should contain the fields token_id, gateway_id, token, user_id, type, is_default.
*
* @since 3.0.0
* @param int $token_id Token ID.
* @return object
*/
public function get_token_by_id( $token_id ) {
global $wpdb;
return $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d",
$token_id
)
);
}
/**
* Returns metadata for a specific payment token.
*
* @since 3.0.0
* @param int $token_id Token ID.
* @return array
*/
public function get_metadata( $token_id ) {
return get_metadata( 'payment_token', $token_id );
}
/**
* Get a token's type by ID.
*
* @since 3.0.0
* @param int $token_id Token ID.
* @return string
*/
public function get_token_type_by_id( $token_id ) {
global $wpdb;
return $wpdb->get_var(
$wpdb->prepare(
"SELECT type FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d",
$token_id
)
);
}
/**
* Update's a tokens default status in the database. Used for quickly
* looping through tokens and setting their statuses instead of creating a bunch
* of objects.
*
* @since 3.0.0
*
* @param int $token_id Token ID.
* @param bool $status Whether given payment token is the default payment token or not.
*
* @return void
*/
public function set_default_status( $token_id, $status = true ) {
global $wpdb;
$wpdb->update(
$wpdb->prefix . 'woocommerce_payment_tokens',
array( 'is_default' => (int) $status ),
array(
'token_id' => $token_id,
)
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,100 @@
<?php
/**
* Class WC_Product_Grouped_Data_Store_CPT file.
*
* @package WooCommerce\DataStores
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC Grouped Product Data Store: Stored in CPT.
*
* @version 3.0.0
*/
class WC_Product_Grouped_Data_Store_CPT extends WC_Product_Data_Store_CPT implements WC_Object_Data_Store_Interface {
/**
* Helper method that updates all the post meta for a grouped product.
*
* @param WC_Product $product Product object.
* @param bool $force Force update. Used during create.
* @since 3.0.0
*/
protected function update_post_meta( &$product, $force = false ) {
$meta_key_to_props = array(
'_children' => 'children',
);
$props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props );
foreach ( $props_to_update as $meta_key => $prop ) {
$value = $product->{"get_$prop"}( 'edit' );
$updated = update_post_meta( $product->get_id(), $meta_key, $value );
if ( $updated ) {
$this->updated_props[] = $prop;
}
}
parent::update_post_meta( $product, $force );
}
/**
* Handle updated meta props after updating meta data.
*
* @since 3.0.0
* @param WC_Product $product Product object.
*/
protected function handle_updated_props( &$product ) {
if ( in_array( 'children', $this->updated_props, true ) ) {
$this->update_prices_from_children( $product );
}
parent::handle_updated_props( $product );
}
/**
* Sync grouped product prices with children.
*
* @since 3.0.0
* @param WC_Product|int $product Product object or product ID.
*/
public function sync_price( &$product ) {
$this->update_prices_from_children( $product );
}
/**
* Loop over child products and update the grouped product prices.
*
* @param WC_Product $product Product object.
*/
protected function update_prices_from_children( &$product ) {
$child_prices = array();
foreach ( $product->get_children( 'edit' ) as $child_id ) {
$child = wc_get_product( $child_id );
if ( $child ) {
$child_prices[] = $child->get_price( 'edit' );
}
}
$child_prices = array_filter( $child_prices );
delete_post_meta( $product->get_id(), '_price' );
delete_post_meta( $product->get_id(), '_sale_price' );
delete_post_meta( $product->get_id(), '_regular_price' );
if ( ! empty( $child_prices ) ) {
add_post_meta( $product->get_id(), '_price', min( $child_prices ) );
add_post_meta( $product->get_id(), '_price', max( $child_prices ) );
}
$this->update_lookup_table( $product->get_id(), 'wc_product_meta_lookup' );
/**
* Fire an action for this direct update so it can be detected by other code.
*
* @since 3.6
* @param int $product_id Product ID that was updated directly.
*/
do_action( 'woocommerce_updated_product_price', $product->get_id() );
}
}

View File

@ -0,0 +1,710 @@
<?php
/**
* File for WC Variable Product Data Store class.
*
* @package WooCommerce\Classes
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC Variable Product Data Store: Stored in CPT.
*
* @version 3.0.0
*/
class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT implements WC_Object_Data_Store_Interface, WC_Product_Variable_Data_Store_Interface {
/**
* Cached & hashed prices array for child variations.
*
* @var array
*/
protected $prices_array = array();
/**
* Read attributes from post meta.
*
* @param WC_Product $product Product object.
*/
protected function read_attributes( &$product ) {
$meta_attributes = get_post_meta( $product->get_id(), '_product_attributes', true );
if ( ! empty( $meta_attributes ) && is_array( $meta_attributes ) ) {
$attributes = array();
$force_update = false;
foreach ( $meta_attributes as $meta_attribute_key => $meta_attribute_value ) {
$meta_value = array_merge(
array(
'name' => '',
'value' => '',
'position' => 0,
'is_visible' => 0,
'is_variation' => 0,
'is_taxonomy' => 0,
),
(array) $meta_attribute_value
);
// Maintain data integrity. 4.9 changed sanitization functions - update the values here so variations function correctly.
if ( $meta_value['is_variation'] && strstr( $meta_value['name'], '/' ) && sanitize_title( $meta_value['name'] ) !== $meta_attribute_key ) {
global $wpdb;
$old_slug = 'attribute_' . $meta_attribute_key;
$new_slug = 'attribute_' . sanitize_title( $meta_value['name'] );
$old_meta_rows = $wpdb->get_results( $wpdb->prepare( "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s;", $old_slug ) ); // WPCS: db call ok, cache ok.
if ( $old_meta_rows ) {
foreach ( $old_meta_rows as $old_meta_row ) {
update_post_meta( $old_meta_row->post_id, $new_slug, $old_meta_row->meta_value );
}
}
$force_update = true;
}
// Check if is a taxonomy attribute.
if ( ! empty( $meta_value['is_taxonomy'] ) ) {
if ( ! taxonomy_exists( $meta_value['name'] ) ) {
continue;
}
$id = wc_attribute_taxonomy_id_by_name( $meta_value['name'] );
$options = wc_get_object_terms( $product->get_id(), $meta_value['name'], 'term_id' );
} else {
$id = 0;
$options = wc_get_text_attributes( $meta_value['value'] );
}
$attribute = new WC_Product_Attribute();
$attribute->set_id( $id );
$attribute->set_name( $meta_value['name'] );
$attribute->set_options( $options );
$attribute->set_position( $meta_value['position'] );
$attribute->set_visible( $meta_value['is_visible'] );
$attribute->set_variation( $meta_value['is_variation'] );
$attributes[] = $attribute;
}
$product->set_attributes( $attributes );
if ( $force_update ) {
$this->update_attributes( $product, true );
}
}
}
/**
* Read product data.
*
* @param WC_Product $product Product object.
*
* @since 3.0.0
*/
protected function read_product_data( &$product ) {
parent::read_product_data( $product );
// Make sure data which does not apply to variables is unset.
$product->set_regular_price( '' );
$product->set_sale_price( '' );
}
/**
* Loads variation child IDs.
*
* @param WC_Product $product Product object.
* @param bool $force_read True to bypass the transient.
*
* @return array
*/
public function read_children( &$product, $force_read = false ) {
$children_transient_name = 'wc_product_children_' . $product->get_id();
$children = get_transient( $children_transient_name );
if ( empty( $children ) || ! is_array( $children ) || ! isset( $children['all'] ) || ! isset( $children['visible'] ) || $force_read ) {
$all_args = array(
'post_parent' => $product->get_id(),
'post_type' => 'product_variation',
'orderby' => array(
'menu_order' => 'ASC',
'ID' => 'ASC',
),
'fields' => 'ids',
'post_status' => array( 'publish', 'private' ),
'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts
);
$visible_only_args = $all_args;
$visible_only_args['post_status'] = 'publish';
if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
$visible_only_args['tax_query'][] = array(
'taxonomy' => 'product_visibility',
'field' => 'name',
'terms' => 'outofstock',
'operator' => 'NOT IN',
);
}
$children['all'] = get_posts( apply_filters( 'woocommerce_variable_children_args', $all_args, $product, false ) );
$children['visible'] = get_posts( apply_filters( 'woocommerce_variable_children_args', $visible_only_args, $product, true ) );
set_transient( $children_transient_name, $children, DAY_IN_SECONDS * 30 );
}
$children['all'] = wp_parse_id_list( (array) $children['all'] );
$children['visible'] = wp_parse_id_list( (array) $children['visible'] );
return $children;
}
/**
* Loads an array of attributes used for variations, as well as their possible values.
*
* @param WC_Product $product Product object.
*
* @return array
*/
public function read_variation_attributes( &$product ) {
global $wpdb;
$variation_attributes = array();
$attributes = $product->get_attributes();
$child_ids = $product->get_children();
$cache_key = WC_Cache_Helper::get_cache_prefix( 'product_' . $product->get_id() ) . 'product_variation_attributes_' . $product->get_id();
$cache_group = 'products';
$cached_data = wp_cache_get( $cache_key, $cache_group );
if ( false !== $cached_data ) {
return $cached_data;
}
if ( ! empty( $attributes ) ) {
foreach ( $attributes as $attribute ) {
if ( empty( $attribute['is_variation'] ) ) {
continue;
}
// Get possible values for this attribute, for only visible variations.
if ( ! empty( $child_ids ) ) {
$format = array_fill( 0, count( $child_ids ), '%d' );
$query_in = '(' . implode( ',', $format ) . ')';
$query_args = array( 'attribute_name' => wc_variation_attribute_name( $attribute['name'] ) ) + $child_ids;
$values = array_unique(
$wpdb->get_col(
$wpdb->prepare(
"SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND post_id IN {$query_in}", // @codingStandardsIgnoreLine.
$query_args
)
)
);
} else {
$values = array();
}
// Empty value indicates that all options for given attribute are available.
if ( in_array( null, $values, true ) || in_array( '', $values, true ) || empty( $values ) ) {
$values = $attribute['is_taxonomy'] ? wc_get_object_terms( $product->get_id(), $attribute['name'], 'slug' ) : wc_get_text_attributes( $attribute['value'] );
// Get custom attributes (non taxonomy) as defined.
} elseif ( ! $attribute['is_taxonomy'] ) {
$text_attributes = wc_get_text_attributes( $attribute['value'] );
$assigned_text_attributes = $values;
$values = array();
// Pre 2.4 handling where 'slugs' were saved instead of the full text attribute.
if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) {
$assigned_text_attributes = array_map( 'sanitize_title', $assigned_text_attributes );
foreach ( $text_attributes as $text_attribute ) {
if ( in_array( sanitize_title( $text_attribute ), $assigned_text_attributes, true ) ) {
$values[] = $text_attribute;
}
}
} else {
foreach ( $text_attributes as $text_attribute ) {
if ( in_array( $text_attribute, $assigned_text_attributes, true ) ) {
$values[] = $text_attribute;
}
}
}
}
$variation_attributes[ $attribute['name'] ] = array_unique( $values );
}
}
wp_cache_set( $cache_key, $variation_attributes, $cache_group );
return $variation_attributes;
}
/**
* Get an array of all sale and regular prices from all variations. This is used for example when displaying the price range at variable product level or seeing if the variable product is on sale.
*
* Can be filtered by plugins which modify costs, but otherwise will include the raw meta costs unlike get_price() which runs costs through the woocommerce_get_price filter.
* This is to ensure modified prices are not cached, unless intended.
*
* @param WC_Product $product Product object.
* @param bool $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes).
*
* @return array of prices
* @since 3.0.0
*/
public function read_price_data( &$product, $for_display = false ) {
/**
* Transient name for storing prices for this product (note: Max transient length is 45)
*
* @since 2.5.0 a single transient is used per product for all prices, rather than many transients per product.
*/
$transient_name = 'wc_var_prices_' . $product->get_id();
$transient_version = WC_Cache_Helper::get_transient_version( 'product' );
$price_hash = $this->get_price_hash( $product, $for_display );
// Check if prices array is stale.
if ( ! isset( $this->prices_array['version'] ) || $this->prices_array['version'] !== $transient_version ) {
$this->prices_array = array(
'version' => $transient_version,
);
}
/**
* $this->prices_array is an array of values which may have been modified from what is stored in transients - this may not match $transient_cached_prices_array.
* If the value has already been generated, we don't need to grab the values again so just return them. They are already filtered.
*/
if ( empty( $this->prices_array[ $price_hash ] ) ) {
$transient_cached_prices_array = array_filter( (array) json_decode( strval( get_transient( $transient_name ) ), true ) );
// If the product version has changed since the transient was last saved, reset the transient cache.
if ( ! isset( $transient_cached_prices_array['version'] ) || $transient_version !== $transient_cached_prices_array['version'] ) {
$transient_cached_prices_array = array(
'version' => $transient_version,
);
}
// If the prices are not stored for this hash, generate them and add to the transient.
if ( empty( $transient_cached_prices_array[ $price_hash ] ) ) {
$prices_array = array(
'price' => array(),
'regular_price' => array(),
'sale_price' => array(),
);
$variation_ids = $product->get_visible_children();
if ( is_callable( '_prime_post_caches' ) ) {
_prime_post_caches( $variation_ids );
}
foreach ( $variation_ids as $variation_id ) {
$variation = wc_get_product( $variation_id );
if ( $variation ) {
$price = apply_filters( 'woocommerce_variation_prices_price', $variation->get_price( 'edit' ), $variation, $product );
$regular_price = apply_filters( 'woocommerce_variation_prices_regular_price', $variation->get_regular_price( 'edit' ), $variation, $product );
$sale_price = apply_filters( 'woocommerce_variation_prices_sale_price', $variation->get_sale_price( 'edit' ), $variation, $product );
// Skip empty prices.
if ( '' === $price ) {
continue;
}
// If sale price does not equal price, the product is not yet on sale.
if ( $sale_price === $regular_price || $sale_price !== $price ) {
$sale_price = $regular_price;
}
// If we are getting prices for display, we need to account for taxes.
if ( $for_display ) {
if ( 'incl' === get_option( 'woocommerce_tax_display_shop' ) ) {
$price = '' === $price ? '' : wc_get_price_including_tax(
$variation,
array(
'qty' => 1,
'price' => $price,
)
);
$regular_price = '' === $regular_price ? '' : wc_get_price_including_tax(
$variation,
array(
'qty' => 1,
'price' => $regular_price,
)
);
$sale_price = '' === $sale_price ? '' : wc_get_price_including_tax(
$variation,
array(
'qty' => 1,
'price' => $sale_price,
)
);
} else {
$price = '' === $price ? '' : wc_get_price_excluding_tax(
$variation,
array(
'qty' => 1,
'price' => $price,
)
);
$regular_price = '' === $regular_price ? '' : wc_get_price_excluding_tax(
$variation,
array(
'qty' => 1,
'price' => $regular_price,
)
);
$sale_price = '' === $sale_price ? '' : wc_get_price_excluding_tax(
$variation,
array(
'qty' => 1,
'price' => $sale_price,
)
);
}
}
$prices_array['price'][ $variation_id ] = wc_format_decimal( $price, wc_get_price_decimals() );
$prices_array['regular_price'][ $variation_id ] = wc_format_decimal( $regular_price, wc_get_price_decimals() );
$prices_array['sale_price'][ $variation_id ] = wc_format_decimal( $sale_price, wc_get_price_decimals() );
$prices_array = apply_filters( 'woocommerce_variation_prices_array', $prices_array, $variation, $for_display );
}
}
// Add all pricing data to the transient array.
foreach ( $prices_array as $key => $values ) {
$transient_cached_prices_array[ $price_hash ][ $key ] = $values;
}
set_transient( $transient_name, wp_json_encode( $transient_cached_prices_array ), DAY_IN_SECONDS * 30 );
}
/**
* Give plugins one last chance to filter the variation prices array which has been generated and store locally to the class.
* This value may differ from the transient cache. It is filtered once before storing locally.
*/
$this->prices_array[ $price_hash ] = apply_filters( 'woocommerce_variation_prices', $transient_cached_prices_array[ $price_hash ], $product, $for_display );
}
return $this->prices_array[ $price_hash ];
}
/**
* Create unique cache key based on the tax location (affects displayed/cached prices), product version and active price filters.
* DEVELOPERS should filter this hash if offering conditional pricing to keep it unique.
*
* @param WC_Product $product Product object.
* @param bool $for_display If taxes should be calculated or not.
*
* @since 3.0.0
* @return string
*/
protected function get_price_hash( &$product, $for_display = false ) {
global $wp_filter;
$price_hash = array( false );
if ( $for_display && wc_tax_enabled() ) {
$price_hash = array(
get_option( 'woocommerce_tax_display_shop', 'excl' ),
WC_Tax::get_rates(),
empty( WC()->customer ) ? false : WC()->customer->is_vat_exempt(),
);
}
$filter_names = array( 'woocommerce_variation_prices_price', 'woocommerce_variation_prices_regular_price', 'woocommerce_variation_prices_sale_price' );
foreach ( $filter_names as $filter_name ) {
if ( ! empty( $wp_filter[ $filter_name ] ) ) {
$price_hash[ $filter_name ] = array();
foreach ( $wp_filter[ $filter_name ] as $priority => $callbacks ) {
$price_hash[ $filter_name ][] = array_values( wp_list_pluck( $callbacks, 'function' ) );
}
}
}
return md5( wp_json_encode( apply_filters( 'woocommerce_get_variation_prices_hash', $price_hash, $product, $for_display ) ) );
}
/**
* Does a child have a weight set?
*
* @param WC_Product $product Product object.
*
* @since 3.0.0
* @return boolean
*/
public function child_has_weight( $product ) {
global $wpdb;
$children = $product->get_visible_children();
if ( ! $children ) {
return false;
}
$format = array_fill( 0, count( $children ), '%d' );
$query_in = '(' . implode( ',', $format ) . ')';
return null !== $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_weight' AND meta_value > 0 AND post_id IN {$query_in}", $children ) ); // @codingStandardsIgnoreLine.
}
/**
* Does a child have dimensions set?
*
* @param WC_Product $product Product object.
*
* @since 3.0.0
* @return boolean
*/
public function child_has_dimensions( $product ) {
global $wpdb;
$children = $product->get_visible_children();
if ( ! $children ) {
return false;
}
$format = array_fill( 0, count( $children ), '%d' );
$query_in = '(' . implode( ',', $format ) . ')';
return null !== $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key IN ( '_length', '_width', '_height' ) AND meta_value > 0 AND post_id IN {$query_in}", $children ) ); // @codingStandardsIgnoreLine.
}
/**
* Is a child in stock?
*
* @param WC_Product $product Product object.
*
* @since 3.0.0
* @return boolean
*/
public function child_is_in_stock( $product ) {
return $this->child_has_stock_status( $product, 'instock' );
}
/**
* Does a child have a stock status?
*
* @param WC_Product $product Product object.
* @param string $status 'instock', 'outofstock', or 'onbackorder'.
*
* @since 3.3.0
* @return boolean
*/
public function child_has_stock_status( $product, $status ) {
global $wpdb;
$children = $product->get_children();
if ( $children ) {
$format = array_fill( 0, count( $children ), '%d' );
$query_in = '(' . implode( ',', $format ) . ')';
$query_args = array( 'stock_status' => $status ) + $children;
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
if ( get_option( 'woocommerce_product_lookup_table_is_generating' ) ) {
$query = "SELECT COUNT( post_id ) FROM {$wpdb->postmeta} WHERE meta_key = '_stock_status' AND meta_value = %s AND post_id IN {$query_in}";
} else {
$query = "SELECT COUNT( product_id ) FROM {$wpdb->wc_product_meta_lookup} WHERE stock_status = %s AND product_id IN {$query_in}";
}
$children_with_status = $wpdb->get_var(
$wpdb->prepare(
$query,
$query_args
)
);
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
} else {
$children_with_status = 0;
}
return (bool) $children_with_status;
}
/**
* Syncs all variation names if the parent name is changed.
*
* @param WC_Product $product Product object.
* @param string $previous_name Variation previous name.
* @param string $new_name Variation new name.
*
* @since 3.0.0
*/
public function sync_variation_names( &$product, $previous_name = '', $new_name = '' ) {
if ( $new_name !== $previous_name ) {
global $wpdb;
$wpdb->query(
$wpdb->prepare(
"UPDATE {$wpdb->posts}
SET post_title = REPLACE( post_title, %s, %s )
WHERE post_type = 'product_variation'
AND post_parent = %d",
$previous_name ? $previous_name : 'AUTO-DRAFT',
$new_name,
$product->get_id()
)
);
}
}
/**
* Stock managed at the parent level - update children being managed by this product.
* This sync function syncs downwards (from parent to child) when the variable product is saved.
*
* @param WC_Product $product Product object.
*
* @since 3.0.0
*/
public function sync_managed_variation_stock_status( &$product ) {
global $wpdb;
if ( $product->get_manage_stock() ) {
$children = $product->get_children();
$changed = false;
if ( $children ) {
$status = $product->get_stock_status();
$format = array_fill( 0, count( $children ), '%d' );
$query_in = '(' . implode( ',', $format ) . ')';
$managed_children = array_unique( $wpdb->get_col( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_manage_stock' AND meta_value != 'yes' AND post_id IN {$query_in}", $children ) ) ); // @codingStandardsIgnoreLine.
foreach ( $managed_children as $managed_child ) {
if ( update_post_meta( $managed_child, '_stock_status', $status ) ) {
$this->update_lookup_table( $managed_child, 'wc_product_meta_lookup' );
$changed = true;
}
}
}
if ( $changed ) {
$children = $this->read_children( $product, true );
$product->set_children( $children['all'] );
$product->set_visible_children( $children['visible'] );
}
}
}
/**
* Sync variable product prices with children.
*
* @param WC_Product $product Product object.
*
* @since 3.0.0
*/
public function sync_price( &$product ) {
global $wpdb;
$children = $product->get_visible_children();
if ( $children ) {
$format = array_fill( 0, count( $children ), '%d' );
$query_in = '(' . implode( ',', $format ) . ')';
$prices = array_unique( $wpdb->get_col( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = '_price' AND post_id IN {$query_in}", $children ) ) ); // @codingStandardsIgnoreLine.
} else {
$prices = array();
}
delete_post_meta( $product->get_id(), '_price' );
delete_post_meta( $product->get_id(), '_sale_price' );
delete_post_meta( $product->get_id(), '_regular_price' );
if ( $prices ) {
sort( $prices, SORT_NUMERIC );
// To allow sorting and filtering by multiple values, we have no choice but to store child prices in this manner.
foreach ( $prices as $price ) {
if ( is_null( $price ) || '' === $price ) {
continue;
}
add_post_meta( $product->get_id(), '_price', $price, false );
}
}
$this->update_lookup_table( $product->get_id(), 'wc_product_meta_lookup' );
/**
* Fire an action for this direct update so it can be detected by other code.
*
* @since 3.6
* @param int $product_id Product ID that was updated directly.
*/
do_action( 'woocommerce_updated_product_price', $product->get_id() );
}
/**
* Sync variable product stock status with children.
* Change does not persist unless saved by caller.
*
* @param WC_Product $product Product object.
*
* @since 3.0.0
*/
public function sync_stock_status( &$product ) {
if ( $product->child_is_in_stock() ) {
$product->set_stock_status( 'instock' );
} elseif ( $product->child_is_on_backorder() ) {
$product->set_stock_status( 'onbackorder' );
} else {
$product->set_stock_status( 'outofstock' );
}
}
/**
* Delete variations of a product.
*
* @param int $product_id Product ID.
* @param bool $force_delete False to trash.
*
* @since 3.0.0
*/
public function delete_variations( $product_id, $force_delete = false ) {
if ( ! is_numeric( $product_id ) || 0 >= $product_id ) {
return;
}
$variation_ids = wp_parse_id_list(
get_posts(
array(
'post_parent' => $product_id,
'post_type' => 'product_variation',
'fields' => 'ids',
'post_status' => array( 'any', 'trash', 'auto-draft' ),
'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts
)
)
);
if ( ! empty( $variation_ids ) ) {
foreach ( $variation_ids as $variation_id ) {
if ( $force_delete ) {
do_action( 'woocommerce_before_delete_product_variation', $variation_id );
wp_delete_post( $variation_id, true );
do_action( 'woocommerce_delete_product_variation', $variation_id );
} else {
wp_trash_post( $variation_id );
do_action( 'woocommerce_trash_product_variation', $variation_id );
}
}
}
delete_transient( 'wc_product_children_' . $product_id );
}
/**
* Untrash variations.
*
* @param int $product_id Product ID.
*/
public function untrash_variations( $product_id ) {
$variation_ids = wp_parse_id_list(
get_posts(
array(
'post_parent' => $product_id,
'post_type' => 'product_variation',
'fields' => 'ids',
'post_status' => 'trash',
'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts
)
)
);
if ( ! empty( $variation_ids ) ) {
foreach ( $variation_ids as $variation_id ) {
wp_untrash_post( $variation_id );
}
}
delete_transient( 'wc_product_children_' . $product_id );
}
}

View File

@ -0,0 +1,547 @@
<?php
/**
* Class WC_Product_Variation_Data_Store_CPT file.
*
* @package WooCommerce\DataStores
*/
use Automattic\Jetpack\Constants;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC Variation Product Data Store: Stored in CPT.
*
* @version 3.0.0
*/
class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT implements WC_Object_Data_Store_Interface {
/**
* Callback to remove unwanted meta data.
*
* @param object $meta Meta object.
* @return bool false if excluded.
*/
protected function exclude_internal_meta_keys( $meta ) {
return ! in_array( $meta->meta_key, $this->internal_meta_keys, true ) && 0 !== stripos( $meta->meta_key, 'attribute_' ) && 0 !== stripos( $meta->meta_key, 'wp_' );
}
/*
|--------------------------------------------------------------------------
| CRUD Methods
|--------------------------------------------------------------------------
*/
/**
* Reads a product from the database and sets its data to the class.
*
* @since 3.0.0
* @param WC_Product_Variation $product Product object.
* @throws WC_Data_Exception If WC_Product::set_tax_status() is called with an invalid tax status (via read_product_data), or when passing an invalid ID.
*/
public function read( &$product ) {
$product->set_defaults();
if ( ! $product->get_id() ) {
return;
}
$post_object = get_post( $product->get_id() );
if ( ! $post_object ) {
return;
}
if ( 'product_variation' !== $post_object->post_type ) {
throw new WC_Data_Exception( 'variation_invalid_id', __( 'Invalid product type: passed ID does not correspond to a product variation.', 'woocommerce' ) );
}
$product->set_props(
array(
'name' => $post_object->post_title,
'slug' => $post_object->post_name,
'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ),
'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ),
'status' => $post_object->post_status,
'menu_order' => $post_object->menu_order,
'reviews_allowed' => 'open' === $post_object->comment_status,
'parent_id' => $post_object->post_parent,
'attribute_summary' => $post_object->post_excerpt,
)
);
// The post parent is not a valid variable product so we should prevent this.
if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) {
$product->set_parent_id( 0 );
}
$this->read_downloads( $product );
$this->read_product_data( $product );
$this->read_extra_data( $product );
$product->set_attributes( wc_get_product_variation_attributes( $product->get_id() ) );
$updates = array();
/**
* If a variation title is not in sync with the parent e.g. saved prior to 3.0, or if the parent title has changed, detect here and update.
*/
$new_title = $this->generate_product_title( $product );
if ( $post_object->post_title !== $new_title ) {
$product->set_name( $new_title );
$updates = array_merge( $updates, array( 'post_title' => $new_title ) );
}
/**
* If the attribute summary is not in sync, update here. Used when searching for variations by attribute values.
* This is meant to also cover the case when global attribute name or value is updated, then the attribute summary is updated
* for respective products when they're read.
*/
$new_attribute_summary = $this->generate_attribute_summary( $product );
if ( $new_attribute_summary !== $post_object->post_excerpt ) {
$product->set_attribute_summary( $new_attribute_summary );
$updates = array_merge( $updates, array( 'post_excerpt' => $new_attribute_summary ) );
}
if ( ! empty( $updates ) ) {
$GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $updates, array( 'ID' => $product->get_id() ) );
clean_post_cache( $product->get_id() );
}
// Set object_read true once all data is read.
$product->set_object_read( true );
}
/**
* Create a new product.
*
* @since 3.0.0
* @param WC_Product_Variation $product Product object.
*/
public function create( &$product ) {
if ( ! $product->get_date_created() ) {
$product->set_date_created( time() );
}
$new_title = $this->generate_product_title( $product );
if ( $product->get_name( 'edit' ) !== $new_title ) {
$product->set_name( $new_title );
}
$attribute_summary = $this->generate_attribute_summary( $product );
$product->set_attribute_summary( $attribute_summary );
// The post parent is not a valid variable product so we should prevent this.
if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) {
$product->set_parent_id( 0 );
}
$id = wp_insert_post(
apply_filters(
'woocommerce_new_product_variation_data',
array(
'post_type' => 'product_variation',
'post_status' => $product->get_status() ? $product->get_status() : 'publish',
'post_author' => get_current_user_id(),
'post_title' => $product->get_name( 'edit' ),
'post_excerpt' => $product->get_attribute_summary( 'edit' ),
'post_content' => '',
'post_parent' => $product->get_parent_id(),
'comment_status' => 'closed',
'ping_status' => 'closed',
'menu_order' => $product->get_menu_order(),
'post_date' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ),
'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ),
'post_name' => $product->get_slug( 'edit' ),
)
),
true
);
if ( $id && ! is_wp_error( $id ) ) {
$product->set_id( $id );
$this->update_post_meta( $product, true );
$this->update_terms( $product, true );
$this->update_visibility( $product, true );
$this->update_attributes( $product, true );
$this->handle_updated_props( $product );
$product->save_meta_data();
$product->apply_changes();
$this->update_version_and_type( $product );
$this->update_guid( $product );
$this->clear_caches( $product );
do_action( 'woocommerce_new_product_variation', $id, $product );
}
}
/**
* Updates an existing product.
*
* @since 3.0.0
* @param WC_Product_Variation $product Product object.
*/
public function update( &$product ) {
$product->save_meta_data();
if ( ! $product->get_date_created() ) {
$product->set_date_created( time() );
}
$new_title = $this->generate_product_title( $product );
if ( $product->get_name( 'edit' ) !== $new_title ) {
$product->set_name( $new_title );
}
// The post parent is not a valid variable product so we should prevent this.
if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) {
$product->set_parent_id( 0 );
}
$changes = $product->get_changes();
if ( array_intersect( array( 'attributes' ), array_keys( $changes ) ) ) {
$product->set_attribute_summary( $this->generate_attribute_summary( $product ) );
}
// Only update the post when the post data changes.
if ( array_intersect( array( 'name', 'parent_id', 'status', 'menu_order', 'date_created', 'date_modified', 'attributes' ), array_keys( $changes ) ) ) {
$post_data = array(
'post_title' => $product->get_name( 'edit' ),
'post_excerpt' => $product->get_attribute_summary( 'edit' ),
'post_parent' => $product->get_parent_id( 'edit' ),
'comment_status' => 'closed',
'post_status' => $product->get_status( 'edit' ) ? $product->get_status( 'edit' ) : 'publish',
'menu_order' => $product->get_menu_order( 'edit' ),
'post_date' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ),
'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ),
'post_modified' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ),
'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ),
'post_type' => 'product_variation',
'post_name' => $product->get_slug( 'edit' ),
);
/**
* When updating this object, to prevent infinite loops, use $wpdb
* to update data, since wp_update_post spawns more calls to the
* save_post action.
*
* This ensures hooks are fired by either WP itself (admin screen save),
* or an update purely from CRUD.
*/
if ( doing_action( 'save_post' ) ) {
$GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $product->get_id() ) );
clean_post_cache( $product->get_id() );
} else {
wp_update_post( array_merge( array( 'ID' => $product->get_id() ), $post_data ) );
}
$product->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook.
} else { // Only update post modified time to record this save event.
$GLOBALS['wpdb']->update(
$GLOBALS['wpdb']->posts,
array(
'post_modified' => current_time( 'mysql' ),
'post_modified_gmt' => current_time( 'mysql', 1 ),
),
array(
'ID' => $product->get_id(),
)
);
clean_post_cache( $product->get_id() );
}
$this->update_post_meta( $product );
$this->update_terms( $product );
$this->update_visibility( $product, true );
$this->update_attributes( $product );
$this->handle_updated_props( $product );
$product->apply_changes();
$this->update_version_and_type( $product );
$this->clear_caches( $product );
do_action( 'woocommerce_update_product_variation', $product->get_id(), $product );
}
/*
|--------------------------------------------------------------------------
| Additional Methods
|--------------------------------------------------------------------------
*/
/**
* Generates a title with attribute information for a variation.
* Products will get a title of the form "Name - Value, Value" or just "Name".
*
* @since 3.0.0
* @param WC_Product $product Product object.
* @return string
*/
protected function generate_product_title( $product ) {
$attributes = (array) $product->get_attributes();
// Do not include attributes if the product has 3+ attributes.
$should_include_attributes = count( $attributes ) < 3;
// Do not include attributes if an attribute name has 2+ words and the
// product has multiple attributes.
if ( $should_include_attributes && 1 < count( $attributes ) ) {
foreach ( $attributes as $name => $value ) {
if ( false !== strpos( $name, '-' ) ) {
$should_include_attributes = false;
break;
}
}
}
$should_include_attributes = apply_filters( 'woocommerce_product_variation_title_include_attributes', $should_include_attributes, $product );
$separator = apply_filters( 'woocommerce_product_variation_title_attributes_separator', ' - ', $product );
$title_base = get_post_field( 'post_title', $product->get_parent_id() );
$title_suffix = $should_include_attributes ? wc_get_formatted_variation( $product, true, false ) : '';
return apply_filters( 'woocommerce_product_variation_title', $title_suffix ? $title_base . $separator . $title_suffix : $title_base, $product, $title_base, $title_suffix );
}
/**
* Generates attribute summary for the variation.
*
* Attribute summary contains comma-delimited 'attribute_name: attribute_value' pairs for all attributes.
*
* @since 3.6.0
* @param WC_Product_Variation $product Product variation to generate the attribute summary for.
*
* @return string
*/
protected function generate_attribute_summary( $product ) {
return wc_get_formatted_variation( $product, true, true );
}
/**
* Make sure we store the product version (to track data changes).
*
* @param WC_Product $product Product object.
* @since 3.0.0
*/
protected function update_version_and_type( &$product ) {
wp_set_object_terms( $product->get_id(), '', 'product_type' );
update_post_meta( $product->get_id(), '_product_version', Constants::get_constant( 'WC_VERSION' ) );
}
/**
* Read post data.
*
* @since 3.0.0
* @param WC_Product_Variation $product Product object.
* @throws WC_Data_Exception If WC_Product::set_tax_status() is called with an invalid tax status.
*/
protected function read_product_data( &$product ) {
$id = $product->get_id();
$product->set_props(
array(
'description' => get_post_meta( $id, '_variation_description', true ),
'regular_price' => get_post_meta( $id, '_regular_price', true ),
'sale_price' => get_post_meta( $id, '_sale_price', true ),
'date_on_sale_from' => get_post_meta( $id, '_sale_price_dates_from', true ),
'date_on_sale_to' => get_post_meta( $id, '_sale_price_dates_to', true ),
'manage_stock' => get_post_meta( $id, '_manage_stock', true ),
'stock_status' => get_post_meta( $id, '_stock_status', true ),
'low_stock_amount' => get_post_meta( $id, '_low_stock_amount', true ),
'shipping_class_id' => current( $this->get_term_ids( $id, 'product_shipping_class' ) ),
'virtual' => get_post_meta( $id, '_virtual', true ),
'downloadable' => get_post_meta( $id, '_downloadable', true ),
'gallery_image_ids' => array_filter( explode( ',', get_post_meta( $id, '_product_image_gallery', true ) ) ),
'download_limit' => get_post_meta( $id, '_download_limit', true ),
'download_expiry' => get_post_meta( $id, '_download_expiry', true ),
'image_id' => get_post_thumbnail_id( $id ),
'backorders' => get_post_meta( $id, '_backorders', true ),
'sku' => get_post_meta( $id, '_sku', true ),
'stock_quantity' => get_post_meta( $id, '_stock', true ),
'weight' => get_post_meta( $id, '_weight', true ),
'length' => get_post_meta( $id, '_length', true ),
'width' => get_post_meta( $id, '_width', true ),
'height' => get_post_meta( $id, '_height', true ),
'tax_class' => ! metadata_exists( 'post', $id, '_tax_class' ) ? 'parent' : get_post_meta( $id, '_tax_class', true ),
)
);
if ( $product->is_on_sale( 'edit' ) ) {
$product->set_price( $product->get_sale_price( 'edit' ) );
} else {
$product->set_price( $product->get_regular_price( 'edit' ) );
}
$parent_object = get_post( $product->get_parent_id() );
$terms = get_the_terms( $product->get_parent_id(), 'product_visibility' );
$term_names = is_array( $terms ) ? wp_list_pluck( $terms, 'name' ) : array();
$exclude_search = in_array( 'exclude-from-search', $term_names, true );
$exclude_catalog = in_array( 'exclude-from-catalog', $term_names, true );
if ( $exclude_search && $exclude_catalog ) {
$catalog_visibility = 'hidden';
} elseif ( $exclude_search ) {
$catalog_visibility = 'catalog';
} elseif ( $exclude_catalog ) {
$catalog_visibility = 'search';
} else {
$catalog_visibility = 'visible';
}
$product->set_parent_data(
array(
'title' => $parent_object ? $parent_object->post_title : '',
'status' => $parent_object ? $parent_object->post_status : '',
'sku' => get_post_meta( $product->get_parent_id(), '_sku', true ),
'manage_stock' => get_post_meta( $product->get_parent_id(), '_manage_stock', true ),
'backorders' => get_post_meta( $product->get_parent_id(), '_backorders', true ),
'stock_quantity' => wc_stock_amount( get_post_meta( $product->get_parent_id(), '_stock', true ) ),
'weight' => get_post_meta( $product->get_parent_id(), '_weight', true ),
'length' => get_post_meta( $product->get_parent_id(), '_length', true ),
'width' => get_post_meta( $product->get_parent_id(), '_width', true ),
'height' => get_post_meta( $product->get_parent_id(), '_height', true ),
'tax_class' => get_post_meta( $product->get_parent_id(), '_tax_class', true ),
'shipping_class_id' => absint( current( $this->get_term_ids( $product->get_parent_id(), 'product_shipping_class' ) ) ),
'image_id' => get_post_thumbnail_id( $product->get_parent_id() ),
'purchase_note' => get_post_meta( $product->get_parent_id(), '_purchase_note', true ),
'catalog_visibility' => $catalog_visibility,
)
);
// Pull data from the parent when there is no user-facing way to set props.
$product->set_sold_individually( get_post_meta( $product->get_parent_id(), '_sold_individually', true ) );
$product->set_tax_status( get_post_meta( $product->get_parent_id(), '_tax_status', true ) );
$product->set_cross_sell_ids( get_post_meta( $product->get_parent_id(), '_crosssell_ids', true ) );
}
/**
* For all stored terms in all taxonomies, save them to the DB.
*
* @since 3.0.0
* @param WC_Product $product Product object.
* @param bool $force Force update. Used during create.
*/
protected function update_terms( &$product, $force = false ) {
$changes = $product->get_changes();
if ( $force || array_key_exists( 'shipping_class_id', $changes ) ) {
wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false );
}
}
/**
* Update visibility terms based on props.
*
* @since 3.0.0
*
* @param WC_Product $product Product object.
* @param bool $force Force update. Used during create.
*/
protected function update_visibility( &$product, $force = false ) {
$changes = $product->get_changes();
if ( $force || array_intersect( array( 'stock_status' ), array_keys( $changes ) ) ) {
$terms = array();
if ( 'outofstock' === $product->get_stock_status() ) {
$terms[] = 'outofstock';
}
wp_set_post_terms( $product->get_id(), $terms, 'product_visibility', false );
}
}
/**
* Update attribute meta values.
*
* @since 3.0.0
* @param WC_Product $product Product object.
* @param bool $force Force update. Used during create.
*/
protected function update_attributes( &$product, $force = false ) {
$changes = $product->get_changes();
if ( $force || array_key_exists( 'attributes', $changes ) ) {
global $wpdb;
$product_id = $product->get_id();
$attributes = $product->get_attributes();
$updated_attribute_keys = array();
foreach ( $attributes as $key => $value ) {
update_post_meta( $product_id, 'attribute_' . $key, wp_slash( $value ) );
$updated_attribute_keys[] = 'attribute_' . $key;
}
// Remove old taxonomies attributes so data is kept up to date - first get attribute key names.
$delete_attribute_keys = $wpdb->get_col(
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.QuotedDynamicPlaceholderGeneration
"SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE %s AND meta_key NOT IN ( '" . implode( "','", array_map( 'esc_sql', $updated_attribute_keys ) ) . "' ) AND post_id = %d",
$wpdb->esc_like( 'attribute_' ) . '%',
$product_id
)
);
foreach ( $delete_attribute_keys as $key ) {
delete_post_meta( $product_id, $key );
}
}
}
/**
* Helper method that updates all the post meta for a product based on it's settings in the WC_Product class.
*
* @since 3.0.0
* @param WC_Product $product Product object.
* @param bool $force Force update. Used during create.
*/
public function update_post_meta( &$product, $force = false ) {
$meta_key_to_props = array(
'_variation_description' => 'description',
);
$props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props );
foreach ( $props_to_update as $meta_key => $prop ) {
$value = $product->{"get_$prop"}( 'edit' );
$updated = update_post_meta( $product->get_id(), $meta_key, $value );
if ( $updated ) {
$this->updated_props[] = $prop;
}
}
parent::update_post_meta( $product, $force );
}
/**
* Update product variation guid.
*
* @param WC_Product_Variation $product Product variation object.
*
* @since 3.6.0
*/
protected function update_guid( $product ) {
global $wpdb;
$guid = home_url(
add_query_arg(
array(
'post_type' => 'product_variation',
'p' => $product->get_id(),
),
''
)
);
$wpdb->update( $wpdb->posts, array( 'guid' => $guid ), array( 'ID' => $product->get_id() ) );
}
}

View File

@ -0,0 +1,373 @@
<?php
/**
* Class WC_Shipping_Zone_Data_Store file.
*
* @package WooCommerce\DataStores
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC Shipping Zone Data Store.
*
* @version 3.0.0
*/
class WC_Shipping_Zone_Data_Store extends WC_Data_Store_WP implements WC_Shipping_Zone_Data_Store_Interface, WC_Object_Data_Store_Interface {
/**
* Method to create a new shipping zone.
*
* @since 3.0.0
* @param WC_Shipping_Zone $zone Shipping zone object.
*/
public function create( &$zone ) {
global $wpdb;
$wpdb->insert(
$wpdb->prefix . 'woocommerce_shipping_zones',
array(
'zone_name' => $zone->get_zone_name(),
'zone_order' => $zone->get_zone_order(),
)
);
$zone->set_id( $wpdb->insert_id );
$zone->save_meta_data();
$this->save_locations( $zone );
$zone->apply_changes();
WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' );
WC_Cache_Helper::get_transient_version( 'shipping', true );
}
/**
* Update zone in the database.
*
* @since 3.0.0
* @param WC_Shipping_Zone $zone Shipping zone object.
*/
public function update( &$zone ) {
global $wpdb;
if ( $zone->get_id() ) {
$wpdb->update(
$wpdb->prefix . 'woocommerce_shipping_zones',
array(
'zone_name' => $zone->get_zone_name(),
'zone_order' => $zone->get_zone_order(),
),
array( 'zone_id' => $zone->get_id() )
);
}
$zone->save_meta_data();
$this->save_locations( $zone );
$zone->apply_changes();
WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' );
WC_Cache_Helper::get_transient_version( 'shipping', true );
}
/**
* Method to read a shipping zone from the database.
*
* @since 3.0.0
* @param WC_Shipping_Zone $zone Shipping zone object.
* @throws Exception If invalid data store.
*/
public function read( &$zone ) {
global $wpdb;
// Zone 0 is used as a default if no other zones fit.
if ( 0 === $zone->get_id() || '0' === $zone->get_id() ) {
$this->read_zone_locations( $zone );
$zone->set_zone_name( __( 'Locations not covered by your other zones', 'woocommerce' ) );
$zone->read_meta_data();
$zone->set_object_read( true );
/**
* Indicate that the WooCommerce shipping zone has been loaded.
*
* @param WC_Shipping_Zone $zone The shipping zone that has been loaded.
*/
do_action( 'woocommerce_shipping_zone_loaded', $zone );
return;
}
$zone_data = $wpdb->get_row(
$wpdb->prepare(
"SELECT zone_name, zone_order FROM {$wpdb->prefix}woocommerce_shipping_zones WHERE zone_id = %d LIMIT 1",
$zone->get_id()
)
);
if ( ! $zone_data ) {
throw new Exception( __( 'Invalid data store.', 'woocommerce' ) );
}
$zone->set_zone_name( $zone_data->zone_name );
$zone->set_zone_order( $zone_data->zone_order );
$this->read_zone_locations( $zone );
$zone->read_meta_data();
$zone->set_object_read( true );
/** This action is documented in includes/datastores/class-wc-shipping-zone-data-store.php. */
do_action( 'woocommerce_shipping_zone_loaded', $zone );
}
/**
* Deletes a shipping zone from the database.
*
* @since 3.0.0
* @param WC_Shipping_Zone $zone Shipping zone object.
* @param array $args Array of args to pass to the delete method.
* @return void
*/
public function delete( &$zone, $args = array() ) {
$zone_id = $zone->get_id();
if ( $zone_id ) {
global $wpdb;
// Delete methods and their settings.
$methods = $this->get_methods( $zone_id, false );
if ( $methods ) {
foreach ( $methods as $method ) {
$this->delete_method( $method->instance_id );
}
}
// Delete zone.
$wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_locations', array( 'zone_id' => $zone_id ) );
$wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zones', array( 'zone_id' => $zone_id ) );
$zone->set_id( null );
WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' );
WC_Cache_Helper::get_transient_version( 'shipping', true );
do_action( 'woocommerce_delete_shipping_zone', $zone_id );
}
}
/**
* Get a list of shipping methods for a specific zone.
*
* @since 3.0.0
* @param int $zone_id Zone ID.
* @param bool $enabled_only True to request enabled methods only.
* @return array Array of objects containing method_id, method_order, instance_id, is_enabled
*/
public function get_methods( $zone_id, $enabled_only ) {
global $wpdb;
if ( $enabled_only ) {
$raw_methods_sql = "SELECT method_id, method_order, instance_id, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d AND is_enabled = 1";
} else {
$raw_methods_sql = "SELECT method_id, method_order, instance_id, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d";
}
return $wpdb->get_results( $wpdb->prepare( $raw_methods_sql, $zone_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Get count of methods for a zone.
*
* @since 3.0.0
* @param int $zone_id Zone ID.
* @return int Method Count
*/
public function get_method_count( $zone_id ) {
global $wpdb;
return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d", $zone_id ) );
}
/**
* Add a shipping method to a zone.
*
* @since 3.0.0
* @param int $zone_id Zone ID.
* @param string $type Method Type/ID.
* @param int $order Method Order.
* @return int Instance ID
*/
public function add_method( $zone_id, $type, $order ) {
global $wpdb;
$wpdb->insert(
$wpdb->prefix . 'woocommerce_shipping_zone_methods',
array(
'method_id' => $type,
'zone_id' => $zone_id,
'method_order' => $order,
),
array(
'%s',
'%d',
'%d',
)
);
return $wpdb->insert_id;
}
/**
* Delete a method instance.
*
* @since 3.0.0
* @param int $instance_id Instance ID.
*/
public function delete_method( $instance_id ) {
global $wpdb;
$method = $this->get_method( $instance_id );
if ( ! $method ) {
return;
}
delete_option( 'woocommerce_' . $method->method_id . '_' . $instance_id . '_settings' );
$wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_methods', array( 'instance_id' => $instance_id ) );
do_action( 'woocommerce_delete_shipping_zone_method', $instance_id );
}
/**
* Get a shipping zone method instance.
*
* @since 3.0.0
* @param int $instance_id Instance ID.
* @return object
*/
public function get_method( $instance_id ) {
global $wpdb;
return $wpdb->get_row( $wpdb->prepare( "SELECT zone_id, method_id, instance_id, method_order, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE instance_id = %d LIMIT 1;", $instance_id ) );
}
/**
* Find a matching zone ID for a given package.
*
* @since 3.0.0
* @param object $package Package information.
* @return int
*/
public function get_zone_id_from_package( $package ) {
global $wpdb;
$country = strtoupper( wc_clean( $package['destination']['country'] ) );
$state = strtoupper( wc_clean( $package['destination']['state'] ) );
$continent = strtoupper( wc_clean( WC()->countries->get_continent_code_for_country( $country ) ) );
$postcode = wc_normalize_postcode( wc_clean( $package['destination']['postcode'] ) );
// Work out criteria for our zone search.
$criteria = array();
$criteria[] = $wpdb->prepare( "( ( location_type = 'country' AND location_code = %s )", $country );
$criteria[] = $wpdb->prepare( "OR ( location_type = 'state' AND location_code = %s )", $country . ':' . $state );
$criteria[] = $wpdb->prepare( "OR ( location_type = 'continent' AND location_code = %s )", $continent );
$criteria[] = 'OR ( location_type IS NULL ) )';
// Postcode range and wildcard matching.
$postcode_locations = $wpdb->get_results( "SELECT zone_id, location_code FROM {$wpdb->prefix}woocommerce_shipping_zone_locations WHERE location_type = 'postcode';" );
if ( $postcode_locations ) {
$zone_ids_with_postcode_rules = array_map( 'absint', wp_list_pluck( $postcode_locations, 'zone_id' ) );
$matches = wc_postcode_location_matcher( $postcode, $postcode_locations, 'zone_id', 'location_code', $country );
$do_not_match = array_unique( array_diff( $zone_ids_with_postcode_rules, array_keys( $matches ) ) );
if ( ! empty( $do_not_match ) ) {
$criteria[] = 'AND zones.zone_id NOT IN (' . implode( ',', $do_not_match ) . ')';
}
}
/**
* Get shipping zone criteria
*
* @since 3.6.6
* @param array $criteria Get zone criteria.
* @param array $package Package information.
* @param array $postcode_locations Postcode range and wildcard matching.
*/
$criteria = apply_filters( 'woocommerce_get_zone_criteria', $criteria, $package, $postcode_locations );
// Get matching zones.
return $wpdb->get_var(
"SELECT zones.zone_id FROM {$wpdb->prefix}woocommerce_shipping_zones as zones
LEFT OUTER JOIN {$wpdb->prefix}woocommerce_shipping_zone_locations as locations ON zones.zone_id = locations.zone_id AND location_type != 'postcode'
WHERE " . implode( ' ', $criteria ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
. ' ORDER BY zone_order ASC, zones.zone_id ASC LIMIT 1'
);
}
/**
* Return an ordered list of zones.
*
* @since 3.0.0
* @return array An array of objects containing a zone_id, zone_name, and zone_order.
*/
public function get_zones() {
global $wpdb;
return $wpdb->get_results( "SELECT zone_id, zone_name, zone_order FROM {$wpdb->prefix}woocommerce_shipping_zones order by zone_order ASC, zone_id ASC;" );
}
/**
* Return a zone ID from an instance ID.
*
* @since 3.0.0
* @param int $id Instnace ID.
* @return int
*/
public function get_zone_id_by_instance_id( $id ) {
global $wpdb;
return $wpdb->get_var( $wpdb->prepare( "SELECT zone_id FROM {$wpdb->prefix}woocommerce_shipping_zone_methods as methods WHERE methods.instance_id = %d LIMIT 1;", $id ) );
}
/**
* Read location data from the database.
*
* @param WC_Shipping_Zone $zone Shipping zone object.
*/
private function read_zone_locations( &$zone ) {
global $wpdb;
$locations = $wpdb->get_results(
$wpdb->prepare(
"SELECT location_code, location_type FROM {$wpdb->prefix}woocommerce_shipping_zone_locations WHERE zone_id = %d",
$zone->get_id()
)
);
if ( $locations ) {
foreach ( $locations as $location ) {
$zone->add_location( $location->location_code, $location->location_type );
}
}
}
/**
* Save locations to the DB.
* This function clears old locations, then re-inserts new if any changes are found.
*
* @since 3.0.0
*
* @param WC_Shipping_Zone $zone Shipping zone object.
*
* @return bool|void
*/
private function save_locations( &$zone ) {
$changed_props = array_keys( $zone->get_changes() );
if ( ! in_array( 'zone_locations', $changed_props, true ) ) {
return false;
}
global $wpdb;
$wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_locations', array( 'zone_id' => $zone->get_id() ) );
foreach ( $zone->get_zone_locations( 'edit' ) as $location ) {
$wpdb->insert(
$wpdb->prefix . 'woocommerce_shipping_zone_locations',
array(
'zone_id' => $zone->get_id(),
'location_code' => $location->code,
'location_type' => $location->type,
)
);
}
}
}

View File

@ -0,0 +1,448 @@
<?php
/**
* Webhook Data Store
*
* @version 3.3.0
* @package WooCommerce\Classes\Data_Store
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Webhook data store class.
*/
class WC_Webhook_Data_Store implements WC_Webhook_Data_Store_Interface {
/**
* Create a new webhook in the database.
*
* @since 3.3.0
* @param WC_Webhook $webhook Webhook instance.
*/
public function create( &$webhook ) {
global $wpdb;
$changes = $webhook->get_changes();
if ( isset( $changes['date_created'] ) ) {
$date_created = $webhook->get_date_created()->date( 'Y-m-d H:i:s' );
$date_created_gmt = gmdate( 'Y-m-d H:i:s', $webhook->get_date_created()->getTimestamp() );
} else {
$date_created = current_time( 'mysql' );
$date_created_gmt = current_time( 'mysql', 1 );
$webhook->set_date_created( $date_created );
}
// Pending delivery by default if not set while creating a new webhook.
if ( ! isset( $changes['pending_delivery'] ) ) {
$webhook->set_pending_delivery( true );
}
$data = array(
'status' => $webhook->get_status( 'edit' ),
'name' => $webhook->get_name( 'edit' ),
'user_id' => $webhook->get_user_id( 'edit' ),
'delivery_url' => $webhook->get_delivery_url( 'edit' ),
'secret' => $webhook->get_secret( 'edit' ),
'topic' => $webhook->get_topic( 'edit' ),
'date_created' => $date_created,
'date_created_gmt' => $date_created_gmt,
'api_version' => $this->get_api_version_number( $webhook->get_api_version( 'edit' ) ),
'failure_count' => $webhook->get_failure_count( 'edit' ),
'pending_delivery' => $webhook->get_pending_delivery( 'edit' ),
);
$wpdb->insert( $wpdb->prefix . 'wc_webhooks', $data ); // WPCS: DB call ok.
$webhook_id = $wpdb->insert_id;
$webhook->set_id( $webhook_id );
$webhook->apply_changes();
$this->delete_transients( $webhook->get_status( 'edit' ) );
WC_Cache_Helper::invalidate_cache_group( 'webhooks' );
do_action( 'woocommerce_new_webhook', $webhook_id, $webhook );
}
/**
* Read a webhook from the database.
*
* @since 3.3.0
* @param WC_Webhook $webhook Webhook instance.
* @throws Exception When webhook is invalid.
*/
public function read( &$webhook ) {
global $wpdb;
$data = wp_cache_get( $webhook->get_id(), 'webhooks' );
if ( false === $data ) {
$data = $wpdb->get_row( $wpdb->prepare( "SELECT webhook_id, status, name, user_id, delivery_url, secret, topic, date_created, date_modified, api_version, failure_count, pending_delivery FROM {$wpdb->prefix}wc_webhooks WHERE webhook_id = %d LIMIT 1;", $webhook->get_id() ), ARRAY_A ); // WPCS: cache ok, DB call ok.
wp_cache_add( $webhook->get_id(), $data, 'webhooks' );
}
if ( is_array( $data ) ) {
$webhook->set_props(
array(
'id' => $data['webhook_id'],
'status' => $data['status'],
'name' => $data['name'],
'user_id' => $data['user_id'],
'delivery_url' => $data['delivery_url'],
'secret' => $data['secret'],
'topic' => $data['topic'],
'date_created' => '0000-00-00 00:00:00' === $data['date_created'] ? null : $data['date_created'],
'date_modified' => '0000-00-00 00:00:00' === $data['date_modified'] ? null : $data['date_modified'],
'api_version' => $data['api_version'],
'failure_count' => $data['failure_count'],
'pending_delivery' => $data['pending_delivery'],
)
);
$webhook->set_object_read( true );
do_action( 'woocommerce_webhook_loaded', $webhook );
} else {
throw new Exception( __( 'Invalid webhook.', 'woocommerce' ) );
}
}
/**
* Update a webhook.
*
* @since 3.3.0
* @param WC_Webhook $webhook Webhook instance.
*/
public function update( &$webhook ) {
global $wpdb;
$changes = $webhook->get_changes();
$trigger = isset( $changes['delivery_url'] );
if ( isset( $changes['date_modified'] ) ) {
$date_modified = $webhook->get_date_modified()->date( 'Y-m-d H:i:s' );
$date_modified_gmt = gmdate( 'Y-m-d H:i:s', $webhook->get_date_modified()->getTimestamp() );
} else {
$date_modified = current_time( 'mysql' );
$date_modified_gmt = current_time( 'mysql', 1 );
$webhook->set_date_modified( $date_modified );
}
$data = array(
'status' => $webhook->get_status( 'edit' ),
'name' => $webhook->get_name( 'edit' ),
'user_id' => $webhook->get_user_id( 'edit' ),
'delivery_url' => $webhook->get_delivery_url( 'edit' ),
'secret' => $webhook->get_secret( 'edit' ),
'topic' => $webhook->get_topic( 'edit' ),
'date_modified' => $date_modified,
'date_modified_gmt' => $date_modified_gmt,
'api_version' => $this->get_api_version_number( $webhook->get_api_version( 'edit' ) ),
'failure_count' => $webhook->get_failure_count( 'edit' ),
'pending_delivery' => $webhook->get_pending_delivery( 'edit' ),
);
$wpdb->update(
$wpdb->prefix . 'wc_webhooks',
$data,
array(
'webhook_id' => $webhook->get_id(),
)
); // WPCS: DB call ok.
$webhook->apply_changes();
if ( isset( $changes['status'] ) ) {
// We need to delete all transients, because we can't be sure of the old status.
$this->delete_transients( 'all' );
}
wp_cache_delete( $webhook->get_id(), 'webhooks' );
WC_Cache_Helper::invalidate_cache_group( 'webhooks' );
if ( 'active' === $webhook->get_status() && ( $trigger || $webhook->get_pending_delivery() ) ) {
$webhook->deliver_ping();
}
do_action( 'woocommerce_webhook_updated', $webhook->get_id() );
}
/**
* Remove a webhook from the database.
*
* @since 3.3.0
* @param WC_Webhook $webhook Webhook instance.
*/
public function delete( &$webhook ) {
global $wpdb;
$wpdb->delete(
$wpdb->prefix . 'wc_webhooks',
array(
'webhook_id' => $webhook->get_id(),
),
array( '%d' )
); // WPCS: cache ok, DB call ok.
$this->delete_transients( 'all' );
wp_cache_delete( $webhook->get_id(), 'webhooks' );
WC_Cache_Helper::invalidate_cache_group( 'webhooks' );
do_action( 'woocommerce_webhook_deleted', $webhook->get_id(), $webhook );
}
/**
* Get API version number.
*
* @since 3.3.0
* @param string $api_version REST API version.
* @return int
*/
public function get_api_version_number( $api_version ) {
return 'legacy_v3' === $api_version ? -1 : intval( substr( $api_version, -1 ) );
}
/**
* Get webhooks IDs from the database.
*
* @since 3.3.0
* @throws InvalidArgumentException If a $status value is passed in that is not in the known wc_get_webhook_statuses() keys.
* @param string $status Optional - status to filter results by. Must be a key in return value of @see wc_get_webhook_statuses(). @since 3.6.0.
* @return int[]
*/
public function get_webhooks_ids( $status = '' ) {
if ( ! empty( $status ) ) {
$this->validate_status( $status );
}
$ids = get_transient( $this->get_transient_key( $status ) );
if ( false === $ids ) {
$ids = $this->search_webhooks(
array(
'limit' => -1,
'status' => $status,
)
);
$ids = array_map( 'absint', $ids );
set_transient( $this->get_transient_key( $status ), $ids );
}
return $ids;
}
/**
* Search webhooks.
*
* @param array $args Search arguments.
* @return array|object
*/
public function search_webhooks( $args ) {
global $wpdb;
$args = wp_parse_args(
$args,
array(
'limit' => 10,
'offset' => 0,
'order' => 'DESC',
'orderby' => 'id',
'paginate' => false,
)
);
// Map post statuses.
$statuses = array(
'publish' => 'active',
'draft' => 'paused',
'pending' => 'disabled',
);
// Map orderby to support a few post keys.
$orderby_mapping = array(
'ID' => 'webhook_id',
'id' => 'webhook_id',
'name' => 'name',
'title' => 'name',
'post_title' => 'name',
'post_name' => 'name',
'date_created' => 'date_created_gmt',
'date' => 'date_created_gmt',
'post_date' => 'date_created_gmt',
'date_modified' => 'date_modified_gmt',
'modified' => 'date_modified_gmt',
'post_modified' => 'date_modified_gmt',
);
$orderby = isset( $orderby_mapping[ $args['orderby'] ] ) ? $orderby_mapping[ $args['orderby'] ] : 'webhook_id';
$sort = 'ASC' === strtoupper( $args['order'] ) ? 'ASC' : 'DESC';
$order = "ORDER BY {$orderby} {$sort}";
$limit = -1 < $args['limit'] ? $wpdb->prepare( 'LIMIT %d', $args['limit'] ) : '';
$offset = 0 < $args['offset'] ? $wpdb->prepare( 'OFFSET %d', $args['offset'] ) : '';
$status = ! empty( $args['status'] ) ? $wpdb->prepare( 'AND `status` = %s', isset( $statuses[ $args['status'] ] ) ? $statuses[ $args['status'] ] : $args['status'] ) : '';
$search = ! empty( $args['search'] ) ? $wpdb->prepare( 'AND `name` LIKE %s', '%' . $wpdb->esc_like( sanitize_text_field( $args['search'] ) ) . '%' ) : '';
$include = '';
$exclude = '';
$date_created = '';
$date_modified = '';
if ( ! empty( $args['include'] ) ) {
$args['include'] = implode( ',', wp_parse_id_list( $args['include'] ) );
$include = 'AND webhook_id IN (' . $args['include'] . ')';
}
if ( ! empty( $args['exclude'] ) ) {
$args['exclude'] = implode( ',', wp_parse_id_list( $args['exclude'] ) );
$exclude = 'AND webhook_id NOT IN (' . $args['exclude'] . ')';
}
if ( ! empty( $args['after'] ) || ! empty( $args['before'] ) ) {
$args['after'] = empty( $args['after'] ) ? '0000-00-00' : $args['after'];
$args['before'] = empty( $args['before'] ) ? current_time( 'mysql', 1 ) : $args['before'];
$date_created = "AND `date_created_gmt` BETWEEN STR_TO_DATE('" . esc_sql( $args['after'] ) . "', '%Y-%m-%d %H:%i:%s') and STR_TO_DATE('" . esc_sql( $args['before'] ) . "', '%Y-%m-%d %H:%i:%s')";
}
if ( ! empty( $args['modified_after'] ) || ! empty( $args['modified_before'] ) ) {
$args['modified_after'] = empty( $args['modified_after'] ) ? '0000-00-00' : $args['modified_after'];
$args['modified_before'] = empty( $args['modified_before'] ) ? current_time( 'mysql', 1 ) : $args['modified_before'];
$date_modified = "AND `date_modified_gmt` BETWEEN STR_TO_DATE('" . esc_sql( $args['modified_after'] ) . "', '%Y-%m-%d %H:%i:%s') and STR_TO_DATE('" . esc_sql( $args['modified_before'] ) . "', '%Y-%m-%d %H:%i:%s')";
}
// Check for cache.
$cache_key = WC_Cache_Helper::get_cache_prefix( 'webhooks' ) . 'search_webhooks' . md5( implode( ',', $args ) );
$cache_value = wp_cache_get( $cache_key, 'webhook_search_results' );
if ( $cache_value ) {
return $cache_value;
}
if ( $args['paginate'] ) {
$query = trim(
"SELECT SQL_CALC_FOUND_ROWS webhook_id
FROM {$wpdb->prefix}wc_webhooks
WHERE 1=1
{$status}
{$search}
{$include}
{$exclude}
{$date_created}
{$date_modified}
{$order}
{$limit}
{$offset}"
);
$webhook_ids = wp_parse_id_list( $wpdb->get_col( $query ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$total = (int) $wpdb->get_var( 'SELECT FOUND_ROWS();' );
$return_value = (object) array(
'webhooks' => $webhook_ids,
'total' => $total,
'max_num_pages' => $args['limit'] > 1 ? ceil( $total / $args['limit'] ) : 1,
);
} else {
$query = trim(
"SELECT webhook_id
FROM {$wpdb->prefix}wc_webhooks
WHERE 1=1
{$status}
{$search}
{$include}
{$exclude}
{$date_created}
{$date_modified}
{$order}
{$limit}
{$offset}"
);
$webhook_ids = wp_parse_id_list( $wpdb->get_col( $query ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$return_value = $webhook_ids;
}
wp_cache_set( $cache_key, $return_value, 'webhook_search_results' );
return $return_value;
}
/**
* Count webhooks.
*
* @since 3.6.0
* @param string $status Status to count.
* @return int
*/
protected function get_webhook_count( $status = 'active' ) {
global $wpdb;
$cache_key = WC_Cache_Helper::get_cache_prefix( 'webhooks' ) . $status . '_count';
$count = wp_cache_get( $cache_key, 'webhooks' );
if ( false === $count ) {
$count = absint( $wpdb->get_var( $wpdb->prepare( "SELECT count( webhook_id ) FROM {$wpdb->prefix}wc_webhooks WHERE `status` = %s;", $status ) ) );
wp_cache_add( $cache_key, $count, 'webhooks' );
}
return $count;
}
/**
* Get total webhook counts by status.
*
* @return array
*/
public function get_count_webhooks_by_status() {
$statuses = array_keys( wc_get_webhook_statuses() );
$counts = array();
foreach ( $statuses as $status ) {
$counts[ $status ] = $this->get_webhook_count( $status );
}
return $counts;
}
/**
* Check if a given string is in known statuses, based on return value of @see wc_get_webhook_statuses().
*
* @since 3.6.0
* @throws InvalidArgumentException If $status is not empty and not in the known wc_get_webhook_statuses() keys.
* @param string $status Status to check.
*/
private function validate_status( $status ) {
if ( ! array_key_exists( $status, wc_get_webhook_statuses() ) ) {
throw new InvalidArgumentException( sprintf( 'Invalid status given: %s. Status must be one of: %s.', $status, implode( ', ', array_keys( wc_get_webhook_statuses() ) ) ) );
}
}
/**
* Get the transient key used to cache a set of webhook IDs, optionally filtered by status.
*
* @since 3.6.0
* @param string $status Optional - status of cache key.
* @return string
*/
private function get_transient_key( $status = '' ) {
return empty( $status ) ? 'woocommerce_webhook_ids' : sprintf( 'woocommerce_webhook_ids_status_%s', $status );
}
/**
* Delete the transients used to cache a set of webhook IDs, optionally filtered by status.
*
* @since 3.6.0
* @param string $status Optional - status of cache to delete, or 'all' to delete all caches.
*/
private function delete_transients( $status = '' ) {
// Always delete the non-filtered cache.
delete_transient( $this->get_transient_key( '' ) );
if ( ! empty( $status ) ) {
if ( 'all' === $status ) {
foreach ( wc_get_webhook_statuses() as $status_key => $status_string ) {
delete_transient( $this->get_transient_key( $status_key ) );
}
} else {
delete_transient( $this->get_transient_key( $status ) );
}
}
}
}