Move legacy API files from /api/ to /legacy/api
This commit is contained in:
parent
3776599fcd
commit
00a8ef5d4d
|
@ -0,0 +1,164 @@
|
|||
<?php
|
||||
/**
|
||||
* REST API Legacy Coupons controller
|
||||
*
|
||||
* Handles requests to the /coupons endpoint.
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 3.0.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* REST API Legacy Coupons controller class.
|
||||
*
|
||||
* @package WooCommerce/API
|
||||
* @extends WC_REST_CRUD_Controller
|
||||
*/
|
||||
class WC_REST_Legacy_Coupons_Controller extends WC_REST_CRUD_Controller {
|
||||
|
||||
/**
|
||||
* Query args.
|
||||
*
|
||||
* @deprecated 3.0.0
|
||||
*
|
||||
* @param array $args Query args
|
||||
* @param WP_REST_Request $request Request data.
|
||||
* @return array
|
||||
*/
|
||||
public function query_args( $args, $request ) {
|
||||
if ( ! empty( $request['code'] ) ) {
|
||||
$id = wc_get_coupon_id_by_code( $request['code'] );
|
||||
$args['post__in'] = array( $id );
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a single coupon output for response.
|
||||
*
|
||||
* @deprecated 3.0.0
|
||||
*
|
||||
* @param WP_Post $post Post object.
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return WP_REST_Response $data
|
||||
*/
|
||||
public function prepare_item_for_response( $post, $request ) {
|
||||
$coupon = new WC_Coupon( (int) $post->ID );
|
||||
$data = $coupon->get_data();
|
||||
|
||||
$format_decimal = array( 'amount', 'minimum_amount', 'maximum_amount' );
|
||||
$format_date = array( 'date_created', 'date_modified', 'date_expires' );
|
||||
$format_null = array( 'usage_limit', 'usage_limit_per_user', 'limit_usage_to_x_items' );
|
||||
|
||||
// Format decimal values.
|
||||
foreach ( $format_decimal as $key ) {
|
||||
$data[ $key ] = wc_format_decimal( $data[ $key ], 2 );
|
||||
}
|
||||
|
||||
// Format date values.
|
||||
foreach ( $format_date as $key ) {
|
||||
$data[ $key ] = $data[ $key ] ? wc_rest_prepare_date_response( get_gmt_from_date( date( 'Y-m-d H:i:s', $data[ $key ] ) ) ) : null;
|
||||
}
|
||||
|
||||
// Format null values.
|
||||
foreach ( $format_null as $key ) {
|
||||
$data[ $key ] = $data[ $key ] ? $data[ $key ] : null;
|
||||
}
|
||||
|
||||
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
|
||||
$data = $this->add_additional_fields_to_object( $data, $request );
|
||||
$data = $this->filter_response_by_context( $data, $context );
|
||||
$response = rest_ensure_response( $data );
|
||||
$response->add_links( $this->prepare_links( $post, $request ) );
|
||||
|
||||
/**
|
||||
* Filter the data for a response.
|
||||
*
|
||||
* The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
|
||||
* prepared for the response.
|
||||
*
|
||||
* @param WP_REST_Response $response The response object.
|
||||
* @param WP_Post $post Post object.
|
||||
* @param WP_REST_Request $request Request object.
|
||||
*/
|
||||
return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a single coupon for create or update.
|
||||
*
|
||||
* @deprecated 3.0.0
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return WP_Error|stdClass $data Post object.
|
||||
*/
|
||||
protected function prepare_item_for_database( $request ) {
|
||||
global $wpdb;
|
||||
|
||||
$id = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
|
||||
$coupon = new WC_Coupon( $id );
|
||||
$schema = $this->get_item_schema();
|
||||
$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );
|
||||
|
||||
// Validate required POST fields.
|
||||
if ( 'POST' === $request->get_method() && 0 === $coupon->get_id() ) {
|
||||
if ( empty( $request['code'] ) ) {
|
||||
return new WP_Error( 'woocommerce_rest_empty_coupon_code', sprintf( __( 'The coupon code cannot be empty.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) );
|
||||
}
|
||||
}
|
||||
|
||||
// Handle all writable props.
|
||||
foreach ( $data_keys as $key ) {
|
||||
$value = $request[ $key ];
|
||||
|
||||
if ( ! is_null( $value ) ) {
|
||||
switch ( $key ) {
|
||||
case 'code' :
|
||||
$coupon_code = wc_format_coupon_code( $value );
|
||||
$id = $coupon->get_id() ? $coupon->get_id() : 0;
|
||||
$id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id );
|
||||
|
||||
if ( $id_from_code ) {
|
||||
return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
$coupon->set_code( $coupon_code );
|
||||
break;
|
||||
case 'meta_data' :
|
||||
if ( is_array( $value ) ) {
|
||||
foreach ( $value as $meta ) {
|
||||
$coupon->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'description' :
|
||||
$coupon->set_description( wp_filter_post_kses( $value ) );
|
||||
break;
|
||||
default :
|
||||
if ( is_callable( array( $coupon, "set_{$key}" ) ) ) {
|
||||
$coupon->{"set_{$key}"}( $value );
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the query_vars used in `get_items` for the constructed query.
|
||||
*
|
||||
* The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
|
||||
* prepared for insertion.
|
||||
*
|
||||
* @param WC_Coupon $coupon The coupon object.
|
||||
* @param WP_REST_Request $request Request object.
|
||||
*/
|
||||
return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $coupon, $request );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,306 @@
|
|||
<?php
|
||||
/**
|
||||
* REST API Legacy Orders controller
|
||||
*
|
||||
* Handles requests to the /orders endpoint.
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 3.0.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* REST API Legacy Orders controller class.
|
||||
*
|
||||
* @package WooCommerce/API
|
||||
* @extends WC_REST_CRUD_Controller
|
||||
*/
|
||||
class WC_REST_Legacy_Orders_Controller extends WC_REST_CRUD_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $namespace = 'wc/v2';
|
||||
|
||||
/**
|
||||
* Query args.
|
||||
*
|
||||
* @deprecated 3.0
|
||||
*
|
||||
* @param array $args
|
||||
* @param WP_REST_Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function query_args( $args, $request ) {
|
||||
global $wpdb;
|
||||
|
||||
// Set post_status.
|
||||
if ( 'any' !== $request['status'] ) {
|
||||
$args['post_status'] = 'wc-' . $request['status'];
|
||||
} else {
|
||||
$args['post_status'] = 'any';
|
||||
}
|
||||
|
||||
if ( ! empty( $request['customer'] ) ) {
|
||||
if ( ! empty( $args['meta_query'] ) ) {
|
||||
$args['meta_query'] = array();
|
||||
}
|
||||
|
||||
$args['meta_query'][] = array(
|
||||
'key' => '_customer_user',
|
||||
'value' => $request['customer'],
|
||||
'type' => 'NUMERIC',
|
||||
);
|
||||
}
|
||||
|
||||
// Search by product.
|
||||
if ( ! empty( $request['product'] ) ) {
|
||||
$order_ids = $wpdb->get_col( $wpdb->prepare( "
|
||||
SELECT order_id
|
||||
FROM {$wpdb->prefix}woocommerce_order_items
|
||||
WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d )
|
||||
AND order_item_type = 'line_item'
|
||||
", $request['product'] ) );
|
||||
|
||||
// Force WP_Query return empty if don't found any order.
|
||||
$order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 );
|
||||
|
||||
$args['post__in'] = $order_ids;
|
||||
}
|
||||
|
||||
// Search.
|
||||
if ( ! empty( $args['s'] ) ) {
|
||||
$order_ids = wc_order_search( $args['s'] );
|
||||
|
||||
if ( ! empty( $order_ids ) ) {
|
||||
unset( $args['s'] );
|
||||
$args['post__in'] = array_merge( $order_ids, array( 0 ) );
|
||||
}
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a single order output for response.
|
||||
*
|
||||
* @deprecated 3.0
|
||||
*
|
||||
* @param WP_Post $post Post object.
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return WP_REST_Response $data
|
||||
*/
|
||||
public function prepare_item_for_response( $post, $request ) {
|
||||
$this->request = $request;
|
||||
$this->request['dp'] = is_null( $this->request['dp'] ) ? wc_get_price_decimals() : absint( $this->request['dp'] );
|
||||
$statuses = wc_get_order_statuses();
|
||||
$order = wc_get_order( $post );
|
||||
$data = array_merge( array( 'id' => $order->get_id() ), $order->get_data() );
|
||||
$format_decimal = array( 'discount_total', 'discount_tax', 'shipping_total', 'shipping_tax', 'shipping_total', 'shipping_tax', 'cart_tax', 'total', 'total_tax' );
|
||||
$format_date = array( 'date_created', 'date_modified', 'date_completed', 'date_paid' );
|
||||
$format_line_items = array( 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines' );
|
||||
|
||||
// Format decimal values.
|
||||
foreach ( $format_decimal as $key ) {
|
||||
$data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] );
|
||||
}
|
||||
|
||||
// Format date values.
|
||||
foreach ( $format_date as $key ) {
|
||||
$data[ $key ] = $data[ $key ] ? wc_rest_prepare_date_response( get_gmt_from_date( date( 'Y-m-d H:i:s', $data[ $key ] ) ) ) : false;
|
||||
}
|
||||
|
||||
// Format the order status.
|
||||
$data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status'];
|
||||
|
||||
// Format line items.
|
||||
foreach ( $format_line_items as $key ) {
|
||||
$data[ $key ] = array_values( array_map( array( $this, 'get_order_item_data' ), $data[ $key ] ) );
|
||||
}
|
||||
|
||||
// Refunds.
|
||||
$data['refunds'] = array();
|
||||
foreach ( $order->get_refunds() as $refund ) {
|
||||
$data['refunds'][] = array(
|
||||
'id' => $refund->get_id(),
|
||||
'refund' => $refund->get_reason() ? $refund->get_reason() : '',
|
||||
'total' => '-' . wc_format_decimal( $refund->get_amount(), $this->request['dp'] ),
|
||||
);
|
||||
}
|
||||
|
||||
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
|
||||
$data = $this->add_additional_fields_to_object( $data, $request );
|
||||
$data = $this->filter_response_by_context( $data, $context );
|
||||
$response = rest_ensure_response( $data );
|
||||
$response->add_links( $this->prepare_links( $order, $request ) );
|
||||
|
||||
/**
|
||||
* Filter the data for a response.
|
||||
*
|
||||
* The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
|
||||
* prepared for the response.
|
||||
*
|
||||
* @param WP_REST_Response $response The response object.
|
||||
* @param WP_Post $post Post object.
|
||||
* @param WP_REST_Request $request Request object.
|
||||
*/
|
||||
return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a single order for create.
|
||||
*
|
||||
* @deprecated 3.0
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return WP_Error|WC_Order $data Object.
|
||||
*/
|
||||
protected function prepare_item_for_database( $request ) {
|
||||
$id = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
|
||||
$order = new WC_Order( $id );
|
||||
$schema = $this->get_item_schema();
|
||||
$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );
|
||||
|
||||
// Handle all writable props
|
||||
foreach ( $data_keys as $key ) {
|
||||
$value = $request[ $key ];
|
||||
|
||||
if ( ! is_null( $value ) ) {
|
||||
switch ( $key ) {
|
||||
case 'billing' :
|
||||
case 'shipping' :
|
||||
$this->update_address( $order, $value, $key );
|
||||
break;
|
||||
case 'line_items' :
|
||||
case 'shipping_lines' :
|
||||
case 'fee_lines' :
|
||||
case 'coupon_lines' :
|
||||
if ( is_array( $value ) ) {
|
||||
foreach ( $value as $item ) {
|
||||
if ( is_array( $item ) ) {
|
||||
if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) {
|
||||
$order->remove_item( $item['id'] );
|
||||
} else {
|
||||
$this->set_item( $order, $key, $item );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'meta_data' :
|
||||
if ( is_array( $value ) ) {
|
||||
foreach ( $value as $meta ) {
|
||||
$order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
|
||||
}
|
||||
}
|
||||
break;
|
||||
default :
|
||||
if ( is_callable( array( $order, "set_{$key}" ) ) ) {
|
||||
$order->{"set_{$key}"}( $value );
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the data for the insert.
|
||||
*
|
||||
* The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
|
||||
* prepared for the response.
|
||||
*
|
||||
* @param WC_Order $order The Order object.
|
||||
* @param WP_REST_Request $request Request object.
|
||||
*/
|
||||
return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $order, $request );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create base WC Order object.
|
||||
*
|
||||
* @deprecated 3.0.0
|
||||
*
|
||||
* @param array $data
|
||||
* @return WC_Order
|
||||
*/
|
||||
protected function create_base_order( $data ) {
|
||||
return wc_create_order( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create order.
|
||||
*
|
||||
* @deprecated 3.0.0
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return int|WP_Error
|
||||
*/
|
||||
protected function create_order( $request ) {
|
||||
try {
|
||||
// Make sure customer exists.
|
||||
if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] && false === get_user_by( 'id', $request['customer_id'] ) ) {
|
||||
throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id',__( 'Customer ID is invalid.', 'woocommerce' ), 400 );
|
||||
}
|
||||
|
||||
// Make sure customer is part of blog.
|
||||
if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) {
|
||||
add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' );
|
||||
}
|
||||
|
||||
$order = $this->prepare_item_for_database( $request );
|
||||
$order->set_created_via( 'rest-api' );
|
||||
$order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
|
||||
$order->calculate_totals();
|
||||
$order->save();
|
||||
|
||||
// Handle set paid.
|
||||
if ( true === $request['set_paid'] ) {
|
||||
$order->payment_complete( $request['transaction_id'] );
|
||||
}
|
||||
|
||||
return $order->get_id();
|
||||
} catch ( WC_Data_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
|
||||
} catch ( WC_REST_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update order.
|
||||
*
|
||||
* @deprecated 3.0.0
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return int|WP_Error
|
||||
*/
|
||||
protected function update_order( $request ) {
|
||||
try {
|
||||
$order = $this->prepare_item_for_database( $request );
|
||||
$order->save();
|
||||
|
||||
// Handle set paid.
|
||||
if ( $order->needs_payment() && true === $request['set_paid'] ) {
|
||||
$order->payment_complete( $request['transaction_id'] );
|
||||
}
|
||||
|
||||
// If items have changed, recalculate order totals.
|
||||
if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) {
|
||||
$order->calculate_totals();
|
||||
}
|
||||
|
||||
return $order->get_id();
|
||||
} catch ( WC_Data_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
|
||||
} catch ( WC_REST_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,804 @@
|
|||
<?php
|
||||
/**
|
||||
* REST API Legacy Products controller
|
||||
*
|
||||
* Handles requests to the /products endpoint.
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 3.0.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* REST API Legacy Products controller class.
|
||||
*
|
||||
* @package WooCommerce/API
|
||||
* @extends WC_REST_CRUD_Controller
|
||||
*/
|
||||
class WC_REST_Legacy_Products_Controller extends WC_REST_CRUD_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $namespace = 'wc/v2';
|
||||
|
||||
/**
|
||||
* Query args.
|
||||
*
|
||||
* @deprecated 3.0.0
|
||||
*
|
||||
* @param array $args Request args.
|
||||
* @param WP_REST_Request $request Request data.
|
||||
* @return array
|
||||
*/
|
||||
public function query_args( $args, $request ) {
|
||||
// Set post_status.
|
||||
$args['post_status'] = $request['status'];
|
||||
|
||||
// Taxonomy query to filter products by type, category,
|
||||
// tag, shipping class, and attribute.
|
||||
$tax_query = array();
|
||||
|
||||
// Map between taxonomy name and arg's key.
|
||||
$taxonomies = array(
|
||||
'product_cat' => 'category',
|
||||
'product_tag' => 'tag',
|
||||
'product_shipping_class' => 'shipping_class',
|
||||
);
|
||||
|
||||
// Set tax_query for each passed arg.
|
||||
foreach ( $taxonomies as $taxonomy => $key ) {
|
||||
if ( ! empty( $request[ $key ] ) ) {
|
||||
$tax_query[] = array(
|
||||
'taxonomy' => $taxonomy,
|
||||
'field' => 'term_id',
|
||||
'terms' => $request[ $key ],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter product type by slug.
|
||||
if ( ! empty( $request['type'] ) ) {
|
||||
$tax_query[] = array(
|
||||
'taxonomy' => 'product_type',
|
||||
'field' => 'slug',
|
||||
'terms' => $request['type'],
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by attribute and term.
|
||||
if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) {
|
||||
if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) {
|
||||
$tax_query[] = array(
|
||||
'taxonomy' => $request['attribute'],
|
||||
'field' => 'term_id',
|
||||
'terms' => $request['attribute_term'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $tax_query ) ) {
|
||||
$args['tax_query'] = $tax_query;
|
||||
}
|
||||
|
||||
// Filter featured.
|
||||
if ( is_bool( $request['featured'] ) ) {
|
||||
$args['tax_query'][] = array(
|
||||
'taxonomy' => 'product_visibility',
|
||||
'field' => 'name',
|
||||
'terms' => 'featured',
|
||||
'operator' => true === $request['featured'] ? 'IN' : 'NOT IN',
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by sku.
|
||||
if ( ! empty( $request['sku'] ) ) {
|
||||
$skus = explode( ',', $request['sku'] );
|
||||
// Include the current string as a SKU too.
|
||||
if ( 1 < count( $skus ) ) {
|
||||
$skus[] = $request['sku'];
|
||||
}
|
||||
|
||||
$args['meta_query'] = $this->add_meta_query( $args, array(
|
||||
'key' => '_sku',
|
||||
'value' => $skus,
|
||||
'compare' => 'IN',
|
||||
) );
|
||||
}
|
||||
|
||||
// Filter by tax class.
|
||||
if ( ! empty( $request['tax_class'] ) ) {
|
||||
$args['meta_query'] = $this->add_meta_query( $args, array(
|
||||
'key' => '_tax_class',
|
||||
'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '',
|
||||
) );
|
||||
}
|
||||
|
||||
// Price filter.
|
||||
if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) {
|
||||
$args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) );
|
||||
}
|
||||
|
||||
// Filter product in stock or out of stock.
|
||||
if ( is_bool( $request['in_stock'] ) ) {
|
||||
$args['meta_query'] = $this->add_meta_query( $args, array(
|
||||
'key' => '_stock_status',
|
||||
'value' => true === $request['in_stock'] ? 'instock' : 'outofstock',
|
||||
) );
|
||||
}
|
||||
|
||||
// Filter by on sale products.
|
||||
if ( is_bool( $request['on_sale'] ) ) {
|
||||
$on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in';
|
||||
$args[ $on_sale_key ] += wc_get_product_ids_on_sale();
|
||||
}
|
||||
|
||||
// Force the post_type argument, since it's not a user input variable.
|
||||
if ( ! empty( $request['sku'] ) ) {
|
||||
$args['post_type'] = array( 'product', 'product_variation' );
|
||||
} else {
|
||||
$args['post_type'] = $this->post_type;
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a single product output for response.
|
||||
*
|
||||
* @deprecated 3.0.0
|
||||
*
|
||||
* @param WP_Post $post Post object.
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function prepare_item_for_response( $post, $request ) {
|
||||
$product = wc_get_product( $post );
|
||||
$data = $this->get_product_data( $product );
|
||||
|
||||
// Add variations to variable products.
|
||||
if ( $product->is_type( 'variable' ) && $product->has_child() ) {
|
||||
$data['variations'] = $product->get_children();
|
||||
}
|
||||
|
||||
// Add grouped products data.
|
||||
if ( $product->is_type( 'grouped' ) && $product->has_child() ) {
|
||||
$data['grouped_products'] = $product->get_children();
|
||||
}
|
||||
|
||||
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
|
||||
$data = $this->add_additional_fields_to_object( $data, $request );
|
||||
$data = $this->filter_response_by_context( $data, $context );
|
||||
|
||||
// Wrap the data in a response object.
|
||||
$response = rest_ensure_response( $data );
|
||||
|
||||
$response->add_links( $this->prepare_links( $product, $request ) );
|
||||
|
||||
/**
|
||||
* Filter the data for a response.
|
||||
*
|
||||
* The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
|
||||
* prepared for the response.
|
||||
*
|
||||
* @param WP_REST_Response $response The response object.
|
||||
* @param WP_Post $post Post object.
|
||||
* @param WP_REST_Request $request Request object.
|
||||
*/
|
||||
return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product menu order.
|
||||
*
|
||||
* @deprecated 3.0.0
|
||||
* @param WC_Product $product Product instance.
|
||||
* @return int
|
||||
*/
|
||||
protected function get_product_menu_order( $product ) {
|
||||
return $product->get_menu_order();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save product meta.
|
||||
*
|
||||
* @deprecated 3.0.0
|
||||
* @param WC_Product $product
|
||||
* @param WP_REST_Request $request
|
||||
* @return bool
|
||||
* @throws WC_REST_Exception
|
||||
*/
|
||||
protected function save_product_meta( $product, $request ) {
|
||||
$product = $this->set_product_meta( $product, $request );
|
||||
$product->save();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set product meta.
|
||||
*
|
||||
* @deprecated 3.0.0
|
||||
*
|
||||
* @throws WC_REST_Exception REST API exceptions.
|
||||
* @param WC_Product $product Product instance.
|
||||
* @param WP_REST_Request $request Request data.
|
||||
* @return WC_Product
|
||||
*/
|
||||
protected function set_product_meta( $product, $request ) {
|
||||
// Virtual.
|
||||
if ( isset( $request['virtual'] ) ) {
|
||||
$product->set_virtual( $request['virtual'] );
|
||||
}
|
||||
|
||||
// Tax status.
|
||||
if ( isset( $request['tax_status'] ) ) {
|
||||
$product->set_tax_status( $request['tax_status'] );
|
||||
}
|
||||
|
||||
// Tax Class.
|
||||
if ( isset( $request['tax_class'] ) ) {
|
||||
$product->set_tax_class( $request['tax_class'] );
|
||||
}
|
||||
|
||||
// Catalog Visibility.
|
||||
if ( isset( $request['catalog_visibility'] ) ) {
|
||||
$product->set_catalog_visibility( $request['catalog_visibility'] );
|
||||
}
|
||||
|
||||
// Purchase Note.
|
||||
if ( isset( $request['purchase_note'] ) ) {
|
||||
$product->set_purchase_note( wc_clean( $request['purchase_note'] ) );
|
||||
}
|
||||
|
||||
// Featured Product.
|
||||
if ( isset( $request['featured'] ) ) {
|
||||
$product->set_featured( $request['featured'] );
|
||||
}
|
||||
|
||||
// Shipping data.
|
||||
$product = $this->save_product_shipping_data( $product, $request );
|
||||
|
||||
// SKU.
|
||||
if ( isset( $request['sku'] ) ) {
|
||||
$product->set_sku( wc_clean( $request['sku'] ) );
|
||||
}
|
||||
|
||||
// Attributes.
|
||||
if ( isset( $request['attributes'] ) ) {
|
||||
$attributes = array();
|
||||
|
||||
foreach ( $request['attributes'] as $attribute ) {
|
||||
$attribute_id = 0;
|
||||
$attribute_name = '';
|
||||
|
||||
// Check ID for global attributes or name for product attributes.
|
||||
if ( ! empty( $attribute['id'] ) ) {
|
||||
$attribute_id = absint( $attribute['id'] );
|
||||
$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
|
||||
} elseif ( ! empty( $attribute['name'] ) ) {
|
||||
$attribute_name = wc_clean( $attribute['name'] );
|
||||
}
|
||||
|
||||
if ( ! $attribute_id && ! $attribute_name ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $attribute_id ) {
|
||||
|
||||
if ( isset( $attribute['options'] ) ) {
|
||||
$options = $attribute['options'];
|
||||
|
||||
if ( ! is_array( $attribute['options'] ) ) {
|
||||
// Text based attributes - Posted values are term names.
|
||||
$options = explode( WC_DELIMITER, $options );
|
||||
}
|
||||
|
||||
$values = array_map( 'wc_sanitize_term_text_based', $options );
|
||||
$values = array_filter( $values, 'strlen' );
|
||||
} else {
|
||||
$values = array();
|
||||
}
|
||||
|
||||
if ( ! empty( $values ) ) {
|
||||
// Add attribute to array, but don't set values.
|
||||
$attribute_object = new WC_Product_Attribute();
|
||||
$attribute_object->set_id( $attribute_id );
|
||||
$attribute_object->set_name( $attribute_name );
|
||||
$attribute_object->set_options( $values );
|
||||
$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
|
||||
$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
|
||||
$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
|
||||
$attributes[] = $attribute_object;
|
||||
}
|
||||
} elseif ( isset( $attribute['options'] ) ) {
|
||||
// Custom attribute - Add attribute to array and set the values.
|
||||
if ( is_array( $attribute['options'] ) ) {
|
||||
$values = $attribute['options'];
|
||||
} else {
|
||||
$values = explode( WC_DELIMITER, $attribute['options'] );
|
||||
}
|
||||
$attribute_object = new WC_Product_Attribute();
|
||||
$attribute_object->set_name( $attribute_name );
|
||||
$attribute_object->set_options( $values );
|
||||
$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
|
||||
$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
|
||||
$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
|
||||
$attributes[] = $attribute_object;
|
||||
}
|
||||
}
|
||||
$product->set_attributes( $attributes );
|
||||
}
|
||||
|
||||
// Sales and prices.
|
||||
if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) {
|
||||
$product->set_regular_price( '' );
|
||||
$product->set_sale_price( '' );
|
||||
$product->set_date_on_sale_to( '' );
|
||||
$product->set_date_on_sale_from( '' );
|
||||
$product->set_price( '' );
|
||||
} else {
|
||||
// Regular Price.
|
||||
if ( isset( $request['regular_price'] ) ) {
|
||||
$product->set_regular_price( $request['regular_price'] );
|
||||
}
|
||||
|
||||
// Sale Price.
|
||||
if ( isset( $request['sale_price'] ) ) {
|
||||
$product->set_sale_price( $request['sale_price'] );
|
||||
}
|
||||
|
||||
if ( isset( $request['date_on_sale_from'] ) ) {
|
||||
$product->set_date_on_sale_from( $request['date_on_sale_from'] );
|
||||
}
|
||||
|
||||
if ( isset( $request['date_on_sale_to'] ) ) {
|
||||
$product->set_date_on_sale_to( $request['date_on_sale_to'] );
|
||||
}
|
||||
}
|
||||
|
||||
// Product parent ID for groups.
|
||||
if ( isset( $request['parent_id'] ) ) {
|
||||
$product->set_parent_id( $request['parent_id'] );
|
||||
}
|
||||
|
||||
// Sold individually.
|
||||
if ( isset( $request['sold_individually'] ) ) {
|
||||
$product->set_sold_individually( $request['sold_individually'] );
|
||||
}
|
||||
|
||||
// Stock status.
|
||||
if ( isset( $request['in_stock'] ) ) {
|
||||
$stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock';
|
||||
} else {
|
||||
$stock_status = $product->get_stock_status();
|
||||
}
|
||||
|
||||
// Stock data.
|
||||
if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
|
||||
// Manage stock.
|
||||
if ( isset( $request['manage_stock'] ) ) {
|
||||
$product->set_manage_stock( $request['manage_stock'] );
|
||||
}
|
||||
|
||||
// Backorders.
|
||||
if ( isset( $request['backorders'] ) ) {
|
||||
$product->set_backorders( $request['backorders'] );
|
||||
}
|
||||
|
||||
if ( $product->is_type( 'grouped' ) ) {
|
||||
$product->set_manage_stock( 'no' );
|
||||
$product->set_backorders( 'no' );
|
||||
$product->set_stock_quantity( '' );
|
||||
$product->set_stock_status( $stock_status );
|
||||
} elseif ( $product->is_type( 'external' ) ) {
|
||||
$product->set_manage_stock( 'no' );
|
||||
$product->set_backorders( 'no' );
|
||||
$product->set_stock_quantity( '' );
|
||||
$product->set_stock_status( 'instock' );
|
||||
} elseif ( $product->get_manage_stock() ) {
|
||||
// Stock status is always determined by children so sync later.
|
||||
if ( ! $product->is_type( 'variable' ) ) {
|
||||
$product->set_stock_status( $stock_status );
|
||||
}
|
||||
|
||||
// Stock quantity.
|
||||
if ( isset( $request['stock_quantity'] ) ) {
|
||||
$product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) );
|
||||
} elseif ( isset( $request['inventory_delta'] ) ) {
|
||||
$stock_quantity = wc_stock_amount( $product->get_stock_quantity() );
|
||||
$stock_quantity += wc_stock_amount( $request['inventory_delta'] );
|
||||
$product->set_stock_quantity( wc_stock_amount( $stock_quantity ) );
|
||||
}
|
||||
} else {
|
||||
// Don't manage stock.
|
||||
$product->set_manage_stock( 'no' );
|
||||
$product->set_stock_quantity( '' );
|
||||
$product->set_stock_status( $stock_status );
|
||||
}
|
||||
} elseif ( ! $product->is_type( 'variable' ) ) {
|
||||
$product->set_stock_status( $stock_status );
|
||||
}
|
||||
|
||||
// Upsells.
|
||||
if ( isset( $request['upsell_ids'] ) ) {
|
||||
$upsells = array();
|
||||
$ids = $request['upsell_ids'];
|
||||
|
||||
if ( ! empty( $ids ) ) {
|
||||
foreach ( $ids as $id ) {
|
||||
if ( $id && $id > 0 ) {
|
||||
$upsells[] = $id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$product->set_upsell_ids( $upsells );
|
||||
}
|
||||
|
||||
// Cross sells.
|
||||
if ( isset( $request['cross_sell_ids'] ) ) {
|
||||
$crosssells = array();
|
||||
$ids = $request['cross_sell_ids'];
|
||||
|
||||
if ( ! empty( $ids ) ) {
|
||||
foreach ( $ids as $id ) {
|
||||
if ( $id && $id > 0 ) {
|
||||
$crosssells[] = $id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$product->set_cross_sell_ids( $crosssells );
|
||||
}
|
||||
|
||||
// Product categories.
|
||||
if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) {
|
||||
$product = $this->save_taxonomy_terms( $product, $request['categories'] );
|
||||
}
|
||||
|
||||
// Product tags.
|
||||
if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) {
|
||||
$product = $this->save_taxonomy_terms( $product, $request['tags'], 'tag' );
|
||||
}
|
||||
|
||||
// Downloadable.
|
||||
if ( isset( $request['downloadable'] ) ) {
|
||||
$product->set_downloadable( $request['downloadable'] );
|
||||
}
|
||||
|
||||
// Downloadable options.
|
||||
if ( $product->get_downloadable() ) {
|
||||
|
||||
// Downloadable files.
|
||||
if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) {
|
||||
$product = $this->save_downloadable_files( $product, $request['downloads'] );
|
||||
}
|
||||
|
||||
// Download limit.
|
||||
if ( isset( $request['download_limit'] ) ) {
|
||||
$product->set_download_limit( $request['download_limit'] );
|
||||
}
|
||||
|
||||
// Download expiry.
|
||||
if ( isset( $request['download_expiry'] ) ) {
|
||||
$product->set_download_expiry( $request['download_expiry'] );
|
||||
}
|
||||
}
|
||||
|
||||
// Product url and button text for external products.
|
||||
if ( $product->is_type( 'external' ) ) {
|
||||
if ( isset( $request['external_url'] ) ) {
|
||||
$product->set_product_url( $request['external_url'] );
|
||||
}
|
||||
|
||||
if ( isset( $request['button_text'] ) ) {
|
||||
$product->set_button_text( $request['button_text'] );
|
||||
}
|
||||
}
|
||||
|
||||
// Save default attributes for variable products.
|
||||
if ( $product->is_type( 'variable' ) ) {
|
||||
$product = $this->save_default_attributes( $product, $request );
|
||||
}
|
||||
|
||||
return $product;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save variations.
|
||||
*
|
||||
* @deprecated 3.0.0
|
||||
*
|
||||
* @throws WC_REST_Exception REST API exceptions.
|
||||
* @param WC_Product $product Product instance.
|
||||
* @param WP_REST_Request $request Request data.
|
||||
* @return bool
|
||||
*/
|
||||
protected function save_variations_data( $product, $request ) {
|
||||
foreach ( $request['variations'] as $menu_order => $data ) {
|
||||
$variation = new WC_Product_Variation( isset( $data['id'] ) ? absint( $data['id'] ) : 0 );
|
||||
|
||||
// Create initial name and status.
|
||||
if ( ! $variation->get_slug() ) {
|
||||
/* translators: 1: variation id 2: product name */
|
||||
$variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) );
|
||||
$variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' );
|
||||
}
|
||||
|
||||
// Parent ID.
|
||||
$variation->set_parent_id( $product->get_id() );
|
||||
|
||||
// Menu order.
|
||||
$variation->set_menu_order( $menu_order );
|
||||
|
||||
// Status.
|
||||
if ( isset( $data['visible'] ) ) {
|
||||
$variation->set_status( false === $data['visible'] ? 'private' : 'publish' );
|
||||
}
|
||||
|
||||
// SKU.
|
||||
if ( isset( $data['sku'] ) ) {
|
||||
$variation->set_sku( wc_clean( $data['sku'] ) );
|
||||
}
|
||||
|
||||
// Thumbnail.
|
||||
if ( isset( $data['image'] ) && is_array( $data['image'] ) ) {
|
||||
$image = $data['image'];
|
||||
$image = current( $image );
|
||||
if ( is_array( $image ) ) {
|
||||
$image['position'] = 0;
|
||||
}
|
||||
|
||||
$variation = $this->set_product_images( $variation, array( $image ) );
|
||||
}
|
||||
|
||||
// Virtual variation.
|
||||
if ( isset( $data['virtual'] ) ) {
|
||||
$variation->set_virtual( $data['virtual'] );
|
||||
}
|
||||
|
||||
// Downloadable variation.
|
||||
if ( isset( $data['downloadable'] ) ) {
|
||||
$variation->set_downloadable( $data['downloadable'] );
|
||||
}
|
||||
|
||||
// Downloads.
|
||||
if ( $variation->get_downloadable() ) {
|
||||
// Downloadable files.
|
||||
if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) {
|
||||
$variation = $this->save_downloadable_files( $variation, $data['downloads'] );
|
||||
}
|
||||
|
||||
// Download limit.
|
||||
if ( isset( $data['download_limit'] ) ) {
|
||||
$variation->set_download_limit( $data['download_limit'] );
|
||||
}
|
||||
|
||||
// Download expiry.
|
||||
if ( isset( $data['download_expiry'] ) ) {
|
||||
$variation->set_download_expiry( $data['download_expiry'] );
|
||||
}
|
||||
}
|
||||
|
||||
// Shipping data.
|
||||
$variation = $this->save_product_shipping_data( $variation, $data );
|
||||
|
||||
// Stock handling.
|
||||
if ( isset( $data['manage_stock'] ) ) {
|
||||
$variation->set_manage_stock( $data['manage_stock'] );
|
||||
}
|
||||
|
||||
if ( isset( $data['in_stock'] ) ) {
|
||||
$variation->set_stock_status( true === $data['in_stock'] ? 'instock' : 'outofstock' );
|
||||
}
|
||||
|
||||
if ( isset( $data['backorders'] ) ) {
|
||||
$variation->set_backorders( $data['backorders'] );
|
||||
}
|
||||
|
||||
if ( $variation->get_manage_stock() ) {
|
||||
if ( isset( $data['stock_quantity'] ) ) {
|
||||
$variation->set_stock_quantity( $data['stock_quantity'] );
|
||||
} elseif ( isset( $data['inventory_delta'] ) ) {
|
||||
$stock_quantity = wc_stock_amount( $variation->get_stock_quantity() );
|
||||
$stock_quantity += wc_stock_amount( $data['inventory_delta'] );
|
||||
$variation->set_stock_quantity( $stock_quantity );
|
||||
}
|
||||
} else {
|
||||
$variation->set_backorders( 'no' );
|
||||
$variation->set_stock_quantity( '' );
|
||||
}
|
||||
|
||||
// Regular Price.
|
||||
if ( isset( $data['regular_price'] ) ) {
|
||||
$variation->set_regular_price( $data['regular_price'] );
|
||||
}
|
||||
|
||||
// Sale Price.
|
||||
if ( isset( $data['sale_price'] ) ) {
|
||||
$variation->set_sale_price( $data['sale_price'] );
|
||||
}
|
||||
|
||||
if ( isset( $data['date_on_sale_from'] ) ) {
|
||||
$variation->set_date_on_sale_from( $data['date_on_sale_from'] );
|
||||
}
|
||||
|
||||
if ( isset( $data['date_on_sale_to'] ) ) {
|
||||
$variation->set_date_on_sale_to( $data['date_on_sale_to'] );
|
||||
}
|
||||
|
||||
// Tax class.
|
||||
if ( isset( $data['tax_class'] ) ) {
|
||||
$variation->set_tax_class( $data['tax_class'] );
|
||||
}
|
||||
|
||||
// Description.
|
||||
if ( isset( $data['description'] ) ) {
|
||||
$variation->set_description( wp_kses_post( $data['description'] ) );
|
||||
}
|
||||
|
||||
// Update taxonomies.
|
||||
if ( isset( $data['attributes'] ) ) {
|
||||
$attributes = array();
|
||||
$parent_attributes = $product->get_attributes();
|
||||
|
||||
foreach ( $data['attributes'] as $attribute ) {
|
||||
$attribute_id = 0;
|
||||
$attribute_name = '';
|
||||
|
||||
// Check ID for global attributes or name for product attributes.
|
||||
if ( ! empty( $attribute['id'] ) ) {
|
||||
$attribute_id = absint( $attribute['id'] );
|
||||
$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
|
||||
} elseif ( ! empty( $attribute['name'] ) ) {
|
||||
$attribute_name = sanitize_title( $attribute['name'] );
|
||||
}
|
||||
|
||||
if ( ! $attribute_id && ! $attribute_name ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() );
|
||||
$attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';
|
||||
|
||||
if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) {
|
||||
// If dealing with a taxonomy, we need to get the slug from the name posted to the API.
|
||||
$term = get_term_by( 'name', $attribute_value, $attribute_name );
|
||||
|
||||
if ( $term && ! is_wp_error( $term ) ) {
|
||||
$attribute_value = $term->slug;
|
||||
} else {
|
||||
$attribute_value = sanitize_title( $attribute_value );
|
||||
}
|
||||
}
|
||||
|
||||
$attributes[ $attribute_key ] = $attribute_value;
|
||||
}
|
||||
|
||||
$variation->set_attributes( $attributes );
|
||||
}
|
||||
|
||||
$variation->save();
|
||||
|
||||
do_action( 'woocommerce_rest_save_product_variation', $variation->get_id(), $menu_order, $data );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add post meta fields.
|
||||
*
|
||||
* @deprecated 3.0.0
|
||||
*
|
||||
* @param WP_Post $post Post data.
|
||||
* @param WP_REST_Request $request Request data.
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
protected function add_post_meta_fields( $post, $request ) {
|
||||
return $this->update_post_meta_fields( $post, $request );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update post meta fields.
|
||||
*
|
||||
* @param WP_Post $post Post data.
|
||||
* @param WP_REST_Request $request Request data.
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
protected function update_post_meta_fields( $post, $request ) {
|
||||
$product = wc_get_product( $post );
|
||||
|
||||
// Check for featured/gallery images, upload it and set it.
|
||||
if ( isset( $request['images'] ) ) {
|
||||
$product = $this->set_product_images( $product, $request['images'] );
|
||||
}
|
||||
|
||||
// Save product meta fields.
|
||||
$product = $this->set_product_meta( $product, $request );
|
||||
|
||||
// Save the product data.
|
||||
$product->save();
|
||||
|
||||
// Save variations.
|
||||
if ( $product->is_type( 'variable' ) ) {
|
||||
if ( isset( $request['variations'] ) && is_array( $request['variations'] ) ) {
|
||||
$this->save_variations_data( $product, $request );
|
||||
}
|
||||
}
|
||||
|
||||
// Clear caches here so in sync with any new variations/children.
|
||||
wc_delete_product_transients( $product->get_id() );
|
||||
wp_cache_delete( 'product-' . $product->get_id(), 'products' );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete post.
|
||||
*
|
||||
* @deprecated 3.0.0
|
||||
*
|
||||
* @param int|WP_Post $id Post ID or WP_Post instance.
|
||||
*/
|
||||
protected function delete_post( $id ) {
|
||||
if ( ! empty( $id->ID ) ) {
|
||||
$id = $id->ID;
|
||||
} elseif ( ! is_numeric( $id ) || 0 >= $id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete product attachments.
|
||||
$attachments = get_posts( array(
|
||||
'post_parent' => $id,
|
||||
'post_status' => 'any',
|
||||
'post_type' => 'attachment',
|
||||
) );
|
||||
|
||||
foreach ( (array) $attachments as $attachment ) {
|
||||
wp_delete_attachment( $attachment->ID, true );
|
||||
}
|
||||
|
||||
// Delete product.
|
||||
$product = wc_get_product( $id );
|
||||
$product->delete( true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get post types.
|
||||
*
|
||||
* @deprecated 3.0.0
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_post_types() {
|
||||
return array( 'product', 'product_variation' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save product images.
|
||||
*
|
||||
* @deprecated 3.0.0
|
||||
*
|
||||
* @param int $product_id
|
||||
* @param array $images
|
||||
* @throws WC_REST_Exception
|
||||
*/
|
||||
protected function save_product_images( $product_id, $images ) {
|
||||
$product = wc_get_product( $product_id );
|
||||
|
||||
return set_product_images( $product, $images );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,410 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API Authentication Class
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1.0
|
||||
* @version 2.4.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_Authentication {
|
||||
|
||||
/**
|
||||
* Setup class
|
||||
*
|
||||
* @since 2.1
|
||||
*/
|
||||
public function __construct() {
|
||||
|
||||
// To disable authentication, hook into this filter at a later priority and return a valid WP_User
|
||||
add_filter( 'woocommerce_api_check_authentication', array( $this, 'authenticate' ), 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the request. The authentication method varies based on whether the request was made over SSL or not.
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WP_User $user
|
||||
* @return null|WP_Error|WP_User
|
||||
*/
|
||||
public function authenticate( $user ) {
|
||||
|
||||
// Allow access to the index by default
|
||||
if ( '/' === WC()->api->server->path ) {
|
||||
return new WP_User( 0 );
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
if ( is_ssl() ) {
|
||||
$keys = $this->perform_ssl_authentication();
|
||||
} else {
|
||||
$keys = $this->perform_oauth_authentication();
|
||||
}
|
||||
|
||||
// Check API key-specific permission
|
||||
$this->check_api_key_permissions( $keys['permissions'] );
|
||||
|
||||
$user = $this->get_user_by_id( $keys['user_id'] );
|
||||
|
||||
$this->update_api_key_last_access( $keys['key_id'] );
|
||||
|
||||
} catch ( Exception $e ) {
|
||||
$user = new WP_Error( 'woocommerce_api_authentication_error', $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* SSL-encrypted requests are not subject to sniffing or man-in-the-middle
|
||||
* attacks, so the request can be authenticated by simply looking up the user
|
||||
* associated with the given consumer key and confirming the consumer secret
|
||||
* provided is valid
|
||||
*
|
||||
* @since 2.1
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
private function perform_ssl_authentication() {
|
||||
|
||||
$params = WC()->api->server->params['GET'];
|
||||
|
||||
// Get consumer key
|
||||
if ( ! empty( $_SERVER['PHP_AUTH_USER'] ) ) {
|
||||
|
||||
// Should be in HTTP Auth header by default
|
||||
$consumer_key = $_SERVER['PHP_AUTH_USER'];
|
||||
|
||||
} elseif ( ! empty( $params['consumer_key'] ) ) {
|
||||
|
||||
// Allow a query string parameter as a fallback
|
||||
$consumer_key = $params['consumer_key'];
|
||||
|
||||
} else {
|
||||
|
||||
throw new Exception( __( 'Consumer key is missing.', 'woocommerce' ), 404 );
|
||||
}
|
||||
|
||||
// Get consumer secret
|
||||
if ( ! empty( $_SERVER['PHP_AUTH_PW'] ) ) {
|
||||
|
||||
// Should be in HTTP Auth header by default
|
||||
$consumer_secret = $_SERVER['PHP_AUTH_PW'];
|
||||
|
||||
} elseif ( ! empty( $params['consumer_secret'] ) ) {
|
||||
|
||||
// Allow a query string parameter as a fallback
|
||||
$consumer_secret = $params['consumer_secret'];
|
||||
|
||||
} else {
|
||||
|
||||
throw new Exception( __( 'Consumer secret is missing.', 'woocommerce' ), 404 );
|
||||
}
|
||||
|
||||
$keys = $this->get_keys_by_consumer_key( $consumer_key );
|
||||
|
||||
if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $consumer_secret ) ) {
|
||||
throw new Exception( __( 'Consumer secret is invalid.', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests
|
||||
*
|
||||
* This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP
|
||||
*
|
||||
* This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions:
|
||||
*
|
||||
* 1) There is no token associated with request/responses, only consumer keys/secrets are used
|
||||
*
|
||||
* 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header,
|
||||
* This is because there is no cross-OS function within PHP to get the raw Authorization header
|
||||
*
|
||||
* @link http://tools.ietf.org/html/rfc5849 for the full spec
|
||||
* @since 2.1
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
private function perform_oauth_authentication() {
|
||||
|
||||
$params = WC()->api->server->params['GET'];
|
||||
|
||||
$param_names = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' );
|
||||
|
||||
// Check for required OAuth parameters
|
||||
foreach ( $param_names as $param_name ) {
|
||||
|
||||
if ( empty( $params[ $param_name ] ) ) {
|
||||
/* translators: %s: parameter name */
|
||||
throw new Exception( sprintf( __( '%s parameter is missing', 'woocommerce' ), $param_name ), 404 );
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch WP user by consumer key
|
||||
$keys = $this->get_keys_by_consumer_key( $params['oauth_consumer_key'] );
|
||||
|
||||
// Perform OAuth validation
|
||||
$this->check_oauth_signature( $keys, $params );
|
||||
$this->check_oauth_timestamp_and_nonce( $keys, $params['oauth_timestamp'], $params['oauth_nonce'] );
|
||||
|
||||
// Authentication successful, return user
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the keys for the given consumer key
|
||||
*
|
||||
* @since 2.4.0
|
||||
* @param string $consumer_key
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
private function get_keys_by_consumer_key( $consumer_key ) {
|
||||
global $wpdb;
|
||||
|
||||
$consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) );
|
||||
|
||||
$keys = $wpdb->get_row( $wpdb->prepare( "
|
||||
SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces
|
||||
FROM {$wpdb->prefix}woocommerce_api_keys
|
||||
WHERE consumer_key = '%s'
|
||||
", $consumer_key ), ARRAY_A );
|
||||
|
||||
if ( empty( $keys ) ) {
|
||||
throw new Exception( __( 'Consumer key is invalid.', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user by ID
|
||||
*
|
||||
* @since 2.4.0
|
||||
* @param int $user_id
|
||||
* @return WP_User
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function get_user_by_id( $user_id ) {
|
||||
$user = get_user_by( 'id', $user_id );
|
||||
|
||||
if ( ! $user ) {
|
||||
throw new Exception( __( 'API user is invalid', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the consumer secret provided for the given user is valid
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $keys_consumer_secret
|
||||
* @param string $consumer_secret
|
||||
* @return bool
|
||||
*/
|
||||
private function is_consumer_secret_valid( $keys_consumer_secret, $consumer_secret ) {
|
||||
return hash_equals( $keys_consumer_secret, $consumer_secret );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the consumer-provided request signature matches our generated signature, this ensures the consumer
|
||||
* has a valid key/secret
|
||||
*
|
||||
* @param array $keys
|
||||
* @param array $params the request parameters
|
||||
* @throws Exception
|
||||
*/
|
||||
private function check_oauth_signature( $keys, $params ) {
|
||||
|
||||
$http_method = strtoupper( WC()->api->server->method );
|
||||
|
||||
$base_request_uri = rawurlencode( untrailingslashit( get_woocommerce_api_url( '' ) ) . WC()->api->server->path );
|
||||
|
||||
// Get the signature provided by the consumer and remove it from the parameters prior to checking the signature
|
||||
$consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) );
|
||||
unset( $params['oauth_signature'] );
|
||||
|
||||
// Remove filters and convert them from array to strings to void normalize issues
|
||||
if ( isset( $params['filter'] ) ) {
|
||||
$filters = $params['filter'];
|
||||
unset( $params['filter'] );
|
||||
foreach ( $filters as $filter => $filter_value ) {
|
||||
$params[ 'filter[' . $filter . ']' ] = $filter_value;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize parameter key/values
|
||||
$params = $this->normalize_parameters( $params );
|
||||
|
||||
// Sort parameters
|
||||
if ( ! uksort( $params, 'strcmp' ) ) {
|
||||
throw new Exception( __( 'Invalid signature - failed to sort parameters.', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
// Form query string
|
||||
$query_params = array();
|
||||
foreach ( $params as $param_key => $param_value ) {
|
||||
|
||||
$query_params[] = $param_key . '%3D' . $param_value; // join with equals sign
|
||||
}
|
||||
$query_string = implode( '%26', $query_params ); // join with ampersand
|
||||
|
||||
$string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string;
|
||||
|
||||
if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) {
|
||||
throw new Exception( __( 'Invalid signature - signature method is invalid.', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) );
|
||||
|
||||
$signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $keys['consumer_secret'], true ) );
|
||||
|
||||
if ( ! hash_equals( $signature, $consumer_signature ) ) {
|
||||
throw new Exception( __( 'Invalid signature - provided signature does not match.', 'woocommerce' ), 401 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize each parameter by assuming each parameter may have already been
|
||||
* encoded, so attempt to decode, and then re-encode according to RFC 3986
|
||||
*
|
||||
* Note both the key and value is normalized so a filter param like:
|
||||
*
|
||||
* 'filter[period]' => 'week'
|
||||
*
|
||||
* is encoded to:
|
||||
*
|
||||
* 'filter%5Bperiod%5D' => 'week'
|
||||
*
|
||||
* This conforms to the OAuth 1.0a spec which indicates the entire query string
|
||||
* should be URL encoded
|
||||
*
|
||||
* @since 2.1
|
||||
* @see rawurlencode()
|
||||
* @param array $parameters un-normalized parameters
|
||||
* @return array normalized parameters
|
||||
*/
|
||||
private function normalize_parameters( $parameters ) {
|
||||
|
||||
$normalized_parameters = array();
|
||||
|
||||
foreach ( $parameters as $key => $value ) {
|
||||
|
||||
// Percent symbols (%) must be double-encoded
|
||||
$key = str_replace( '%', '%25', rawurlencode( rawurldecode( $key ) ) );
|
||||
$value = str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) );
|
||||
|
||||
$normalized_parameters[ $key ] = $value;
|
||||
}
|
||||
|
||||
return $normalized_parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where
|
||||
* an attacker could attempt to re-send an intercepted request at a later time.
|
||||
*
|
||||
* - A timestamp is valid if it is within 15 minutes of now
|
||||
* - A nonce is valid if it has not been used within the last 15 minutes
|
||||
*
|
||||
* @param array $keys
|
||||
* @param int $timestamp the unix timestamp for when the request was made
|
||||
* @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated
|
||||
* @throws Exception
|
||||
*/
|
||||
private function check_oauth_timestamp_and_nonce( $keys, $timestamp, $nonce ) {
|
||||
global $wpdb;
|
||||
|
||||
$valid_window = 15 * 60; // 15 minute window
|
||||
|
||||
if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) {
|
||||
throw new Exception( __( 'Invalid timestamp.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
$used_nonces = maybe_unserialize( $keys['nonces'] );
|
||||
|
||||
if ( empty( $used_nonces ) ) {
|
||||
$used_nonces = array();
|
||||
}
|
||||
|
||||
if ( in_array( $nonce, $used_nonces ) ) {
|
||||
throw new Exception( __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$used_nonces[ $timestamp ] = $nonce;
|
||||
|
||||
// Remove expired nonces
|
||||
foreach ( $used_nonces as $nonce_timestamp => $nonce ) {
|
||||
if ( $nonce_timestamp < ( time() - $valid_window ) ) {
|
||||
unset( $used_nonces[ $nonce_timestamp ] );
|
||||
}
|
||||
}
|
||||
|
||||
$used_nonces = maybe_serialize( $used_nonces );
|
||||
|
||||
$wpdb->update(
|
||||
$wpdb->prefix . 'woocommerce_api_keys',
|
||||
array( 'nonces' => $used_nonces ),
|
||||
array( 'key_id' => $keys['key_id'] ),
|
||||
array( '%s' ),
|
||||
array( '%d' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the API keys provided have the proper key-specific permissions to either read or write API resources
|
||||
*
|
||||
* @param string $key_permissions
|
||||
* @throws Exception if the permission check fails
|
||||
*/
|
||||
public function check_api_key_permissions( $key_permissions ) {
|
||||
switch ( WC()->api->server->method ) {
|
||||
|
||||
case 'HEAD':
|
||||
case 'GET':
|
||||
if ( 'read' !== $key_permissions && 'read_write' !== $key_permissions ) {
|
||||
throw new Exception( __( 'The API key provided does not have read permissions.', 'woocommerce' ), 401 );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
case 'PUT':
|
||||
case 'PATCH':
|
||||
case 'DELETE':
|
||||
if ( 'write' !== $key_permissions && 'read_write' !== $key_permissions ) {
|
||||
throw new Exception( __( 'The API key provided does not have write permissions.', 'woocommerce' ), 401 );
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updated API Key last access datetime
|
||||
*
|
||||
* @since 2.4.0
|
||||
*
|
||||
* @param int $key_id
|
||||
*/
|
||||
private function update_api_key_last_access( $key_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->update(
|
||||
$wpdb->prefix . 'woocommerce_api_keys',
|
||||
array( 'last_access' => current_time( 'mysql' ) ),
|
||||
array( 'key_id' => $key_id ),
|
||||
array( '%s' ),
|
||||
array( '%d' )
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,247 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API Coupons Class
|
||||
*
|
||||
* Handles requests to the /coupons endpoint
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1
|
||||
* @version 2.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_Coupons extends WC_API_Resource {
|
||||
|
||||
/** @var string $base the route base */
|
||||
protected $base = '/coupons';
|
||||
|
||||
/**
|
||||
* Register the routes for this class
|
||||
*
|
||||
* GET /coupons
|
||||
* GET /coupons/count
|
||||
* GET /coupons/<id>
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $routes
|
||||
* @return array
|
||||
*/
|
||||
public function register_routes( $routes ) {
|
||||
|
||||
# GET /coupons
|
||||
$routes[ $this->base ] = array(
|
||||
array( array( $this, 'get_coupons' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# GET /coupons/count
|
||||
$routes[ $this->base . '/count' ] = array(
|
||||
array( array( $this, 'get_coupons_count' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# GET /coupons/<id>
|
||||
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
|
||||
array( array( $this, 'get_coupon' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# GET /coupons/code/<code>, note that coupon codes can contain spaces, dashes and underscores
|
||||
$routes[ $this->base . '/code/(?P<code>\w[\w\s\-]*)' ] = array(
|
||||
array( array( $this, 'get_coupon_by_code' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all coupons
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $fields
|
||||
* @param array $filter
|
||||
* @param int $page
|
||||
* @return array
|
||||
*/
|
||||
public function get_coupons( $fields = null, $filter = array(), $page = 1 ) {
|
||||
|
||||
$filter['page'] = $page;
|
||||
|
||||
$query = $this->query_coupons( $filter );
|
||||
|
||||
$coupons = array();
|
||||
|
||||
foreach ( $query->posts as $coupon_id ) {
|
||||
|
||||
if ( ! $this->is_readable( $coupon_id ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$coupons[] = current( $this->get_coupon( $coupon_id, $fields ) );
|
||||
}
|
||||
|
||||
$this->server->add_pagination_headers( $query );
|
||||
|
||||
return array( 'coupons' => $coupons );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the coupon for the given ID
|
||||
*
|
||||
* @since 2.1
|
||||
*
|
||||
* @param int $id the coupon ID
|
||||
* @param string $fields fields to include in response
|
||||
*
|
||||
* @return array|WP_Error
|
||||
* @throws WC_API_Exception
|
||||
*/
|
||||
public function get_coupon( $id, $fields = null ) {
|
||||
$id = $this->validate_request( $id, 'shop_coupon', 'read' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$coupon = new WC_Coupon( $id );
|
||||
|
||||
if ( 0 === $coupon->get_id() ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_id', __( 'Invalid coupon ID', 'woocommerce' ), 404 );
|
||||
}
|
||||
|
||||
$coupon_data = array(
|
||||
'id' => $coupon->get_id(),
|
||||
'code' => $coupon->get_code(),
|
||||
'type' => $coupon->get_discount_type(),
|
||||
'created_at' => $this->server->format_datetime( $coupon->get_date_created() ? $coupon->get_date_created()->getTimestamp() : 0 ), // API gives UTC times.
|
||||
'updated_at' => $this->server->format_datetime( $coupon->get_date_modified() ? $coupon->get_date_modified()->getTimestamp() : 0 ), // API gives UTC times.
|
||||
'amount' => wc_format_decimal( $coupon->get_amount(), 2 ),
|
||||
'individual_use' => $coupon->get_individual_use(),
|
||||
'product_ids' => array_map( 'absint', (array) $coupon->get_product_ids() ),
|
||||
'exclude_product_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_ids() ),
|
||||
'usage_limit' => $coupon->get_usage_limit() ? $coupon->get_usage_limit() : null,
|
||||
'usage_limit_per_user' => $coupon->get_usage_limit_per_user() ? $coupon->get_usage_limit_per_user() : null,
|
||||
'limit_usage_to_x_items' => (int) $coupon->get_limit_usage_to_x_items(),
|
||||
'usage_count' => (int) $coupon->get_usage_count(),
|
||||
'expiry_date' => $this->server->format_datetime( $coupon->get_date_expires() ? $coupon->get_date_expires()->getTimestamp() : 0 ), // API gives UTC times.
|
||||
'enable_free_shipping' => $coupon->get_free_shipping(),
|
||||
'product_category_ids' => array_map( 'absint', (array) $coupon->get_product_categories() ),
|
||||
'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_categories() ),
|
||||
'exclude_sale_items' => $coupon->get_exclude_sale_items(),
|
||||
'minimum_amount' => wc_format_decimal( $coupon->get_minimum_amount(), 2 ),
|
||||
'customer_emails' => $coupon->get_email_restrictions(),
|
||||
);
|
||||
|
||||
return array( 'coupon' => apply_filters( 'woocommerce_api_coupon_response', $coupon_data, $coupon, $fields, $this->server ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of coupons
|
||||
*
|
||||
* @since 2.1
|
||||
*
|
||||
* @param array $filter
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_coupons_count( $filter = array() ) {
|
||||
|
||||
$query = $this->query_coupons( $filter );
|
||||
|
||||
if ( ! current_user_can( 'read_private_shop_coupons' ) ) {
|
||||
return new WP_Error( 'woocommerce_api_user_cannot_read_coupons_count', __( 'You do not have permission to read the coupons count', 'woocommerce' ), array( 'status' => 401 ) );
|
||||
}
|
||||
|
||||
return array( 'count' => (int) $query->found_posts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the coupon for the given code
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $code the coupon code
|
||||
* @param string $fields fields to include in response
|
||||
* @return int|WP_Error
|
||||
*/
|
||||
public function get_coupon_by_code( $code, $fields = null ) {
|
||||
global $wpdb;
|
||||
|
||||
$id = $wpdb->get_var( $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 LIMIT 1;", $code ) );
|
||||
|
||||
if ( is_null( $id ) ) {
|
||||
return new WP_Error( 'woocommerce_api_invalid_coupon_code', __( 'Invalid coupon code', 'woocommerce' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
return $this->get_coupon( $id, $fields );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a coupon
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function create_coupon( $data ) {
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a coupon
|
||||
*
|
||||
* @param int $id the coupon ID
|
||||
* @param array $data
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function edit_coupon( $id, $data ) {
|
||||
|
||||
$id = $this->validate_request( $id, 'shop_coupon', 'edit' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
return $this->get_coupon( $id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a coupon
|
||||
*
|
||||
* @param int $id the coupon ID
|
||||
* @param bool $force true to permanently delete coupon, false to move to trash
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function delete_coupon( $id, $force = false ) {
|
||||
|
||||
$id = $this->validate_request( $id, 'shop_coupon', 'delete' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
return $this->delete( $id, 'shop_coupon', ( 'true' === $force ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get coupon post objects
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $args request arguments for filtering query
|
||||
* @return WP_Query
|
||||
*/
|
||||
private function query_coupons( $args ) {
|
||||
|
||||
// set base query arguments
|
||||
$query_args = array(
|
||||
'fields' => 'ids',
|
||||
'post_type' => 'shop_coupon',
|
||||
'post_status' => 'publish',
|
||||
);
|
||||
|
||||
$query_args = $this->merge_query_args( $query_args, $args );
|
||||
|
||||
return new WP_Query( $query_args );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,481 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API Customers Class
|
||||
*
|
||||
* Handles requests to the /customers endpoint
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1
|
||||
* @version 2.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_Customers extends WC_API_Resource {
|
||||
|
||||
/** @var string $base the route base */
|
||||
protected $base = '/customers';
|
||||
|
||||
/** @var string $created_at_min for date filtering */
|
||||
private $created_at_min = null;
|
||||
|
||||
/** @var string $created_at_max for date filtering */
|
||||
private $created_at_max = null;
|
||||
|
||||
/**
|
||||
* Setup class, overridden to provide customer data to order response
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WC_API_Server $server
|
||||
*/
|
||||
public function __construct( WC_API_Server $server ) {
|
||||
|
||||
parent::__construct( $server );
|
||||
|
||||
// add customer data to order responses
|
||||
add_filter( 'woocommerce_api_order_response', array( $this, 'add_customer_data' ), 10, 2 );
|
||||
|
||||
// modify WP_User_Query to support created_at date filtering
|
||||
add_action( 'pre_user_query', array( $this, 'modify_user_query' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the routes for this class
|
||||
*
|
||||
* GET /customers
|
||||
* GET /customers/count
|
||||
* GET /customers/<id>
|
||||
* GET /customers/<id>/orders
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $routes
|
||||
* @return array
|
||||
*/
|
||||
public function register_routes( $routes ) {
|
||||
|
||||
# GET /customers
|
||||
$routes[ $this->base ] = array(
|
||||
array( array( $this, 'get_customers' ), WC_API_SERVER::READABLE ),
|
||||
);
|
||||
|
||||
# GET /customers/count
|
||||
$routes[ $this->base . '/count' ] = array(
|
||||
array( array( $this, 'get_customers_count' ), WC_API_SERVER::READABLE ),
|
||||
);
|
||||
|
||||
# GET /customers/<id>
|
||||
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
|
||||
array( array( $this, 'get_customer' ), WC_API_SERVER::READABLE ),
|
||||
);
|
||||
|
||||
# GET /customers/<id>/orders
|
||||
$routes[ $this->base . '/(?P<id>\d+)/orders' ] = array(
|
||||
array( array( $this, 'get_customer_orders' ), WC_API_SERVER::READABLE ),
|
||||
);
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all customers
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $fields
|
||||
* @param array $filter
|
||||
* @param int $page
|
||||
* @return array
|
||||
*/
|
||||
public function get_customers( $fields = null, $filter = array(), $page = 1 ) {
|
||||
|
||||
$filter['page'] = $page;
|
||||
|
||||
$query = $this->query_customers( $filter );
|
||||
|
||||
$customers = array();
|
||||
|
||||
foreach ( $query->get_results() as $user_id ) {
|
||||
|
||||
if ( ! $this->is_readable( $user_id ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$customers[] = current( $this->get_customer( $user_id, $fields ) );
|
||||
}
|
||||
|
||||
$this->server->add_pagination_headers( $query );
|
||||
|
||||
return array( 'customers' => $customers );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the customer for the given ID
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int $id the customer ID
|
||||
* @param string $fields
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_customer( $id, $fields = null ) {
|
||||
global $wpdb;
|
||||
|
||||
$id = $this->validate_request( $id, 'customer', 'read' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$customer = new WC_Customer( $id );
|
||||
$last_order = $customer->get_last_order();
|
||||
$customer_data = array(
|
||||
'id' => $customer->get_id(),
|
||||
'created_at' => $this->server->format_datetime( $customer->get_date_created() ? $customer->get_date_created()->getTimestamp() : 0 ), // API gives UTC times.
|
||||
'email' => $customer->get_email(),
|
||||
'first_name' => $customer->get_first_name(),
|
||||
'last_name' => $customer->get_last_name(),
|
||||
'username' => $customer->get_username(),
|
||||
'last_order_id' => is_object( $last_order ) ? $last_order->get_id() : null,
|
||||
'last_order_date' => is_object( $last_order ) ? $this->server->format_datetime( $last_order->get_date_created() ? $last_order->get_date_created()->getTimestamp() : 0 ) : null, // API gives UTC times.
|
||||
'orders_count' => $customer->get_order_count(),
|
||||
'total_spent' => wc_format_decimal( $customer->get_total_spent(), 2 ),
|
||||
'avatar_url' => $customer->get_avatar_url(),
|
||||
'billing_address' => array(
|
||||
'first_name' => $customer->get_billing_first_name(),
|
||||
'last_name' => $customer->get_billing_last_name(),
|
||||
'company' => $customer->get_billing_company(),
|
||||
'address_1' => $customer->get_billing_address_1(),
|
||||
'address_2' => $customer->get_billing_address_2(),
|
||||
'city' => $customer->get_billing_city(),
|
||||
'state' => $customer->get_billing_state(),
|
||||
'postcode' => $customer->get_billing_postcode(),
|
||||
'country' => $customer->get_billing_country(),
|
||||
'email' => $customer->get_billing_email(),
|
||||
'phone' => $customer->get_billing_phone(),
|
||||
),
|
||||
'shipping_address' => array(
|
||||
'first_name' => $customer->get_shipping_first_name(),
|
||||
'last_name' => $customer->get_shipping_last_name(),
|
||||
'company' => $customer->get_shipping_company(),
|
||||
'address_1' => $customer->get_shipping_address_1(),
|
||||
'address_2' => $customer->get_shipping_address_2(),
|
||||
'city' => $customer->get_shipping_city(),
|
||||
'state' => $customer->get_shipping_state(),
|
||||
'postcode' => $customer->get_shipping_postcode(),
|
||||
'country' => $customer->get_shipping_country(),
|
||||
),
|
||||
);
|
||||
|
||||
return array( 'customer' => apply_filters( 'woocommerce_api_customer_response', $customer_data, $customer, $fields, $this->server ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of customers
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $filter
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_customers_count( $filter = array() ) {
|
||||
|
||||
$query = $this->query_customers( $filter );
|
||||
|
||||
if ( ! current_user_can( 'list_users' ) ) {
|
||||
return new WP_Error( 'woocommerce_api_user_cannot_read_customers_count', __( 'You do not have permission to read the customers count', 'woocommerce' ), array( 'status' => 401 ) );
|
||||
}
|
||||
|
||||
return array( 'count' => count( $query->get_results() ) );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a customer
|
||||
*
|
||||
* @param array $data
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function create_customer( $data ) {
|
||||
|
||||
if ( ! current_user_can( 'create_users' ) ) {
|
||||
return new WP_Error( 'woocommerce_api_user_cannot_create_customer', __( 'You do not have permission to create this customer', 'woocommerce' ), array( 'status' => 401 ) );
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a customer
|
||||
*
|
||||
* @param int $id the customer ID
|
||||
* @param array $data
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function edit_customer( $id, $data ) {
|
||||
|
||||
$id = $this->validate_request( $id, 'customer', 'edit' );
|
||||
|
||||
if ( ! is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
return $this->get_customer( $id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a customer
|
||||
*
|
||||
* @param int $id the customer ID
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function delete_customer( $id ) {
|
||||
|
||||
$id = $this->validate_request( $id, 'customer', 'delete' );
|
||||
|
||||
if ( ! is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
return $this->delete( $id, 'customer' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the orders for a customer
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int $id the customer ID
|
||||
* @param string $fields fields to include in response
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_customer_orders( $id, $fields = null ) {
|
||||
global $wpdb;
|
||||
|
||||
$id = $this->validate_request( $id, 'customer', 'read' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$order_ids = wc_get_orders( array(
|
||||
'customer' => $id,
|
||||
'limit' => -1,
|
||||
'orderby' => 'date',
|
||||
'order' => 'ASC',
|
||||
'return' => 'ids',
|
||||
) );
|
||||
|
||||
if ( empty( $order_ids ) ) {
|
||||
return array( 'orders' => array() );
|
||||
}
|
||||
|
||||
$orders = array();
|
||||
|
||||
foreach ( $order_ids as $order_id ) {
|
||||
$orders[] = current( WC()->api->WC_API_Orders->get_order( $order_id, $fields ) );
|
||||
}
|
||||
|
||||
return array( 'orders' => apply_filters( 'woocommerce_api_customer_orders_response', $orders, $id, $fields, $order_ids, $this->server ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get customer user objects
|
||||
*
|
||||
* Note that WP_User_Query does not have built-in pagination so limit & offset are used to provide limited
|
||||
* pagination support
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $args request arguments for filtering query
|
||||
* @return WP_User_Query
|
||||
*/
|
||||
private function query_customers( $args = array() ) {
|
||||
|
||||
// default users per page
|
||||
$users_per_page = get_option( 'posts_per_page' );
|
||||
|
||||
// set base query arguments
|
||||
$query_args = array(
|
||||
'fields' => 'ID',
|
||||
'role' => 'customer',
|
||||
'orderby' => 'registered',
|
||||
'number' => $users_per_page,
|
||||
);
|
||||
|
||||
// search
|
||||
if ( ! empty( $args['q'] ) ) {
|
||||
$query_args['search'] = $args['q'];
|
||||
}
|
||||
|
||||
// limit number of users returned
|
||||
if ( ! empty( $args['limit'] ) ) {
|
||||
|
||||
$query_args['number'] = absint( $args['limit'] );
|
||||
|
||||
$users_per_page = absint( $args['limit'] );
|
||||
}
|
||||
|
||||
// page
|
||||
$page = ( isset( $args['page'] ) ) ? absint( $args['page'] ) : 1;
|
||||
|
||||
// offset
|
||||
if ( ! empty( $args['offset'] ) ) {
|
||||
$query_args['offset'] = absint( $args['offset'] );
|
||||
} else {
|
||||
$query_args['offset'] = $users_per_page * ( $page - 1 );
|
||||
}
|
||||
|
||||
// created date
|
||||
if ( ! empty( $args['created_at_min'] ) ) {
|
||||
$this->created_at_min = $this->server->parse_datetime( $args['created_at_min'] );
|
||||
}
|
||||
|
||||
if ( ! empty( $args['created_at_max'] ) ) {
|
||||
$this->created_at_max = $this->server->parse_datetime( $args['created_at_max'] );
|
||||
}
|
||||
|
||||
$query = new WP_User_Query( $query_args );
|
||||
|
||||
// helper members for pagination headers
|
||||
$query->total_pages = ceil( $query->get_total() / $users_per_page );
|
||||
$query->page = $page;
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add customer data to orders
|
||||
*
|
||||
* @since 2.1
|
||||
* @param $order_data
|
||||
* @param $order
|
||||
* @return array
|
||||
*/
|
||||
public function add_customer_data( $order_data, $order ) {
|
||||
|
||||
if ( 0 == $order->get_user_id() ) {
|
||||
|
||||
// add customer data from order
|
||||
$order_data['customer'] = array(
|
||||
'id' => 0,
|
||||
'email' => $order->get_billing_email(),
|
||||
'first_name' => $order->get_billing_first_name(),
|
||||
'last_name' => $order->get_billing_last_name(),
|
||||
'billing_address' => array(
|
||||
'first_name' => $order->get_billing_first_name(),
|
||||
'last_name' => $order->get_billing_last_name(),
|
||||
'company' => $order->get_billing_company(),
|
||||
'address_1' => $order->get_billing_address_1(),
|
||||
'address_2' => $order->get_billing_address_2(),
|
||||
'city' => $order->get_billing_city(),
|
||||
'state' => $order->get_billing_state(),
|
||||
'postcode' => $order->get_billing_postcode(),
|
||||
'country' => $order->get_billing_country(),
|
||||
'email' => $order->get_billing_email(),
|
||||
'phone' => $order->get_billing_phone(),
|
||||
),
|
||||
'shipping_address' => array(
|
||||
'first_name' => $order->get_shipping_first_name(),
|
||||
'last_name' => $order->get_shipping_last_name(),
|
||||
'company' => $order->get_shipping_company(),
|
||||
'address_1' => $order->get_shipping_address_1(),
|
||||
'address_2' => $order->get_shipping_address_2(),
|
||||
'city' => $order->get_shipping_city(),
|
||||
'state' => $order->get_shipping_state(),
|
||||
'postcode' => $order->get_shipping_postcode(),
|
||||
'country' => $order->get_shipping_country(),
|
||||
),
|
||||
);
|
||||
|
||||
} else {
|
||||
|
||||
$order_data['customer'] = current( $this->get_customer( $order->get_user_id() ) );
|
||||
}
|
||||
|
||||
return $order_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the WP_User_Query to support filtering on the date the customer was created
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WP_User_Query $query
|
||||
*/
|
||||
public function modify_user_query( $query ) {
|
||||
|
||||
if ( $this->created_at_min ) {
|
||||
$query->query_where .= sprintf( " AND user_registered >= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%h:%%i:%%s' )", esc_sql( $this->created_at_min ) );
|
||||
}
|
||||
|
||||
if ( $this->created_at_max ) {
|
||||
$query->query_where .= sprintf( " AND user_registered <= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%h:%%i:%%s' )", esc_sql( $this->created_at_max ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the request by checking:
|
||||
*
|
||||
* 1) the ID is a valid integer
|
||||
* 2) the ID returns a valid WP_User
|
||||
* 3) the current user has the proper permissions
|
||||
*
|
||||
* @since 2.1
|
||||
* @see WC_API_Resource::validate_request()
|
||||
* @param string|int $id the customer ID
|
||||
* @param string $type the request type, unused because this method overrides the parent class
|
||||
* @param string $context the context of the request, either `read`, `edit` or `delete`
|
||||
* @return int|WP_Error valid user ID or WP_Error if any of the checks fails
|
||||
*/
|
||||
protected function validate_request( $id, $type, $context ) {
|
||||
|
||||
$id = absint( $id );
|
||||
|
||||
// validate ID
|
||||
if ( empty( $id ) ) {
|
||||
return new WP_Error( 'woocommerce_api_invalid_customer_id', __( 'Invalid customer ID', 'woocommerce' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
// non-existent IDs return a valid WP_User object with the user ID = 0
|
||||
$customer = new WP_User( $id );
|
||||
|
||||
if ( 0 === $customer->ID ) {
|
||||
return new WP_Error( 'woocommerce_api_invalid_customer', __( 'Invalid customer', 'woocommerce' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
// validate permissions
|
||||
switch ( $context ) {
|
||||
|
||||
case 'read':
|
||||
if ( ! current_user_can( 'list_users' ) ) {
|
||||
return new WP_Error( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read this customer', 'woocommerce' ), array( 'status' => 401 ) );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'edit':
|
||||
if ( ! current_user_can( 'edit_users' ) ) {
|
||||
return new WP_Error( 'woocommerce_api_user_cannot_edit_customer', __( 'You do not have permission to edit this customer', 'woocommerce' ), array( 'status' => 401 ) );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
if ( ! current_user_can( 'delete_users' ) ) {
|
||||
return new WP_Error( 'woocommerce_api_user_cannot_delete_customer', __( 'You do not have permission to delete this customer', 'woocommerce' ), array( 'status' => 401 ) );
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user can read users
|
||||
*
|
||||
* @since 2.1
|
||||
* @see WC_API_Resource::is_readable()
|
||||
* @param int|WP_Post $post unused
|
||||
* @return bool true if the current user can read users, false otherwise
|
||||
*/
|
||||
protected function is_readable( $post ) {
|
||||
|
||||
return current_user_can( 'list_users' );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API
|
||||
*
|
||||
* Handles parsing JSON request bodies and generating JSON responses
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1
|
||||
* @version 2.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_JSON_Handler implements WC_API_Handler {
|
||||
|
||||
/**
|
||||
* Get the content type for the response
|
||||
*
|
||||
* @since 2.1
|
||||
* @return string
|
||||
*/
|
||||
public function get_content_type() {
|
||||
|
||||
return sprintf( '%s; charset=%s', isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json', get_option( 'blog_charset' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the raw request body entity
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $body the raw request body
|
||||
* @return array|mixed
|
||||
*/
|
||||
public function parse_body( $body ) {
|
||||
|
||||
return json_decode( $body, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a JSON response given an array of data
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $data the response data
|
||||
* @return string
|
||||
*/
|
||||
public function generate_response( $data ) {
|
||||
if ( isset( $_GET['_jsonp'] ) ) {
|
||||
|
||||
if ( ! apply_filters( 'woocommerce_api_jsonp_enabled', true ) ) {
|
||||
WC()->api->server->send_status( 400 );
|
||||
return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) ) );
|
||||
}
|
||||
|
||||
$jsonp_callback = $_GET['_jsonp'];
|
||||
|
||||
if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) {
|
||||
WC()->api->server->send_status( 400 );
|
||||
return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ) );
|
||||
}
|
||||
|
||||
WC()->api->server->header( 'X-Content-Type-Options', 'nosniff' );
|
||||
|
||||
// Prepend '/**/' to mitigate possible JSONP Flash attacks.
|
||||
// https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
|
||||
return '/**/' . $jsonp_callback . '(' . wp_json_encode( $data ) . ')';
|
||||
}
|
||||
|
||||
return wp_json_encode( $data );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,396 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API Orders Class
|
||||
*
|
||||
* Handles requests to the /orders endpoint
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1
|
||||
* @version 2.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_Orders extends WC_API_Resource {
|
||||
|
||||
/** @var string $base the route base */
|
||||
protected $base = '/orders';
|
||||
|
||||
/**
|
||||
* Register the routes for this class
|
||||
*
|
||||
* GET /orders
|
||||
* GET /orders/count
|
||||
* GET|PUT /orders/<id>
|
||||
* GET /orders/<id>/notes
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $routes
|
||||
* @return array
|
||||
*/
|
||||
public function register_routes( $routes ) {
|
||||
|
||||
# GET /orders
|
||||
$routes[ $this->base ] = array(
|
||||
array( array( $this, 'get_orders' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# GET /orders/count
|
||||
$routes[ $this->base . '/count' ] = array(
|
||||
array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# GET|PUT /orders/<id>
|
||||
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
|
||||
array( array( $this, 'get_order' ), WC_API_Server::READABLE ),
|
||||
array( array( $this, 'edit_order' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
|
||||
);
|
||||
|
||||
# GET /orders/<id>/notes
|
||||
$routes[ $this->base . '/(?P<id>\d+)/notes' ] = array(
|
||||
array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all orders
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $fields
|
||||
* @param array $filter
|
||||
* @param string $status
|
||||
* @param int $page
|
||||
* @return array
|
||||
*/
|
||||
public function get_orders( $fields = null, $filter = array(), $status = null, $page = 1 ) {
|
||||
|
||||
if ( ! empty( $status ) ) {
|
||||
$filter['status'] = $status;
|
||||
}
|
||||
|
||||
$filter['page'] = $page;
|
||||
|
||||
$query = $this->query_orders( $filter );
|
||||
|
||||
$orders = array();
|
||||
|
||||
foreach ( $query->posts as $order_id ) {
|
||||
|
||||
if ( ! $this->is_readable( $order_id ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$orders[] = current( $this->get_order( $order_id, $fields ) );
|
||||
}
|
||||
|
||||
$this->server->add_pagination_headers( $query );
|
||||
|
||||
return array( 'orders' => $orders );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the order for the given ID
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int $id the order ID
|
||||
* @param array $fields
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_order( $id, $fields = null ) {
|
||||
|
||||
// ensure order ID is valid & user has permission to read
|
||||
$id = $this->validate_request( $id, 'shop_order', 'read' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$order = wc_get_order( $id );
|
||||
$order_data = array(
|
||||
'id' => $order->get_id(),
|
||||
'order_number' => $order->get_order_number(),
|
||||
'created_at' => $this->server->format_datetime( $order->get_date_created() ? $order->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times.
|
||||
'updated_at' => $this->server->format_datetime( $order->get_date_modified() ? $order->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times.
|
||||
'completed_at' => $this->server->format_datetime( $order->get_date_completed() ? $order->get_date_completed()->getTimestamp() : 0, false, false ), // API gives UTC times.
|
||||
'status' => $order->get_status(),
|
||||
'currency' => $order->get_currency(),
|
||||
'total' => wc_format_decimal( $order->get_total(), 2 ),
|
||||
'subtotal' => wc_format_decimal( $this->get_order_subtotal( $order ), 2 ),
|
||||
'total_line_items_quantity' => $order->get_item_count(),
|
||||
'total_tax' => wc_format_decimal( $order->get_total_tax(), 2 ),
|
||||
'total_shipping' => wc_format_decimal( $order->get_shipping_total(), 2 ),
|
||||
'cart_tax' => wc_format_decimal( $order->get_cart_tax(), 2 ),
|
||||
'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), 2 ),
|
||||
'total_discount' => wc_format_decimal( $order->get_total_discount(), 2 ),
|
||||
'cart_discount' => wc_format_decimal( 0, 2 ),
|
||||
'order_discount' => wc_format_decimal( 0, 2 ),
|
||||
'shipping_methods' => $order->get_shipping_method(),
|
||||
'payment_details' => array(
|
||||
'method_id' => $order->get_payment_method(),
|
||||
'method_title' => $order->get_payment_method_title(),
|
||||
'paid' => ! is_null( $order->get_date_paid() ),
|
||||
),
|
||||
'billing_address' => array(
|
||||
'first_name' => $order->get_billing_first_name(),
|
||||
'last_name' => $order->get_billing_last_name(),
|
||||
'company' => $order->get_billing_company(),
|
||||
'address_1' => $order->get_billing_address_1(),
|
||||
'address_2' => $order->get_billing_address_2(),
|
||||
'city' => $order->get_billing_city(),
|
||||
'state' => $order->get_billing_state(),
|
||||
'postcode' => $order->get_billing_postcode(),
|
||||
'country' => $order->get_billing_country(),
|
||||
'email' => $order->get_billing_email(),
|
||||
'phone' => $order->get_billing_phone(),
|
||||
),
|
||||
'shipping_address' => array(
|
||||
'first_name' => $order->get_shipping_first_name(),
|
||||
'last_name' => $order->get_shipping_last_name(),
|
||||
'company' => $order->get_shipping_company(),
|
||||
'address_1' => $order->get_shipping_address_1(),
|
||||
'address_2' => $order->get_shipping_address_2(),
|
||||
'city' => $order->get_shipping_city(),
|
||||
'state' => $order->get_shipping_state(),
|
||||
'postcode' => $order->get_shipping_postcode(),
|
||||
'country' => $order->get_shipping_country(),
|
||||
),
|
||||
'note' => $order->get_customer_note(),
|
||||
'customer_ip' => $order->get_customer_ip_address(),
|
||||
'customer_user_agent' => $order->get_customer_user_agent(),
|
||||
'customer_id' => $order->get_user_id(),
|
||||
'view_order_url' => $order->get_view_order_url(),
|
||||
'line_items' => array(),
|
||||
'shipping_lines' => array(),
|
||||
'tax_lines' => array(),
|
||||
'fee_lines' => array(),
|
||||
'coupon_lines' => array(),
|
||||
);
|
||||
|
||||
// add line items
|
||||
foreach ( $order->get_items() as $item_id => $item ) {
|
||||
$product = $item->get_product();
|
||||
$order_data['line_items'][] = array(
|
||||
'id' => $item_id,
|
||||
'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ),
|
||||
'total' => wc_format_decimal( $order->get_line_total( $item ), 2 ),
|
||||
'total_tax' => wc_format_decimal( $order->get_line_tax( $item ), 2 ),
|
||||
'price' => wc_format_decimal( $order->get_item_total( $item ), 2 ),
|
||||
'quantity' => $item->get_quantity(),
|
||||
'tax_class' => $item->get_tax_class(),
|
||||
'name' => $item->get_name(),
|
||||
'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(),
|
||||
'sku' => is_object( $product ) ? $product->get_sku() : null,
|
||||
);
|
||||
}
|
||||
|
||||
// add shipping
|
||||
foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) {
|
||||
$order_data['shipping_lines'][] = array(
|
||||
'id' => $shipping_item_id,
|
||||
'method_id' => $shipping_item->get_method_id(),
|
||||
'method_title' => $shipping_item->get_name(),
|
||||
'total' => wc_format_decimal( $shipping_item->get_total(), 2 ),
|
||||
);
|
||||
}
|
||||
|
||||
// add taxes
|
||||
foreach ( $order->get_tax_totals() as $tax_code => $tax ) {
|
||||
$order_data['tax_lines'][] = array(
|
||||
'code' => $tax_code,
|
||||
'title' => $tax->label,
|
||||
'total' => wc_format_decimal( $tax->amount, 2 ),
|
||||
'compound' => (bool) $tax->is_compound,
|
||||
);
|
||||
}
|
||||
|
||||
// add fees
|
||||
foreach ( $order->get_fees() as $fee_item_id => $fee_item ) {
|
||||
$order_data['fee_lines'][] = array(
|
||||
'id' => $fee_item_id,
|
||||
'title' => $fee_item->get_name(),
|
||||
'tax_class' => $fee_item->get_tax_class(),
|
||||
'total' => wc_format_decimal( $order->get_line_total( $fee_item ), 2 ),
|
||||
'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), 2 ),
|
||||
);
|
||||
}
|
||||
|
||||
// add coupons
|
||||
foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) {
|
||||
$order_data['coupon_lines'][] = array(
|
||||
'id' => $coupon_item_id,
|
||||
'code' => $coupon_item->get_code(),
|
||||
'amount' => wc_format_decimal( $coupon_item->get_discount(), 2 ),
|
||||
);
|
||||
}
|
||||
|
||||
return array( 'order' => apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of orders
|
||||
*
|
||||
* @since 2.1
|
||||
*
|
||||
* @param string $status
|
||||
* @param array $filter
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_orders_count( $status = null, $filter = array() ) {
|
||||
|
||||
if ( ! empty( $status ) ) {
|
||||
$filter['status'] = $status;
|
||||
}
|
||||
|
||||
$query = $this->query_orders( $filter );
|
||||
|
||||
if ( ! current_user_can( 'read_private_shop_orders' ) ) {
|
||||
return new WP_Error( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), array( 'status' => 401 ) );
|
||||
}
|
||||
|
||||
return array( 'count' => (int) $query->found_posts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit an order
|
||||
*
|
||||
* API v1 only allows updating the status of an order
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int $id the order ID
|
||||
* @param array $data
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function edit_order( $id, $data ) {
|
||||
|
||||
$id = $this->validate_request( $id, 'shop_order', 'edit' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$order = wc_get_order( $id );
|
||||
|
||||
if ( ! empty( $data['status'] ) ) {
|
||||
|
||||
$order->update_status( $data['status'], isset( $data['note'] ) ? $data['note'] : '' );
|
||||
}
|
||||
|
||||
return $this->get_order( $id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an order
|
||||
*
|
||||
* @param int $id the order ID
|
||||
* @param bool $force true to permanently delete order, false to move to trash
|
||||
* @return array
|
||||
*/
|
||||
public function delete_order( $id, $force = false ) {
|
||||
|
||||
$id = $this->validate_request( $id, 'shop_order', 'delete' );
|
||||
|
||||
return $this->delete( $id, 'order', ( 'true' === $force ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the admin order notes for an order
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int $id the order ID
|
||||
* @param string $fields fields to include in response
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_order_notes( $id, $fields = null ) {
|
||||
|
||||
// ensure ID is valid order ID
|
||||
$id = $this->validate_request( $id, 'shop_order', 'read' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$args = array(
|
||||
'post_id' => $id,
|
||||
'approve' => 'approve',
|
||||
'type' => 'order_note',
|
||||
);
|
||||
|
||||
remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );
|
||||
|
||||
$notes = get_comments( $args );
|
||||
|
||||
add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );
|
||||
|
||||
$order_notes = array();
|
||||
|
||||
foreach ( $notes as $note ) {
|
||||
|
||||
$order_notes[] = array(
|
||||
'id' => $note->comment_ID,
|
||||
'created_at' => $this->server->format_datetime( $note->comment_date_gmt ),
|
||||
'note' => $note->comment_content,
|
||||
'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ),
|
||||
);
|
||||
}
|
||||
|
||||
return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $id, $fields, $notes, $this->server ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get order post objects
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $args request arguments for filtering query
|
||||
* @return WP_Query
|
||||
*/
|
||||
private function query_orders( $args ) {
|
||||
|
||||
// set base query arguments
|
||||
$query_args = array(
|
||||
'fields' => 'ids',
|
||||
'post_type' => 'shop_order',
|
||||
'post_status' => array_keys( wc_get_order_statuses() ),
|
||||
);
|
||||
|
||||
// add status argument
|
||||
if ( ! empty( $args['status'] ) ) {
|
||||
|
||||
$statuses = 'wc-' . str_replace( ',', ',wc-', $args['status'] );
|
||||
$statuses = explode( ',', $statuses );
|
||||
$query_args['post_status'] = $statuses;
|
||||
|
||||
unset( $args['status'] );
|
||||
|
||||
}
|
||||
|
||||
$query_args = $this->merge_query_args( $query_args, $args );
|
||||
|
||||
return new WP_Query( $query_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get the order subtotal
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WC_Order $order
|
||||
* @return float
|
||||
*/
|
||||
private function get_order_subtotal( $order ) {
|
||||
$subtotal = 0;
|
||||
|
||||
// subtotal
|
||||
foreach ( $order->get_items() as $item ) {
|
||||
$subtotal += $item->get_subtotal();
|
||||
}
|
||||
|
||||
return $subtotal;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,548 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API Products Class
|
||||
*
|
||||
* Handles requests to the /products endpoint
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1
|
||||
* @version 3.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_Products extends WC_API_Resource {
|
||||
|
||||
/** @var string $base the route base */
|
||||
protected $base = '/products';
|
||||
|
||||
/**
|
||||
* Register the routes for this class
|
||||
*
|
||||
* GET /products
|
||||
* GET /products/count
|
||||
* GET /products/<id>
|
||||
* GET /products/<id>/reviews
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $routes
|
||||
* @return array
|
||||
*/
|
||||
public function register_routes( $routes ) {
|
||||
|
||||
# GET /products
|
||||
$routes[ $this->base ] = array(
|
||||
array( array( $this, 'get_products' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# GET /products/count
|
||||
$routes[ $this->base . '/count' ] = array(
|
||||
array( array( $this, 'get_products_count' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# GET /products/<id>
|
||||
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
|
||||
array( array( $this, 'get_product' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# GET /products/<id>/reviews
|
||||
$routes[ $this->base . '/(?P<id>\d+)/reviews' ] = array(
|
||||
array( array( $this, 'get_product_reviews' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all products
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $fields
|
||||
* @param string $type
|
||||
* @param array $filter
|
||||
* @param int $page
|
||||
* @return array
|
||||
*/
|
||||
public function get_products( $fields = null, $type = null, $filter = array(), $page = 1 ) {
|
||||
|
||||
if ( ! empty( $type ) ) {
|
||||
$filter['type'] = $type;
|
||||
}
|
||||
|
||||
$filter['page'] = $page;
|
||||
|
||||
$query = $this->query_products( $filter );
|
||||
|
||||
$products = array();
|
||||
|
||||
foreach ( $query->posts as $product_id ) {
|
||||
|
||||
if ( ! $this->is_readable( $product_id ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$products[] = current( $this->get_product( $product_id, $fields ) );
|
||||
}
|
||||
|
||||
$this->server->add_pagination_headers( $query );
|
||||
|
||||
return array( 'products' => $products );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the product for the given ID
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int $id the product ID
|
||||
* @param string $fields
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_product( $id, $fields = null ) {
|
||||
|
||||
$id = $this->validate_request( $id, 'product', 'read' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$product = wc_get_product( $id );
|
||||
|
||||
// add data that applies to every product type
|
||||
$product_data = $this->get_product_data( $product );
|
||||
|
||||
// add variations to variable products
|
||||
if ( $product->is_type( 'variable' ) && $product->has_child() ) {
|
||||
$product_data['variations'] = $this->get_variation_data( $product );
|
||||
}
|
||||
|
||||
// add the parent product data to an individual variation
|
||||
if ( $product->is_type( 'variation' ) ) {
|
||||
$product_data['parent'] = $this->get_product_data( $product->get_parent_id() );
|
||||
}
|
||||
|
||||
return array( 'product' => apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields, $this->server ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of orders
|
||||
*
|
||||
* @since 2.1
|
||||
*
|
||||
* @param string $type
|
||||
* @param array $filter
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_products_count( $type = null, $filter = array() ) {
|
||||
|
||||
if ( ! empty( $type ) ) {
|
||||
$filter['type'] = $type;
|
||||
}
|
||||
|
||||
if ( ! current_user_can( 'read_private_products' ) ) {
|
||||
return new WP_Error( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), array( 'status' => 401 ) );
|
||||
}
|
||||
|
||||
$query = $this->query_products( $filter );
|
||||
|
||||
return array( 'count' => (int) $query->found_posts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a product
|
||||
*
|
||||
* @param int $id the product ID
|
||||
* @param array $data
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function edit_product( $id, $data ) {
|
||||
|
||||
$id = $this->validate_request( $id, 'product', 'edit' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
return $this->get_product( $id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a product
|
||||
*
|
||||
* @param int $id the product ID
|
||||
* @param bool $force true to permanently delete order, false to move to trash
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function delete_product( $id, $force = false ) {
|
||||
|
||||
$id = $this->validate_request( $id, 'product', 'delete' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
return $this->delete( $id, 'product', ( 'true' === $force ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reviews for a product
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int $id the product ID to get reviews for
|
||||
* @param string $fields fields to include in response
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_product_reviews( $id, $fields = null ) {
|
||||
|
||||
$id = $this->validate_request( $id, 'product', 'read' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$args = array(
|
||||
'post_id' => $id,
|
||||
'approve' => 'approve',
|
||||
);
|
||||
|
||||
$comments = get_comments( $args );
|
||||
|
||||
$reviews = array();
|
||||
|
||||
foreach ( $comments as $comment ) {
|
||||
|
||||
$reviews[] = array(
|
||||
'id' => $comment->comment_ID,
|
||||
'created_at' => $this->server->format_datetime( $comment->comment_date_gmt ),
|
||||
'review' => $comment->comment_content,
|
||||
'rating' => get_comment_meta( $comment->comment_ID, 'rating', true ),
|
||||
'reviewer_name' => $comment->comment_author,
|
||||
'reviewer_email' => $comment->comment_author_email,
|
||||
'verified' => wc_review_is_from_verified_owner( $comment->comment_ID ),
|
||||
);
|
||||
}
|
||||
|
||||
return array( 'product_reviews' => apply_filters( 'woocommerce_api_product_reviews_response', $reviews, $id, $fields, $comments, $this->server ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get product post objects
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $args request arguments for filtering query
|
||||
* @return WP_Query
|
||||
*/
|
||||
private function query_products( $args ) {
|
||||
|
||||
// set base query arguments
|
||||
$query_args = array(
|
||||
'fields' => 'ids',
|
||||
'post_type' => 'product',
|
||||
'post_status' => 'publish',
|
||||
'meta_query' => array(),
|
||||
);
|
||||
|
||||
if ( ! empty( $args['type'] ) ) {
|
||||
|
||||
$types = explode( ',', $args['type'] );
|
||||
|
||||
$query_args['tax_query'] = array(
|
||||
array(
|
||||
'taxonomy' => 'product_type',
|
||||
'field' => 'slug',
|
||||
'terms' => $types,
|
||||
),
|
||||
);
|
||||
|
||||
unset( $args['type'] );
|
||||
}
|
||||
|
||||
$query_args = $this->merge_query_args( $query_args, $args );
|
||||
|
||||
return new WP_Query( $query_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get standard product data that applies to every product type
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WC_Product|int $product
|
||||
* @return array
|
||||
*/
|
||||
private function get_product_data( $product ) {
|
||||
if ( is_numeric( $product ) ) {
|
||||
$product = wc_get_product( $product );
|
||||
}
|
||||
|
||||
if ( ! is_a( $product, 'WC_Product' ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return array(
|
||||
'title' => $product->get_name(),
|
||||
'id' => $product->get_id(),
|
||||
'created_at' => $this->server->format_datetime( $product->get_date_created(), false, true ),
|
||||
'updated_at' => $this->server->format_datetime( $product->get_date_modified(), false, true ),
|
||||
'type' => $product->get_type(),
|
||||
'status' => $product->get_status(),
|
||||
'downloadable' => $product->is_downloadable(),
|
||||
'virtual' => $product->is_virtual(),
|
||||
'permalink' => $product->get_permalink(),
|
||||
'sku' => $product->get_sku(),
|
||||
'price' => wc_format_decimal( $product->get_price(), 2 ),
|
||||
'regular_price' => wc_format_decimal( $product->get_regular_price(), 2 ),
|
||||
'sale_price' => $product->get_sale_price() ? wc_format_decimal( $product->get_sale_price(), 2 ) : null,
|
||||
'price_html' => $product->get_price_html(),
|
||||
'taxable' => $product->is_taxable(),
|
||||
'tax_status' => $product->get_tax_status(),
|
||||
'tax_class' => $product->get_tax_class(),
|
||||
'managing_stock' => $product->managing_stock(),
|
||||
'stock_quantity' => $product->get_stock_quantity(),
|
||||
'in_stock' => $product->is_in_stock(),
|
||||
'backorders_allowed' => $product->backorders_allowed(),
|
||||
'backordered' => $product->is_on_backorder(),
|
||||
'sold_individually' => $product->is_sold_individually(),
|
||||
'purchaseable' => $product->is_purchasable(),
|
||||
'featured' => $product->is_featured(),
|
||||
'visible' => $product->is_visible(),
|
||||
'catalog_visibility' => $product->get_catalog_visibility(),
|
||||
'on_sale' => $product->is_on_sale(),
|
||||
'weight' => $product->get_weight() ? wc_format_decimal( $product->get_weight(), 2 ) : null,
|
||||
'dimensions' => array(
|
||||
'length' => $product->get_length(),
|
||||
'width' => $product->get_width(),
|
||||
'height' => $product->get_height(),
|
||||
'unit' => get_option( 'woocommerce_dimension_unit' ),
|
||||
),
|
||||
'shipping_required' => $product->needs_shipping(),
|
||||
'shipping_taxable' => $product->is_shipping_taxable(),
|
||||
'shipping_class' => $product->get_shipping_class(),
|
||||
'shipping_class_id' => ( 0 !== $product->get_shipping_class_id() ) ? $product->get_shipping_class_id() : null,
|
||||
'description' => apply_filters( 'the_content', $product->get_description() ),
|
||||
'short_description' => apply_filters( 'woocommerce_short_description', $product->get_short_description() ),
|
||||
'reviews_allowed' => $product->get_reviews_allowed(),
|
||||
'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ),
|
||||
'rating_count' => $product->get_rating_count(),
|
||||
'related_ids' => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ),
|
||||
'upsell_ids' => array_map( 'absint', $product->get_upsell_ids() ),
|
||||
'cross_sell_ids' => array_map( 'absint', $product->get_cross_sell_ids() ),
|
||||
'categories' => wc_get_object_terms( $product->get_id(), 'product_cat', 'name' ),
|
||||
'tags' => wc_get_object_terms( $product->get_id(), 'product_tag', 'name' ),
|
||||
'images' => $this->get_images( $product ),
|
||||
'featured_src' => wp_get_attachment_url( get_post_thumbnail_id( $product->get_id() ) ),
|
||||
'attributes' => $this->get_attributes( $product ),
|
||||
'downloads' => $this->get_downloads( $product ),
|
||||
'download_limit' => $product->get_download_limit(),
|
||||
'download_expiry' => $product->get_download_expiry(),
|
||||
'download_type' => 'standard',
|
||||
'purchase_note' => apply_filters( 'the_content', $product->get_purchase_note() ),
|
||||
'total_sales' => $product->get_total_sales(),
|
||||
'variations' => array(),
|
||||
'parent' => array(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an individual variation's data
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WC_Product $product
|
||||
* @return array
|
||||
*/
|
||||
private function get_variation_data( $product ) {
|
||||
$variations = array();
|
||||
|
||||
foreach ( $product->get_children() as $child_id ) {
|
||||
$variation = wc_get_product( $child_id );
|
||||
|
||||
if ( ! $variation || ! $variation->exists() ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$variations[] = array(
|
||||
'id' => $variation->get_id(),
|
||||
'created_at' => $this->server->format_datetime( $variation->get_date_created(), false, true ),
|
||||
'updated_at' => $this->server->format_datetime( $variation->get_date_modified(), false, true ),
|
||||
'downloadable' => $variation->is_downloadable(),
|
||||
'virtual' => $variation->is_virtual(),
|
||||
'permalink' => $variation->get_permalink(),
|
||||
'sku' => $variation->get_sku(),
|
||||
'price' => wc_format_decimal( $variation->get_price(), 2 ),
|
||||
'regular_price' => wc_format_decimal( $variation->get_regular_price(), 2 ),
|
||||
'sale_price' => $variation->get_sale_price() ? wc_format_decimal( $variation->get_sale_price(), 2 ) : null,
|
||||
'taxable' => $variation->is_taxable(),
|
||||
'tax_status' => $variation->get_tax_status(),
|
||||
'tax_class' => $variation->get_tax_class(),
|
||||
'stock_quantity' => (int) $variation->get_stock_quantity(),
|
||||
'in_stock' => $variation->is_in_stock(),
|
||||
'backordered' => $variation->is_on_backorder(),
|
||||
'purchaseable' => $variation->is_purchasable(),
|
||||
'visible' => $variation->variation_is_visible(),
|
||||
'on_sale' => $variation->is_on_sale(),
|
||||
'weight' => $variation->get_weight() ? wc_format_decimal( $variation->get_weight(), 2 ) : null,
|
||||
'dimensions' => array(
|
||||
'length' => $variation->get_length(),
|
||||
'width' => $variation->get_width(),
|
||||
'height' => $variation->get_height(),
|
||||
'unit' => get_option( 'woocommerce_dimension_unit' ),
|
||||
),
|
||||
'shipping_class' => $variation->get_shipping_class(),
|
||||
'shipping_class_id' => ( 0 !== $variation->get_shipping_class_id() ) ? $variation->get_shipping_class_id() : null,
|
||||
'image' => $this->get_images( $variation ),
|
||||
'attributes' => $this->get_attributes( $variation ),
|
||||
'downloads' => $this->get_downloads( $variation ),
|
||||
'download_limit' => (int) $product->get_download_limit(),
|
||||
'download_expiry' => (int) $product->get_download_expiry(),
|
||||
);
|
||||
}
|
||||
|
||||
return $variations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the images for a product or product variation
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WC_Product|WC_Product_Variation $product
|
||||
* @return array
|
||||
*/
|
||||
private function get_images( $product ) {
|
||||
$images = $attachment_ids = array();
|
||||
$product_image = $product->get_image_id();
|
||||
|
||||
// Add featured image.
|
||||
if ( ! empty( $product_image ) ) {
|
||||
$attachment_ids[] = $product_image;
|
||||
}
|
||||
|
||||
// add gallery images.
|
||||
$attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() );
|
||||
|
||||
// Build image data.
|
||||
foreach ( $attachment_ids as $position => $attachment_id ) {
|
||||
|
||||
$attachment_post = get_post( $attachment_id );
|
||||
|
||||
if ( is_null( $attachment_post ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attachment = wp_get_attachment_image_src( $attachment_id, 'full' );
|
||||
|
||||
if ( ! is_array( $attachment ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$images[] = array(
|
||||
'id' => (int) $attachment_id,
|
||||
'created_at' => $this->server->format_datetime( $attachment_post->post_date_gmt ),
|
||||
'updated_at' => $this->server->format_datetime( $attachment_post->post_modified_gmt ),
|
||||
'src' => current( $attachment ),
|
||||
'title' => get_the_title( $attachment_id ),
|
||||
'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
|
||||
'position' => $position,
|
||||
);
|
||||
}
|
||||
|
||||
// Set a placeholder image if the product has no images set.
|
||||
if ( empty( $images ) ) {
|
||||
|
||||
$images[] = array(
|
||||
'id' => 0,
|
||||
'created_at' => $this->server->format_datetime( time() ), // default to now
|
||||
'updated_at' => $this->server->format_datetime( time() ),
|
||||
'src' => wc_placeholder_img_src(),
|
||||
'title' => __( 'Placeholder', 'woocommerce' ),
|
||||
'alt' => __( 'Placeholder', 'woocommerce' ),
|
||||
'position' => 0,
|
||||
);
|
||||
}
|
||||
|
||||
return $images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attribute options.
|
||||
*
|
||||
* @param int $product_id
|
||||
* @param array $attribute
|
||||
* @return array
|
||||
*/
|
||||
protected function get_attribute_options( $product_id, $attribute ) {
|
||||
if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) {
|
||||
return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) );
|
||||
} elseif ( isset( $attribute['value'] ) ) {
|
||||
return array_map( 'trim', explode( '|', $attribute['value'] ) );
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attributes for a product or product variation
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WC_Product|WC_Product_Variation $product
|
||||
* @return array
|
||||
*/
|
||||
private function get_attributes( $product ) {
|
||||
|
||||
$attributes = array();
|
||||
|
||||
if ( $product->is_type( 'variation' ) ) {
|
||||
|
||||
// variation attributes
|
||||
foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) {
|
||||
|
||||
// taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`
|
||||
$attributes[] = array(
|
||||
'name' => ucwords( str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $attribute_name ) ) ),
|
||||
'option' => $attribute,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
||||
foreach ( $product->get_attributes() as $attribute ) {
|
||||
$attributes[] = array(
|
||||
'name' => ucwords( wc_attribute_taxonomy_slug( $attribute['name'] ) ),
|
||||
'position' => $attribute['position'],
|
||||
'visible' => (bool) $attribute['is_visible'],
|
||||
'variation' => (bool) $attribute['is_variation'],
|
||||
'options' => $this->get_attribute_options( $product->get_id(), $attribute ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the downloads for a product or product variation
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WC_Product|WC_Product_Variation $product
|
||||
* @return array
|
||||
*/
|
||||
private function get_downloads( $product ) {
|
||||
|
||||
$downloads = array();
|
||||
|
||||
if ( $product->is_downloadable() ) {
|
||||
|
||||
foreach ( $product->get_downloads() as $file_id => $file ) {
|
||||
|
||||
$downloads[] = array(
|
||||
'id' => $file_id, // do not cast as int as this is a hash
|
||||
'name' => $file['name'],
|
||||
'file' => $file['file'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $downloads;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,482 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API Reports Class
|
||||
*
|
||||
* Handles requests to the /reports endpoint
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1
|
||||
* @version 2.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_Reports extends WC_API_Resource {
|
||||
|
||||
/** @var string $base the route base */
|
||||
protected $base = '/reports';
|
||||
|
||||
/** @var WC_Admin_Report instance */
|
||||
private $report;
|
||||
|
||||
/**
|
||||
* Register the routes for this class
|
||||
*
|
||||
* GET /reports
|
||||
* GET /reports/sales
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $routes
|
||||
* @return array
|
||||
*/
|
||||
public function register_routes( $routes ) {
|
||||
|
||||
# GET /reports
|
||||
$routes[ $this->base ] = array(
|
||||
array( array( $this, 'get_reports' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# GET /reports/sales
|
||||
$routes[ $this->base . '/sales' ] = array(
|
||||
array( array( $this, 'get_sales_report' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# GET /reports/sales/top_sellers
|
||||
$routes[ $this->base . '/sales/top_sellers' ] = array(
|
||||
array( array( $this, 'get_top_sellers_report' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a simple listing of available reports
|
||||
*
|
||||
* @since 2.1
|
||||
* @return array
|
||||
*/
|
||||
public function get_reports() {
|
||||
|
||||
return array( 'reports' => array( 'sales', 'sales/top_sellers' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sales report
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $fields fields to include in response
|
||||
* @param array $filter date filtering
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_sales_report( $fields = null, $filter = array() ) {
|
||||
|
||||
// check user permissions
|
||||
$check = $this->validate_request();
|
||||
|
||||
if ( is_wp_error( $check ) ) {
|
||||
return $check;
|
||||
}
|
||||
|
||||
// set date filtering
|
||||
$this->setup_report( $filter );
|
||||
|
||||
// total sales, taxes, shipping, and order count
|
||||
$totals = $this->report->get_order_report_data( array(
|
||||
'data' => array(
|
||||
'_order_total' => array(
|
||||
'type' => 'meta',
|
||||
'function' => 'SUM',
|
||||
'name' => 'sales',
|
||||
),
|
||||
'_order_tax' => array(
|
||||
'type' => 'meta',
|
||||
'function' => 'SUM',
|
||||
'name' => 'tax',
|
||||
),
|
||||
'_order_shipping_tax' => array(
|
||||
'type' => 'meta',
|
||||
'function' => 'SUM',
|
||||
'name' => 'shipping_tax',
|
||||
),
|
||||
'_order_shipping' => array(
|
||||
'type' => 'meta',
|
||||
'function' => 'SUM',
|
||||
'name' => 'shipping',
|
||||
),
|
||||
'ID' => array(
|
||||
'type' => 'post_data',
|
||||
'function' => 'COUNT',
|
||||
'name' => 'order_count',
|
||||
),
|
||||
),
|
||||
'filter_range' => true,
|
||||
) );
|
||||
|
||||
// total items ordered
|
||||
$total_items = absint( $this->report->get_order_report_data( array(
|
||||
'data' => array(
|
||||
'_qty' => array(
|
||||
'type' => 'order_item_meta',
|
||||
'order_item_type' => 'line_item',
|
||||
'function' => 'SUM',
|
||||
'name' => 'order_item_qty',
|
||||
),
|
||||
),
|
||||
'query_type' => 'get_var',
|
||||
'filter_range' => true,
|
||||
) ) );
|
||||
|
||||
// total discount used
|
||||
$total_discount = $this->report->get_order_report_data( array(
|
||||
'data' => array(
|
||||
'discount_amount' => array(
|
||||
'type' => 'order_item_meta',
|
||||
'order_item_type' => 'coupon',
|
||||
'function' => 'SUM',
|
||||
'name' => 'discount_amount',
|
||||
),
|
||||
),
|
||||
'where' => array(
|
||||
array(
|
||||
'key' => 'order_item_type',
|
||||
'value' => 'coupon',
|
||||
'operator' => '=',
|
||||
),
|
||||
),
|
||||
'query_type' => 'get_var',
|
||||
'filter_range' => true,
|
||||
) );
|
||||
|
||||
// new customers
|
||||
$users_query = new WP_User_Query(
|
||||
array(
|
||||
'fields' => array( 'user_registered' ),
|
||||
'role' => 'customer',
|
||||
)
|
||||
);
|
||||
|
||||
$customers = $users_query->get_results();
|
||||
|
||||
foreach ( $customers as $key => $customer ) {
|
||||
if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) {
|
||||
unset( $customers[ $key ] );
|
||||
}
|
||||
}
|
||||
|
||||
$total_customers = count( $customers );
|
||||
|
||||
// get order totals grouped by period
|
||||
$orders = $this->report->get_order_report_data( array(
|
||||
'data' => array(
|
||||
'_order_total' => array(
|
||||
'type' => 'meta',
|
||||
'function' => 'SUM',
|
||||
'name' => 'total_sales',
|
||||
),
|
||||
'_order_shipping' => array(
|
||||
'type' => 'meta',
|
||||
'function' => 'SUM',
|
||||
'name' => 'total_shipping',
|
||||
),
|
||||
'_order_tax' => array(
|
||||
'type' => 'meta',
|
||||
'function' => 'SUM',
|
||||
'name' => 'total_tax',
|
||||
),
|
||||
'_order_shipping_tax' => array(
|
||||
'type' => 'meta',
|
||||
'function' => 'SUM',
|
||||
'name' => 'total_shipping_tax',
|
||||
),
|
||||
'ID' => array(
|
||||
'type' => 'post_data',
|
||||
'function' => 'COUNT',
|
||||
'name' => 'total_orders',
|
||||
'distinct' => true,
|
||||
),
|
||||
'post_date' => array(
|
||||
'type' => 'post_data',
|
||||
'function' => '',
|
||||
'name' => 'post_date',
|
||||
),
|
||||
),
|
||||
'group_by' => $this->report->group_by_query,
|
||||
'order_by' => 'post_date ASC',
|
||||
'query_type' => 'get_results',
|
||||
'filter_range' => true,
|
||||
) );
|
||||
|
||||
// get order item totals grouped by period
|
||||
$order_items = $this->report->get_order_report_data( array(
|
||||
'data' => array(
|
||||
'_qty' => array(
|
||||
'type' => 'order_item_meta',
|
||||
'order_item_type' => 'line_item',
|
||||
'function' => 'SUM',
|
||||
'name' => 'order_item_count',
|
||||
),
|
||||
'post_date' => array(
|
||||
'type' => 'post_data',
|
||||
'function' => '',
|
||||
'name' => 'post_date',
|
||||
),
|
||||
),
|
||||
'where' => array(
|
||||
array(
|
||||
'key' => 'order_item_type',
|
||||
'value' => 'line_item',
|
||||
'operator' => '=',
|
||||
),
|
||||
),
|
||||
'group_by' => $this->report->group_by_query,
|
||||
'order_by' => 'post_date ASC',
|
||||
'query_type' => 'get_results',
|
||||
'filter_range' => true,
|
||||
) );
|
||||
|
||||
// get discount totals grouped by period
|
||||
$discounts = $this->report->get_order_report_data( array(
|
||||
'data' => array(
|
||||
'discount_amount' => array(
|
||||
'type' => 'order_item_meta',
|
||||
'order_item_type' => 'coupon',
|
||||
'function' => 'SUM',
|
||||
'name' => 'discount_amount',
|
||||
),
|
||||
'post_date' => array(
|
||||
'type' => 'post_data',
|
||||
'function' => '',
|
||||
'name' => 'post_date',
|
||||
),
|
||||
),
|
||||
'where' => array(
|
||||
array(
|
||||
'key' => 'order_item_type',
|
||||
'value' => 'coupon',
|
||||
'operator' => '=',
|
||||
),
|
||||
),
|
||||
'group_by' => $this->report->group_by_query . ', order_item_name',
|
||||
'order_by' => 'post_date ASC',
|
||||
'query_type' => 'get_results',
|
||||
'filter_range' => true,
|
||||
) );
|
||||
|
||||
$period_totals = array();
|
||||
|
||||
// setup period totals by ensuring each period in the interval has data
|
||||
for ( $i = 0; $i <= $this->report->chart_interval; $i ++ ) {
|
||||
|
||||
switch ( $this->report->chart_groupby ) {
|
||||
case 'day' :
|
||||
$time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) );
|
||||
break;
|
||||
case 'month' :
|
||||
$time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) );
|
||||
break;
|
||||
}
|
||||
|
||||
// set the customer signups for each period
|
||||
$customer_count = 0;
|
||||
foreach ( $customers as $customer ) {
|
||||
|
||||
if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) {
|
||||
$customer_count++;
|
||||
}
|
||||
}
|
||||
|
||||
$period_totals[ $time ] = array(
|
||||
'sales' => wc_format_decimal( 0.00, 2 ),
|
||||
'orders' => 0,
|
||||
'items' => 0,
|
||||
'tax' => wc_format_decimal( 0.00, 2 ),
|
||||
'shipping' => wc_format_decimal( 0.00, 2 ),
|
||||
'discount' => wc_format_decimal( 0.00, 2 ),
|
||||
'customers' => $customer_count,
|
||||
);
|
||||
}
|
||||
|
||||
// add total sales, total order count, total tax and total shipping for each period
|
||||
foreach ( $orders as $order ) {
|
||||
|
||||
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );
|
||||
|
||||
if ( ! isset( $period_totals[ $time ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$period_totals[ $time ]['sales'] = wc_format_decimal( $order->total_sales, 2 );
|
||||
$period_totals[ $time ]['orders'] = (int) $order->total_orders;
|
||||
$period_totals[ $time ]['tax'] = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 );
|
||||
$period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 );
|
||||
}
|
||||
|
||||
// add total order items for each period
|
||||
foreach ( $order_items as $order_item ) {
|
||||
|
||||
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) );
|
||||
|
||||
if ( ! isset( $period_totals[ $time ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$period_totals[ $time ]['items'] = (int) $order_item->order_item_count;
|
||||
}
|
||||
|
||||
// add total discount for each period
|
||||
foreach ( $discounts as $discount ) {
|
||||
|
||||
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) );
|
||||
|
||||
if ( ! isset( $period_totals[ $time ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 );
|
||||
}
|
||||
|
||||
$sales_data = array(
|
||||
'total_sales' => wc_format_decimal( $totals->sales, 2 ),
|
||||
'average_sales' => wc_format_decimal( $totals->sales / ( $this->report->chart_interval + 1 ), 2 ),
|
||||
'total_orders' => (int) $totals->order_count,
|
||||
'total_items' => $total_items,
|
||||
'total_tax' => wc_format_decimal( $totals->tax + $totals->shipping_tax, 2 ),
|
||||
'total_shipping' => wc_format_decimal( $totals->shipping, 2 ),
|
||||
'total_discount' => is_null( $total_discount ) ? wc_format_decimal( 0.00, 2 ) : wc_format_decimal( $total_discount, 2 ),
|
||||
'totals_grouped_by' => $this->report->chart_groupby,
|
||||
'totals' => $period_totals,
|
||||
'total_customers' => $total_customers,
|
||||
);
|
||||
|
||||
return array( 'sales' => apply_filters( 'woocommerce_api_report_response', $sales_data, $this->report, $fields, $this->server ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the top sellers report
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $fields fields to include in response
|
||||
* @param array $filter date filtering
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_top_sellers_report( $fields = null, $filter = array() ) {
|
||||
|
||||
// check user permissions
|
||||
$check = $this->validate_request();
|
||||
|
||||
if ( is_wp_error( $check ) ) {
|
||||
return $check;
|
||||
}
|
||||
|
||||
// set date filtering
|
||||
$this->setup_report( $filter );
|
||||
|
||||
$top_sellers = $this->report->get_order_report_data( array(
|
||||
'data' => array(
|
||||
'_product_id' => array(
|
||||
'type' => 'order_item_meta',
|
||||
'order_item_type' => 'line_item',
|
||||
'function' => '',
|
||||
'name' => 'product_id',
|
||||
),
|
||||
'_qty' => array(
|
||||
'type' => 'order_item_meta',
|
||||
'order_item_type' => 'line_item',
|
||||
'function' => 'SUM',
|
||||
'name' => 'order_item_qty',
|
||||
),
|
||||
),
|
||||
'order_by' => 'order_item_qty DESC',
|
||||
'group_by' => 'product_id',
|
||||
'limit' => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12,
|
||||
'query_type' => 'get_results',
|
||||
'filter_range' => true,
|
||||
) );
|
||||
|
||||
$top_sellers_data = array();
|
||||
|
||||
foreach ( $top_sellers as $top_seller ) {
|
||||
|
||||
$product = wc_get_product( $top_seller->product_id );
|
||||
|
||||
$top_sellers_data[] = array(
|
||||
'title' => $product->get_name(),
|
||||
'product_id' => $top_seller->product_id,
|
||||
'quantity' => $top_seller->order_item_qty,
|
||||
);
|
||||
}
|
||||
|
||||
return array( 'top_sellers' => apply_filters( 'woocommerce_api_report_response', $top_sellers_data, $this->report, $fields, $this->server ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the report object and parse any date filtering
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $filter date filtering
|
||||
*/
|
||||
private function setup_report( $filter ) {
|
||||
|
||||
include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' );
|
||||
|
||||
$this->report = new WC_Admin_Report();
|
||||
|
||||
if ( empty( $filter['period'] ) ) {
|
||||
|
||||
// custom date range
|
||||
$filter['period'] = 'custom';
|
||||
|
||||
if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) {
|
||||
|
||||
// overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges
|
||||
$_GET['start_date'] = $this->server->parse_datetime( $filter['date_min'] );
|
||||
$_GET['end_date'] = isset( $filter['date_max'] ) ? $this->server->parse_datetime( $filter['date_max'] ) : null;
|
||||
|
||||
} else {
|
||||
|
||||
// default custom range to today
|
||||
$_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) );
|
||||
}
|
||||
} else {
|
||||
|
||||
// ensure period is valid
|
||||
if ( ! in_array( $filter['period'], array( 'week', 'month', 'last_month', 'year' ) ) ) {
|
||||
$filter['period'] = 'week';
|
||||
}
|
||||
|
||||
// TODO: change WC_Admin_Report class to use "week" instead, as it's more consistent with other periods
|
||||
// allow "week" for period instead of "7day"
|
||||
if ( 'week' === $filter['period'] ) {
|
||||
$filter['period'] = '7day';
|
||||
}
|
||||
}
|
||||
|
||||
$this->report->calculate_current_range( $filter['period'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the current user has permission to view reports
|
||||
*
|
||||
* @since 2.1
|
||||
* @see WC_API_Resource::validate_request()
|
||||
* @param null $id unused
|
||||
* @param null $type unused
|
||||
* @param null $context unused
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
protected function validate_request( $id = null, $type = null, $context = null ) {
|
||||
|
||||
if ( ! current_user_can( 'view_woocommerce_reports' ) ) {
|
||||
|
||||
return new WP_Error( 'woocommerce_api_user_cannot_read_report', __( 'You do not have permission to read this report', 'woocommerce' ), array( 'status' => 401 ) );
|
||||
|
||||
} else {
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,409 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API Resource class
|
||||
*
|
||||
* Provides shared functionality for resource-specific API classes
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1
|
||||
* @version 2.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_Resource {
|
||||
|
||||
/** @var WC_API_Server the API server */
|
||||
protected $server;
|
||||
|
||||
/** @var string sub-classes override this to set a resource-specific base route */
|
||||
protected $base;
|
||||
|
||||
/**
|
||||
* Setup class
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WC_API_Server $server
|
||||
*/
|
||||
public function __construct( WC_API_Server $server ) {
|
||||
|
||||
$this->server = $server;
|
||||
|
||||
// automatically register routes for sub-classes
|
||||
add_filter( 'woocommerce_api_endpoints', array( $this, 'register_routes' ) );
|
||||
|
||||
// remove fields from responses when requests specify certain fields
|
||||
// note these are hooked at a later priority so data added via filters (e.g. customer data to the order response)
|
||||
// still has the fields filtered properly
|
||||
foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) {
|
||||
|
||||
add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 );
|
||||
add_filter( "woocommerce_api_{$resource}_response", array( $this, 'filter_response_fields' ), 20, 3 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the request by checking:
|
||||
*
|
||||
* 1) the ID is a valid integer
|
||||
* 2) the ID returns a valid post object and matches the provided post type
|
||||
* 3) the current user has the proper permissions to read/edit/delete the post
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string|int $id the post ID
|
||||
* @param string $type the post type, either `shop_order`, `shop_coupon`, or `product`
|
||||
* @param string $context the context of the request, either `read`, `edit` or `delete`
|
||||
* @return int|WP_Error valid post ID or WP_Error if any of the checks fails
|
||||
*/
|
||||
protected function validate_request( $id, $type, $context ) {
|
||||
|
||||
if ( 'shop_order' === $type || 'shop_coupon' === $type ) {
|
||||
$resource_name = str_replace( 'shop_', '', $type );
|
||||
} else {
|
||||
$resource_name = $type;
|
||||
}
|
||||
|
||||
$id = absint( $id );
|
||||
|
||||
// validate ID
|
||||
if ( empty( $id ) ) {
|
||||
return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
// only custom post types have per-post type/permission checks
|
||||
if ( 'customer' !== $type ) {
|
||||
|
||||
$post = get_post( $id );
|
||||
|
||||
// for checking permissions, product variations are the same as the product post type
|
||||
$post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type;
|
||||
|
||||
// validate post type
|
||||
if ( $type !== $post_type ) {
|
||||
return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
// validate permissions
|
||||
switch ( $context ) {
|
||||
|
||||
case 'read':
|
||||
if ( ! $this->is_readable( $post ) ) {
|
||||
return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'edit':
|
||||
if ( ! $this->is_editable( $post ) ) {
|
||||
return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
if ( ! $this->is_deletable( $post ) ) {
|
||||
return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add common request arguments to argument list before WP_Query is run
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $base_args required arguments for the query (e.g. `post_type`, etc)
|
||||
* @param array $request_args arguments provided in the request
|
||||
* @return array
|
||||
*/
|
||||
protected function merge_query_args( $base_args, $request_args ) {
|
||||
|
||||
$args = array();
|
||||
|
||||
// date
|
||||
if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) {
|
||||
|
||||
$args['date_query'] = array();
|
||||
|
||||
// resources created after specified date
|
||||
if ( ! empty( $request_args['created_at_min'] ) ) {
|
||||
$args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true );
|
||||
}
|
||||
|
||||
// resources created before specified date
|
||||
if ( ! empty( $request_args['created_at_max'] ) ) {
|
||||
$args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true );
|
||||
}
|
||||
|
||||
// resources updated after specified date
|
||||
if ( ! empty( $request_args['updated_at_min'] ) ) {
|
||||
$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true );
|
||||
}
|
||||
|
||||
// resources updated before specified date
|
||||
if ( ! empty( $request_args['updated_at_max'] ) ) {
|
||||
$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true );
|
||||
}
|
||||
}
|
||||
|
||||
// search
|
||||
if ( ! empty( $request_args['q'] ) ) {
|
||||
$args['s'] = $request_args['q'];
|
||||
}
|
||||
|
||||
// resources per response
|
||||
if ( ! empty( $request_args['limit'] ) ) {
|
||||
$args['posts_per_page'] = $request_args['limit'];
|
||||
}
|
||||
|
||||
// resource offset
|
||||
if ( ! empty( $request_args['offset'] ) ) {
|
||||
$args['offset'] = $request_args['offset'];
|
||||
}
|
||||
|
||||
// resource page
|
||||
$args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1;
|
||||
|
||||
return array_merge( $base_args, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add meta to resources when requested by the client. Meta is added as a top-level
|
||||
* `<resource_name>_meta` attribute (e.g. `order_meta`) as a list of key/value pairs
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $data the resource data
|
||||
* @param object $resource the resource object (e.g WC_Order)
|
||||
* @return mixed
|
||||
*/
|
||||
public function maybe_add_meta( $data, $resource ) {
|
||||
|
||||
if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) {
|
||||
|
||||
// don't attempt to add meta more than once
|
||||
if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
// define the top-level property name for the meta
|
||||
switch ( get_class( $resource ) ) {
|
||||
|
||||
case 'WC_Order':
|
||||
$meta_name = 'order_meta';
|
||||
break;
|
||||
|
||||
case 'WC_Coupon':
|
||||
$meta_name = 'coupon_meta';
|
||||
break;
|
||||
|
||||
case 'WP_User':
|
||||
$meta_name = 'customer_meta';
|
||||
break;
|
||||
|
||||
default:
|
||||
$meta_name = 'product_meta';
|
||||
break;
|
||||
}
|
||||
|
||||
if ( is_a( $resource, 'WP_User' ) ) {
|
||||
|
||||
// customer meta
|
||||
$meta = (array) get_user_meta( $resource->ID );
|
||||
|
||||
} else {
|
||||
|
||||
// coupon/order/product meta
|
||||
$meta = (array) get_post_meta( $resource->get_id() );
|
||||
}
|
||||
|
||||
foreach ( $meta as $meta_key => $meta_value ) {
|
||||
|
||||
// don't add hidden meta by default
|
||||
if ( ! is_protected_meta( $meta_key ) ) {
|
||||
$data[ $meta_name ][ $meta_key ] = maybe_unserialize( $meta_value[0] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restrict the fields included in the response if the request specified certain only certain fields should be returned
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $data the response data
|
||||
* @param object $resource the object that provided the response data, e.g. WC_Coupon or WC_Order
|
||||
* @param array|string the requested list of fields to include in the response
|
||||
* @return array response data
|
||||
*/
|
||||
public function filter_response_fields( $data, $resource, $fields ) {
|
||||
|
||||
if ( ! is_array( $data ) || empty( $fields ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$fields = explode( ',', $fields );
|
||||
$sub_fields = array();
|
||||
|
||||
// get sub fields
|
||||
foreach ( $fields as $field ) {
|
||||
|
||||
if ( false !== strpos( $field, '.' ) ) {
|
||||
|
||||
list( $name, $value ) = explode( '.', $field );
|
||||
|
||||
$sub_fields[ $name ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// iterate through top-level fields
|
||||
foreach ( $data as $data_field => $data_value ) {
|
||||
|
||||
// if a field has sub-fields and the top-level field has sub-fields to filter
|
||||
if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) {
|
||||
|
||||
// iterate through each sub-field
|
||||
foreach ( $data_value as $sub_field => $sub_field_value ) {
|
||||
|
||||
// remove non-matching sub-fields
|
||||
if ( ! in_array( $sub_field, $sub_fields ) ) {
|
||||
unset( $data[ $data_field ][ $sub_field ] );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// remove non-matching top-level fields
|
||||
if ( ! in_array( $data_field, $fields ) ) {
|
||||
unset( $data[ $data_field ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a given resource
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int $id the resource ID
|
||||
* @param string $type the resource post type, or `customer`
|
||||
* @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`)
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
protected function delete( $id, $type, $force = false ) {
|
||||
|
||||
if ( 'shop_order' === $type || 'shop_coupon' === $type ) {
|
||||
$resource_name = str_replace( 'shop_', '', $type );
|
||||
} else {
|
||||
$resource_name = $type;
|
||||
}
|
||||
|
||||
if ( 'customer' === $type ) {
|
||||
|
||||
$result = wp_delete_user( $id );
|
||||
|
||||
if ( $result ) {
|
||||
return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) );
|
||||
} else {
|
||||
return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) );
|
||||
}
|
||||
} else {
|
||||
|
||||
// delete order/coupon/product
|
||||
$result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id );
|
||||
|
||||
if ( ! $result ) {
|
||||
return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) );
|
||||
}
|
||||
|
||||
if ( $force ) {
|
||||
return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) );
|
||||
|
||||
} else {
|
||||
|
||||
$this->server->send_status( '202' );
|
||||
|
||||
return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the given post is readable by the current user
|
||||
*
|
||||
* @since 2.1
|
||||
* @see WC_API_Resource::check_permission()
|
||||
* @param WP_Post|int $post
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_readable( $post ) {
|
||||
|
||||
return $this->check_permission( $post, 'read' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given post is editable by the current user
|
||||
*
|
||||
* @since 2.1
|
||||
* @see WC_API_Resource::check_permission()
|
||||
* @param WP_Post|int $post
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_editable( $post ) {
|
||||
|
||||
return $this->check_permission( $post, 'edit' );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given post is deletable by the current user
|
||||
*
|
||||
* @since 2.1
|
||||
* @see WC_API_Resource::check_permission()
|
||||
* @param WP_Post|int $post
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_deletable( $post ) {
|
||||
|
||||
return $this->check_permission( $post, 'delete' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the permissions for the current user given a post and context
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WP_Post|int $post
|
||||
* @param string $context the type of permission to check, either `read`, `write`, or `delete`
|
||||
* @return bool true if the current user has the permissions to perform the context on the post
|
||||
*/
|
||||
private function check_permission( $post, $context ) {
|
||||
|
||||
if ( ! is_a( $post, 'WP_Post' ) ) {
|
||||
$post = get_post( $post );
|
||||
}
|
||||
|
||||
if ( is_null( $post ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$post_type = get_post_type_object( $post->post_type );
|
||||
|
||||
if ( 'read' === $context ) {
|
||||
return current_user_can( $post_type->cap->read_private_posts, $post->ID );
|
||||
} elseif ( 'edit' === $context ) {
|
||||
return current_user_can( $post_type->cap->edit_post, $post->ID );
|
||||
} elseif ( 'delete' === $context ) {
|
||||
return current_user_can( $post_type->cap->delete_post, $post->ID );
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,782 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API
|
||||
*
|
||||
* Handles REST API requests
|
||||
*
|
||||
* This class and related code (JSON response handler, resource classes) are based on WP-API v0.6 (https://github.com/WP-API/WP-API)
|
||||
* Many thanks to Ryan McCue and any other contributors!
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1
|
||||
* @version 2.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/admin.php';
|
||||
|
||||
class WC_API_Server {
|
||||
|
||||
const METHOD_GET = 1;
|
||||
const METHOD_POST = 2;
|
||||
const METHOD_PUT = 4;
|
||||
const METHOD_PATCH = 8;
|
||||
const METHOD_DELETE = 16;
|
||||
|
||||
const READABLE = 1; // GET
|
||||
const CREATABLE = 2; // POST
|
||||
const EDITABLE = 14; // POST | PUT | PATCH
|
||||
const DELETABLE = 16; // DELETE
|
||||
const ALLMETHODS = 31; // GET | POST | PUT | PATCH | DELETE
|
||||
|
||||
/**
|
||||
* Does the endpoint accept a raw request body?
|
||||
*/
|
||||
const ACCEPT_RAW_DATA = 64;
|
||||
|
||||
/** Does the endpoint accept a request body? (either JSON or XML) */
|
||||
const ACCEPT_DATA = 128;
|
||||
|
||||
/**
|
||||
* Should we hide this endpoint from the index?
|
||||
*/
|
||||
const HIDDEN_ENDPOINT = 256;
|
||||
|
||||
/**
|
||||
* Map of HTTP verbs to constants
|
||||
* @var array
|
||||
*/
|
||||
public static $method_map = array(
|
||||
'HEAD' => self::METHOD_GET,
|
||||
'GET' => self::METHOD_GET,
|
||||
'POST' => self::METHOD_POST,
|
||||
'PUT' => self::METHOD_PUT,
|
||||
'PATCH' => self::METHOD_PATCH,
|
||||
'DELETE' => self::METHOD_DELETE,
|
||||
);
|
||||
|
||||
/**
|
||||
* Requested path (relative to the API root, wp-json.php)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $path = '';
|
||||
|
||||
/**
|
||||
* Requested method (GET/HEAD/POST/PUT/PATCH/DELETE)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $method = 'HEAD';
|
||||
|
||||
/**
|
||||
* Request parameters
|
||||
*
|
||||
* This acts as an abstraction of the superglobals
|
||||
* (GET => $_GET, POST => $_POST)
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $params = array( 'GET' => array(), 'POST' => array() );
|
||||
|
||||
/**
|
||||
* Request headers
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $headers = array();
|
||||
|
||||
/**
|
||||
* Request files (matches $_FILES)
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $files = array();
|
||||
|
||||
/**
|
||||
* Request/Response handler, either JSON by default
|
||||
* or XML if requested by client
|
||||
*
|
||||
* @var WC_API_Handler
|
||||
*/
|
||||
public $handler;
|
||||
|
||||
|
||||
/**
|
||||
* Setup class and set request/response handler
|
||||
*
|
||||
* @since 2.1
|
||||
* @param $path
|
||||
*/
|
||||
public function __construct( $path ) {
|
||||
|
||||
if ( empty( $path ) ) {
|
||||
if ( isset( $_SERVER['PATH_INFO'] ) ) {
|
||||
$path = $_SERVER['PATH_INFO'];
|
||||
} else {
|
||||
$path = '/';
|
||||
}
|
||||
}
|
||||
|
||||
$this->path = $path;
|
||||
$this->method = $_SERVER['REQUEST_METHOD'];
|
||||
$this->params['GET'] = $_GET;
|
||||
$this->params['POST'] = $_POST;
|
||||
$this->headers = $this->get_headers( $_SERVER );
|
||||
$this->files = $_FILES;
|
||||
|
||||
// Compatibility for clients that can't use PUT/PATCH/DELETE
|
||||
if ( isset( $_GET['_method'] ) ) {
|
||||
$this->method = strtoupper( $_GET['_method'] );
|
||||
} elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
|
||||
$this->method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
|
||||
}
|
||||
|
||||
// determine type of request/response and load handler, JSON by default
|
||||
if ( $this->is_json_request() ) {
|
||||
$handler_class = 'WC_API_JSON_Handler';
|
||||
} elseif ( $this->is_xml_request() ) {
|
||||
$handler_class = 'WC_API_XML_Handler';
|
||||
} else {
|
||||
$handler_class = apply_filters( 'woocommerce_api_default_response_handler', 'WC_API_JSON_Handler', $this->path, $this );
|
||||
}
|
||||
|
||||
$this->handler = new $handler_class();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check authentication for the request
|
||||
*
|
||||
* @since 2.1
|
||||
* @return WP_User|WP_Error WP_User object indicates successful login, WP_Error indicates unsuccessful login
|
||||
*/
|
||||
public function check_authentication() {
|
||||
|
||||
// allow plugins to remove default authentication or add their own authentication
|
||||
$user = apply_filters( 'woocommerce_api_check_authentication', null, $this );
|
||||
|
||||
// API requests run under the context of the authenticated user
|
||||
if ( is_a( $user, 'WP_User' ) ) {
|
||||
wp_set_current_user( $user->ID );
|
||||
} elseif ( ! is_wp_error( $user ) ) {
|
||||
// WP_Errors are handled in serve_request()
|
||||
$user = new WP_Error( 'woocommerce_api_authentication_error', __( 'Invalid authentication method', 'woocommerce' ), array( 'code' => 500 ) );
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an error to an array
|
||||
*
|
||||
* This iterates over all error codes and messages to change it into a flat
|
||||
* array. This enables simpler client behaviour, as it is represented as a
|
||||
* list in JSON rather than an object/map
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WP_Error $error
|
||||
* @return array List of associative arrays with code and message keys
|
||||
*/
|
||||
protected function error_to_array( $error ) {
|
||||
$errors = array();
|
||||
foreach ( (array) $error->errors as $code => $messages ) {
|
||||
foreach ( (array) $messages as $message ) {
|
||||
$errors[] = array( 'code' => $code, 'message' => $message );
|
||||
}
|
||||
}
|
||||
return array( 'errors' => $errors );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle serving an API request
|
||||
*
|
||||
* Matches the current server URI to a route and runs the first matching
|
||||
* callback then outputs a JSON representation of the returned value.
|
||||
*
|
||||
* @since 2.1
|
||||
* @uses WC_API_Server::dispatch()
|
||||
*/
|
||||
public function serve_request() {
|
||||
|
||||
do_action( 'woocommerce_api_server_before_serve', $this );
|
||||
|
||||
$this->header( 'Content-Type', $this->handler->get_content_type(), true );
|
||||
|
||||
// the API is enabled by default
|
||||
if ( ! apply_filters( 'woocommerce_api_enabled', true, $this ) || ( 'no' === get_option( 'woocommerce_api_enabled' ) ) ) {
|
||||
|
||||
$this->send_status( 404 );
|
||||
|
||||
echo $this->handler->generate_response( array( 'errors' => array( 'code' => 'woocommerce_api_disabled', 'message' => 'The WooCommerce API is disabled on this site' ) ) );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->check_authentication();
|
||||
|
||||
// if authorization check was successful, dispatch the request
|
||||
if ( ! is_wp_error( $result ) ) {
|
||||
$result = $this->dispatch();
|
||||
}
|
||||
|
||||
// handle any dispatch errors
|
||||
if ( is_wp_error( $result ) ) {
|
||||
$data = $result->get_error_data();
|
||||
if ( is_array( $data ) && isset( $data['status'] ) ) {
|
||||
$this->send_status( $data['status'] );
|
||||
}
|
||||
|
||||
$result = $this->error_to_array( $result );
|
||||
}
|
||||
|
||||
// This is a filter rather than an action, since this is designed to be
|
||||
// re-entrant if needed
|
||||
$served = apply_filters( 'woocommerce_api_serve_request', false, $result, $this );
|
||||
|
||||
if ( ! $served ) {
|
||||
|
||||
if ( 'HEAD' === $this->method ) {
|
||||
return;
|
||||
}
|
||||
|
||||
echo $this->handler->generate_response( $result );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the route map
|
||||
*
|
||||
* The route map is an associative array with path regexes as the keys. The
|
||||
* value is an indexed array with the callback function/method as the first
|
||||
* item, and a bitmask of HTTP methods as the second item (see the class
|
||||
* constants).
|
||||
*
|
||||
* Each route can be mapped to more than one callback by using an array of
|
||||
* the indexed arrays. This allows mapping e.g. GET requests to one callback
|
||||
* and POST requests to another.
|
||||
*
|
||||
* Note that the path regexes (array keys) must have @ escaped, as this is
|
||||
* used as the delimiter with preg_match()
|
||||
*
|
||||
* @since 2.1
|
||||
* @return array `'/path/regex' => array( $callback, $bitmask )` or `'/path/regex' => array( array( $callback, $bitmask ), ...)`
|
||||
*/
|
||||
public function get_routes() {
|
||||
|
||||
// index added by default
|
||||
$endpoints = array(
|
||||
|
||||
'/' => array( array( $this, 'get_index' ), self::READABLE ),
|
||||
);
|
||||
|
||||
$endpoints = apply_filters( 'woocommerce_api_endpoints', $endpoints );
|
||||
|
||||
// Normalise the endpoints
|
||||
foreach ( $endpoints as $route => &$handlers ) {
|
||||
if ( count( $handlers ) <= 2 && isset( $handlers[1] ) && ! is_array( $handlers[1] ) ) {
|
||||
$handlers = array( $handlers );
|
||||
}
|
||||
}
|
||||
|
||||
return $endpoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match the request to a callback and call it
|
||||
*
|
||||
* @since 2.1
|
||||
* @return mixed The value returned by the callback, or a WP_Error instance
|
||||
*/
|
||||
public function dispatch() {
|
||||
|
||||
switch ( $this->method ) {
|
||||
|
||||
case 'HEAD':
|
||||
case 'GET':
|
||||
$method = self::METHOD_GET;
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
$method = self::METHOD_POST;
|
||||
break;
|
||||
|
||||
case 'PUT':
|
||||
$method = self::METHOD_PUT;
|
||||
break;
|
||||
|
||||
case 'PATCH':
|
||||
$method = self::METHOD_PATCH;
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
$method = self::METHOD_DELETE;
|
||||
break;
|
||||
|
||||
default:
|
||||
return new WP_Error( 'woocommerce_api_unsupported_method', __( 'Unsupported request method', 'woocommerce' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
foreach ( $this->get_routes() as $route => $handlers ) {
|
||||
foreach ( $handlers as $handler ) {
|
||||
$callback = $handler[0];
|
||||
$supported = isset( $handler[1] ) ? $handler[1] : self::METHOD_GET;
|
||||
|
||||
if ( ! ( $supported & $method ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$match = preg_match( '@^' . $route . '$@i', urldecode( $this->path ), $args );
|
||||
|
||||
if ( ! $match ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! is_callable( $callback ) ) {
|
||||
return new WP_Error( 'woocommerce_api_invalid_handler', __( 'The handler for the route is invalid', 'woocommerce' ), array( 'status' => 500 ) );
|
||||
}
|
||||
|
||||
$args = array_merge( $args, $this->params['GET'] );
|
||||
if ( $method & self::METHOD_POST ) {
|
||||
$args = array_merge( $args, $this->params['POST'] );
|
||||
}
|
||||
if ( $supported & self::ACCEPT_DATA ) {
|
||||
$data = $this->handler->parse_body( $this->get_raw_data() );
|
||||
$args = array_merge( $args, array( 'data' => $data ) );
|
||||
} elseif ( $supported & self::ACCEPT_RAW_DATA ) {
|
||||
$data = $this->get_raw_data();
|
||||
$args = array_merge( $args, array( 'data' => $data ) );
|
||||
}
|
||||
|
||||
$args['_method'] = $method;
|
||||
$args['_route'] = $route;
|
||||
$args['_path'] = $this->path;
|
||||
$args['_headers'] = $this->headers;
|
||||
$args['_files'] = $this->files;
|
||||
|
||||
$args = apply_filters( 'woocommerce_api_dispatch_args', $args, $callback );
|
||||
|
||||
// Allow plugins to halt the request via this filter
|
||||
if ( is_wp_error( $args ) ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
$params = $this->sort_callback_params( $callback, $args );
|
||||
if ( is_wp_error( $params ) ) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
return call_user_func_array( $callback, $params );
|
||||
}
|
||||
}
|
||||
|
||||
return new WP_Error( 'woocommerce_api_no_route', __( 'No route was found matching the URL and request method', 'woocommerce' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort parameters by order specified in method declaration
|
||||
*
|
||||
* Takes a callback and a list of available params, then filters and sorts
|
||||
* by the parameters the method actually needs, using the Reflection API
|
||||
*
|
||||
* @since 2.1
|
||||
*
|
||||
* @param callable|array $callback the endpoint callback
|
||||
* @param array $provided the provided request parameters
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
protected function sort_callback_params( $callback, $provided ) {
|
||||
if ( is_array( $callback ) ) {
|
||||
$ref_func = new ReflectionMethod( $callback[0], $callback[1] );
|
||||
} else {
|
||||
$ref_func = new ReflectionFunction( $callback );
|
||||
}
|
||||
|
||||
$wanted = $ref_func->getParameters();
|
||||
$ordered_parameters = array();
|
||||
|
||||
foreach ( $wanted as $param ) {
|
||||
if ( isset( $provided[ $param->getName() ] ) ) {
|
||||
// We have this parameters in the list to choose from
|
||||
$ordered_parameters[] = is_array( $provided[ $param->getName() ] ) ? array_map( 'urldecode', $provided[ $param->getName() ] ) : urldecode( $provided[ $param->getName() ] );
|
||||
} elseif ( $param->isDefaultValueAvailable() ) {
|
||||
// We don't have this parameter, but it's optional
|
||||
$ordered_parameters[] = $param->getDefaultValue();
|
||||
} else {
|
||||
// We don't have this parameter and it wasn't optional, abort!
|
||||
return new WP_Error( 'woocommerce_api_missing_callback_param', sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param->getName() ), array( 'status' => 400 ) );
|
||||
}
|
||||
}
|
||||
return $ordered_parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the site index.
|
||||
*
|
||||
* This endpoint describes the capabilities of the site.
|
||||
*
|
||||
* @since 2.1
|
||||
* @return array Index entity
|
||||
*/
|
||||
public function get_index() {
|
||||
|
||||
// General site data
|
||||
$available = array(
|
||||
'store' => array(
|
||||
'name' => get_option( 'blogname' ),
|
||||
'description' => get_option( 'blogdescription' ),
|
||||
'URL' => get_option( 'siteurl' ),
|
||||
'wc_version' => WC()->version,
|
||||
'routes' => array(),
|
||||
'meta' => array(
|
||||
'timezone' => wc_timezone_string(),
|
||||
'currency' => get_woocommerce_currency(),
|
||||
'currency_format' => get_woocommerce_currency_symbol(),
|
||||
'tax_included' => wc_prices_include_tax(),
|
||||
'weight_unit' => get_option( 'woocommerce_weight_unit' ),
|
||||
'dimension_unit' => get_option( 'woocommerce_dimension_unit' ),
|
||||
'ssl_enabled' => ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ),
|
||||
'permalinks_enabled' => ( '' !== get_option( 'permalink_structure' ) ),
|
||||
'links' => array(
|
||||
'help' => 'https://woocommerce.github.io/woocommerce/rest-api/',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Find the available routes
|
||||
foreach ( $this->get_routes() as $route => $callbacks ) {
|
||||
$data = array();
|
||||
|
||||
$route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route );
|
||||
$methods = array();
|
||||
foreach ( self::$method_map as $name => $bitmask ) {
|
||||
foreach ( $callbacks as $callback ) {
|
||||
// Skip to the next route if any callback is hidden
|
||||
if ( $callback[1] & self::HIDDEN_ENDPOINT ) {
|
||||
continue 3;
|
||||
}
|
||||
|
||||
if ( $callback[1] & $bitmask ) {
|
||||
$data['supports'][] = $name;
|
||||
}
|
||||
|
||||
if ( $callback[1] & self::ACCEPT_DATA ) {
|
||||
$data['accepts_data'] = true;
|
||||
}
|
||||
|
||||
// For non-variable routes, generate links
|
||||
if ( strpos( $route, '<' ) === false ) {
|
||||
$data['meta'] = array(
|
||||
'self' => get_woocommerce_api_url( $route ),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
$available['store']['routes'][ $route ] = apply_filters( 'woocommerce_api_endpoints_description', $data );
|
||||
}
|
||||
return apply_filters( 'woocommerce_api_index', $available );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a HTTP status code
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int $code HTTP status
|
||||
*/
|
||||
public function send_status( $code ) {
|
||||
status_header( $code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a HTTP header
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $key Header key
|
||||
* @param string $value Header value
|
||||
* @param boolean $replace Should we replace the existing header?
|
||||
*/
|
||||
public function header( $key, $value, $replace = true ) {
|
||||
header( sprintf( '%s: %s', $key, $value ), $replace );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a Link header
|
||||
*
|
||||
* @internal The $rel parameter is first, as this looks nicer when sending multiple
|
||||
*
|
||||
* @link http://tools.ietf.org/html/rfc5988
|
||||
* @link http://www.iana.org/assignments/link-relations/link-relations.xml
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $rel Link relation. Either a registered type, or an absolute URL
|
||||
* @param string $link Target IRI for the link
|
||||
* @param array $other Other parameters to send, as an associative array
|
||||
*/
|
||||
public function link_header( $rel, $link, $other = array() ) {
|
||||
|
||||
$header = sprintf( '<%s>; rel="%s"', $link, esc_attr( $rel ) );
|
||||
|
||||
foreach ( $other as $key => $value ) {
|
||||
|
||||
if ( 'title' == $key ) {
|
||||
|
||||
$value = '"' . $value . '"';
|
||||
}
|
||||
|
||||
$header .= '; ' . $key . '=' . $value;
|
||||
}
|
||||
|
||||
$this->header( 'Link', $header, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send pagination headers for resources
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WP_Query|WP_User_Query $query
|
||||
*/
|
||||
public function add_pagination_headers( $query ) {
|
||||
|
||||
// WP_User_Query
|
||||
if ( is_a( $query, 'WP_User_Query' ) ) {
|
||||
|
||||
$page = $query->page;
|
||||
$single = count( $query->get_results() ) == 1;
|
||||
$total = $query->get_total();
|
||||
$total_pages = $query->total_pages;
|
||||
|
||||
// WP_Query
|
||||
} else {
|
||||
|
||||
$page = $query->get( 'paged' );
|
||||
$single = $query->is_single();
|
||||
$total = $query->found_posts;
|
||||
$total_pages = $query->max_num_pages;
|
||||
}
|
||||
|
||||
if ( ! $page ) {
|
||||
$page = 1;
|
||||
}
|
||||
|
||||
$next_page = absint( $page ) + 1;
|
||||
|
||||
if ( ! $single ) {
|
||||
|
||||
// first/prev
|
||||
if ( $page > 1 ) {
|
||||
$this->link_header( 'first', $this->get_paginated_url( 1 ) );
|
||||
$this->link_header( 'prev', $this->get_paginated_url( $page -1 ) );
|
||||
}
|
||||
|
||||
// next
|
||||
if ( $next_page <= $total_pages ) {
|
||||
$this->link_header( 'next', $this->get_paginated_url( $next_page ) );
|
||||
}
|
||||
|
||||
// last
|
||||
if ( $page != $total_pages ) {
|
||||
$this->link_header( 'last', $this->get_paginated_url( $total_pages ) );
|
||||
}
|
||||
}
|
||||
|
||||
$this->header( 'X-WC-Total', $total );
|
||||
$this->header( 'X-WC-TotalPages', $total_pages );
|
||||
|
||||
do_action( 'woocommerce_api_pagination_headers', $this, $query );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request URL with the page query parameter set to the specified page
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int $page
|
||||
* @return string
|
||||
*/
|
||||
private function get_paginated_url( $page ) {
|
||||
|
||||
// remove existing page query param
|
||||
$request = remove_query_arg( 'page' );
|
||||
|
||||
// add provided page query param
|
||||
$request = urldecode( add_query_arg( 'page', $page, $request ) );
|
||||
|
||||
// get the home host
|
||||
$host = parse_url( get_home_url(), PHP_URL_HOST );
|
||||
|
||||
return set_url_scheme( "http://{$host}{$request}" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the raw request entity (body)
|
||||
*
|
||||
* @since 2.1
|
||||
* @return string
|
||||
*/
|
||||
public function get_raw_data() {
|
||||
// @codingStandardsIgnoreStart
|
||||
// $HTTP_RAW_POST_DATA is deprecated on PHP 5.6.
|
||||
if ( function_exists( 'phpversion' ) && version_compare( phpversion(), '5.6', '>=' ) ) {
|
||||
return file_get_contents( 'php://input' );
|
||||
}
|
||||
|
||||
global $HTTP_RAW_POST_DATA;
|
||||
|
||||
// A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default,
|
||||
// but we can do it ourself.
|
||||
if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
|
||||
$HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
|
||||
}
|
||||
|
||||
return $HTTP_RAW_POST_DATA;
|
||||
// @codingStandardsIgnoreEnd
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an RFC3339 datetime into a MySQl datetime
|
||||
*
|
||||
* Invalid dates default to unix epoch
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $datetime RFC3339 datetime
|
||||
* @return string MySQl datetime (YYYY-MM-DD HH:MM:SS)
|
||||
*/
|
||||
public function parse_datetime( $datetime ) {
|
||||
|
||||
// Strip millisecond precision (a full stop followed by one or more digits)
|
||||
if ( strpos( $datetime, '.' ) !== false ) {
|
||||
$datetime = preg_replace( '/\.\d+/', '', $datetime );
|
||||
}
|
||||
|
||||
// default timezone to UTC
|
||||
$datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime );
|
||||
|
||||
try {
|
||||
|
||||
$datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) );
|
||||
|
||||
} catch ( Exception $e ) {
|
||||
|
||||
$datetime = new DateTime( '@0' );
|
||||
|
||||
}
|
||||
|
||||
return $datetime->format( 'Y-m-d H:i:s' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a unix timestamp or MySQL datetime into an RFC3339 datetime
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int|string $timestamp unix timestamp or MySQL datetime
|
||||
* @param bool $convert_to_utc
|
||||
* @param bool $convert_to_gmt Use GMT timezone.
|
||||
* @return string RFC3339 datetime
|
||||
*/
|
||||
public function format_datetime( $timestamp, $convert_to_utc = false, $convert_to_gmt = false ) {
|
||||
if ( $convert_to_gmt ) {
|
||||
if ( is_numeric( $timestamp ) ) {
|
||||
$timestamp = date( 'Y-m-d H:i:s', $timestamp );
|
||||
}
|
||||
|
||||
$timestamp = get_gmt_from_date( $timestamp );
|
||||
}
|
||||
|
||||
if ( $convert_to_utc ) {
|
||||
$timezone = new DateTimeZone( wc_timezone_string() );
|
||||
} else {
|
||||
$timezone = new DateTimeZone( 'UTC' );
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
if ( is_numeric( $timestamp ) ) {
|
||||
$date = new DateTime( "@{$timestamp}" );
|
||||
} else {
|
||||
$date = new DateTime( $timestamp, $timezone );
|
||||
}
|
||||
|
||||
// convert to UTC by adjusting the time based on the offset of the site's timezone
|
||||
if ( $convert_to_utc ) {
|
||||
$date->modify( -1 * $date->getOffset() . ' seconds' );
|
||||
}
|
||||
} catch ( Exception $e ) {
|
||||
|
||||
$date = new DateTime( '@0' );
|
||||
}
|
||||
|
||||
return $date->format( 'Y-m-d\TH:i:s\Z' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract headers from a PHP-style $_SERVER array
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $server Associative array similar to $_SERVER
|
||||
* @return array Headers extracted from the input
|
||||
*/
|
||||
public function get_headers( $server ) {
|
||||
$headers = array();
|
||||
// CONTENT_* headers are not prefixed with HTTP_
|
||||
$additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true );
|
||||
|
||||
foreach ( $server as $key => $value ) {
|
||||
if ( strpos( $key, 'HTTP_' ) === 0 ) {
|
||||
$headers[ substr( $key, 5 ) ] = $value;
|
||||
} elseif ( isset( $additional[ $key ] ) ) {
|
||||
$headers[ $key ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current request accepts a JSON response by checking the endpoint suffix (.json) or
|
||||
* the HTTP ACCEPT header
|
||||
*
|
||||
* @since 2.1
|
||||
* @return bool
|
||||
*/
|
||||
private function is_json_request() {
|
||||
|
||||
// check path
|
||||
if ( false !== stripos( $this->path, '.json' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// check ACCEPT header, only 'application/json' is acceptable, see RFC 4627
|
||||
if ( isset( $this->headers['ACCEPT'] ) && 'application/json' == $this->headers['ACCEPT'] ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current request accepts an XML response by checking the endpoint suffix (.xml) or
|
||||
* the HTTP ACCEPT header
|
||||
*
|
||||
* @since 2.1
|
||||
* @return bool
|
||||
*/
|
||||
private function is_xml_request() {
|
||||
|
||||
// check path
|
||||
if ( false !== stripos( $this->path, '.xml' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// check headers, 'application/xml' or 'text/xml' are acceptable, see RFC 2376
|
||||
if ( isset( $this->headers['ACCEPT'] ) && ( 'application/xml' == $this->headers['ACCEPT'] || 'text/xml' == $this->headers['ACCEPT'] ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,308 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API
|
||||
*
|
||||
* Handles parsing XML request bodies and generating XML responses
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1
|
||||
* @version 2.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_XML_Handler implements WC_API_Handler {
|
||||
|
||||
/** @var XMLWriter instance */
|
||||
private $xml;
|
||||
|
||||
/**
|
||||
* Add some response filters
|
||||
*
|
||||
* @since 2.1
|
||||
*/
|
||||
public function __construct() {
|
||||
|
||||
// tweak sales report response data
|
||||
add_filter( 'woocommerce_api_report_response', array( $this, 'format_sales_report_data' ), 100 );
|
||||
|
||||
// tweak product response data
|
||||
add_filter( 'woocommerce_api_product_response', array( $this, 'format_product_data' ), 100 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content type for the response
|
||||
*
|
||||
* @since 2.1
|
||||
* @return string
|
||||
*/
|
||||
public function get_content_type() {
|
||||
|
||||
return 'application/xml; charset=' . get_option( 'blog_charset' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the raw request body entity
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $data the raw request body
|
||||
* @return array
|
||||
*/
|
||||
public function parse_body( $data ) {
|
||||
|
||||
// TODO: implement simpleXML parsing
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an XML response given an array of data
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $data the response data
|
||||
* @return string
|
||||
*/
|
||||
public function generate_response( $data ) {
|
||||
|
||||
$this->xml = new XMLWriter();
|
||||
|
||||
$this->xml->openMemory();
|
||||
|
||||
$this->xml->setIndent( true );
|
||||
|
||||
$this->xml->startDocument( '1.0', 'UTF-8' );
|
||||
|
||||
$root_element = key( $data );
|
||||
|
||||
$data = $data[ $root_element ];
|
||||
|
||||
switch ( $root_element ) {
|
||||
|
||||
case 'orders':
|
||||
$data = array( 'order' => $data );
|
||||
break;
|
||||
|
||||
case 'order_notes':
|
||||
$data = array( 'order_note' => $data );
|
||||
break;
|
||||
|
||||
case 'customers':
|
||||
$data = array( 'customer' => $data );
|
||||
break;
|
||||
|
||||
case 'coupons':
|
||||
$data = array( 'coupon' => $data );
|
||||
break;
|
||||
|
||||
case 'products':
|
||||
$data = array( 'product' => $data );
|
||||
break;
|
||||
|
||||
case 'product_reviews':
|
||||
$data = array( 'product_review' => $data );
|
||||
break;
|
||||
|
||||
default:
|
||||
$data = apply_filters( 'woocommerce_api_xml_data', $data, $root_element );
|
||||
break;
|
||||
}
|
||||
|
||||
// generate xml starting with the root element and recursively generating child elements
|
||||
$this->array_to_xml( $root_element, $data );
|
||||
|
||||
$this->xml->endDocument();
|
||||
|
||||
return $this->xml->outputMemory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert array into XML by recursively generating child elements
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string|array $element_key - name for element, e.g. <OrderID>
|
||||
* @param string|array $element_value - value for element, e.g. 1234
|
||||
* @return string - generated XML
|
||||
*/
|
||||
private function array_to_xml( $element_key, $element_value = array() ) {
|
||||
|
||||
if ( is_array( $element_value ) ) {
|
||||
|
||||
// handle attributes
|
||||
if ( '@attributes' === $element_key ) {
|
||||
foreach ( $element_value as $attribute_key => $attribute_value ) {
|
||||
|
||||
$this->xml->startAttribute( $attribute_key );
|
||||
$this->xml->text( $attribute_value );
|
||||
$this->xml->endAttribute();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// handle multi-elements (e.g. multiple <Order> elements)
|
||||
if ( is_numeric( key( $element_value ) ) ) {
|
||||
|
||||
// recursively generate child elements
|
||||
foreach ( $element_value as $child_element_key => $child_element_value ) {
|
||||
|
||||
$this->xml->startElement( $element_key );
|
||||
|
||||
foreach ( $child_element_value as $sibling_element_key => $sibling_element_value ) {
|
||||
$this->array_to_xml( $sibling_element_key, $sibling_element_value );
|
||||
}
|
||||
|
||||
$this->xml->endElement();
|
||||
}
|
||||
} else {
|
||||
|
||||
// start root element
|
||||
$this->xml->startElement( $element_key );
|
||||
|
||||
// recursively generate child elements
|
||||
foreach ( $element_value as $child_element_key => $child_element_value ) {
|
||||
$this->array_to_xml( $child_element_key, $child_element_value );
|
||||
}
|
||||
|
||||
// end root element
|
||||
$this->xml->endElement();
|
||||
}
|
||||
} else {
|
||||
|
||||
// handle single elements
|
||||
if ( '@value' == $element_key ) {
|
||||
|
||||
$this->xml->text( $element_value );
|
||||
|
||||
} else {
|
||||
|
||||
// wrap element in CDATA tags if it contains illegal characters
|
||||
if ( false !== strpos( $element_value, '<' ) || false !== strpos( $element_value, '>' ) ) {
|
||||
|
||||
$this->xml->startElement( $element_key );
|
||||
$this->xml->writeCdata( $element_value );
|
||||
$this->xml->endElement();
|
||||
|
||||
} else {
|
||||
|
||||
$this->xml->writeElement( $element_key, $element_value );
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust the sales report array format to change totals keyed with the sales date to become an
|
||||
* attribute for the totals element instead
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function format_sales_report_data( $data ) {
|
||||
|
||||
if ( ! empty( $data['totals'] ) ) {
|
||||
|
||||
foreach ( $data['totals'] as $date => $totals ) {
|
||||
|
||||
unset( $data['totals'][ $date ] );
|
||||
|
||||
$data['totals'][] = array_merge( array( '@attributes' => array( 'date' => $date ) ), $totals );
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust the product data to handle options for attributes without a named child element and other
|
||||
* fields that have no named child elements (e.g. categories = array( 'cat1', 'cat2' ) )
|
||||
*
|
||||
* Note that the parent product data for variations is also adjusted in the same manner as needed
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function format_product_data( $data ) {
|
||||
|
||||
// handle attribute values
|
||||
if ( ! empty( $data['attributes'] ) ) {
|
||||
|
||||
foreach ( $data['attributes'] as $attribute_key => $attribute ) {
|
||||
|
||||
if ( ! empty( $attribute['options'] ) && is_array( $attribute['options'] ) ) {
|
||||
|
||||
foreach ( $attribute['options'] as $option_key => $option ) {
|
||||
|
||||
unset( $data['attributes'][ $attribute_key ]['options'][ $option_key ] );
|
||||
|
||||
$data['attributes'][ $attribute_key ]['options']['option'][] = array( $option );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// simple arrays are fine for JSON, but XML requires a child element name, so this adjusts the data
|
||||
// array to define a child element name for each field
|
||||
$fields_to_fix = array(
|
||||
'related_ids' => 'related_id',
|
||||
'upsell_ids' => 'upsell_id',
|
||||
'cross_sell_ids' => 'cross_sell_id',
|
||||
'categories' => 'category',
|
||||
'tags' => 'tag',
|
||||
);
|
||||
|
||||
foreach ( $fields_to_fix as $parent_field_name => $child_field_name ) {
|
||||
|
||||
if ( ! empty( $data[ $parent_field_name ] ) ) {
|
||||
|
||||
foreach ( $data[ $parent_field_name ] as $field_key => $field ) {
|
||||
|
||||
unset( $data[ $parent_field_name ][ $field_key ] );
|
||||
|
||||
$data[ $parent_field_name ][ $child_field_name ][] = array( $field );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handle adjusting the parent product for variations
|
||||
if ( ! empty( $data['parent'] ) ) {
|
||||
|
||||
// attributes
|
||||
if ( ! empty( $data['parent']['attributes'] ) ) {
|
||||
|
||||
foreach ( $data['parent']['attributes'] as $attribute_key => $attribute ) {
|
||||
|
||||
if ( ! empty( $attribute['options'] ) && is_array( $attribute['options'] ) ) {
|
||||
|
||||
foreach ( $attribute['options'] as $option_key => $option ) {
|
||||
|
||||
unset( $data['parent']['attributes'][ $attribute_key ]['options'][ $option_key ] );
|
||||
|
||||
$data['parent']['attributes'][ $attribute_key ]['options']['option'][] = array( $option );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fields
|
||||
foreach ( $fields_to_fix as $parent_field_name => $child_field_name ) {
|
||||
|
||||
if ( ! empty( $data['parent'][ $parent_field_name ] ) ) {
|
||||
|
||||
foreach ( $data['parent'][ $parent_field_name ] as $field_key => $field ) {
|
||||
|
||||
unset( $data['parent'][ $parent_field_name ][ $field_key ] );
|
||||
|
||||
$data['parent'][ $parent_field_name ][ $child_field_name ][] = array( $field );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API
|
||||
*
|
||||
* Defines an interface that API request/response handlers should implement
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1
|
||||
* @version 2.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
interface WC_API_Handler {
|
||||
|
||||
/**
|
||||
* Get the content type for the response
|
||||
*
|
||||
* This should return the proper HTTP content-type for the response
|
||||
*
|
||||
* @since 2.1
|
||||
* @return string
|
||||
*/
|
||||
public function get_content_type();
|
||||
|
||||
/**
|
||||
* Parse the raw request body entity into an array
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $data
|
||||
* @return array
|
||||
*/
|
||||
public function parse_body( $data );
|
||||
|
||||
/**
|
||||
* Generate a response from an array of data
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $data
|
||||
* @return string
|
||||
*/
|
||||
public function generate_response( $data );
|
||||
|
||||
}
|
|
@ -0,0 +1,408 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API Authentication Class
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1.0
|
||||
* @version 2.4.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_Authentication {
|
||||
|
||||
/**
|
||||
* Setup class
|
||||
*
|
||||
* @since 2.1
|
||||
*/
|
||||
public function __construct() {
|
||||
|
||||
// To disable authentication, hook into this filter at a later priority and return a valid WP_User
|
||||
add_filter( 'woocommerce_api_check_authentication', array( $this, 'authenticate' ), 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the request. The authentication method varies based on whether the request was made over SSL or not.
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WP_User $user
|
||||
* @return null|WP_Error|WP_User
|
||||
*/
|
||||
public function authenticate( $user ) {
|
||||
|
||||
// Allow access to the index by default
|
||||
if ( '/' === WC()->api->server->path ) {
|
||||
return new WP_User( 0 );
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
if ( is_ssl() ) {
|
||||
$keys = $this->perform_ssl_authentication();
|
||||
} else {
|
||||
$keys = $this->perform_oauth_authentication();
|
||||
}
|
||||
|
||||
// Check API key-specific permission
|
||||
$this->check_api_key_permissions( $keys['permissions'] );
|
||||
|
||||
$user = $this->get_user_by_id( $keys['user_id'] );
|
||||
|
||||
$this->update_api_key_last_access( $keys['key_id'] );
|
||||
|
||||
} catch ( Exception $e ) {
|
||||
$user = new WP_Error( 'woocommerce_api_authentication_error', $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* SSL-encrypted requests are not subject to sniffing or man-in-the-middle
|
||||
* attacks, so the request can be authenticated by simply looking up the user
|
||||
* associated with the given consumer key and confirming the consumer secret
|
||||
* provided is valid
|
||||
*
|
||||
* @since 2.1
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
private function perform_ssl_authentication() {
|
||||
|
||||
$params = WC()->api->server->params['GET'];
|
||||
|
||||
// Get consumer key
|
||||
if ( ! empty( $_SERVER['PHP_AUTH_USER'] ) ) {
|
||||
|
||||
// Should be in HTTP Auth header by default
|
||||
$consumer_key = $_SERVER['PHP_AUTH_USER'];
|
||||
|
||||
} elseif ( ! empty( $params['consumer_key'] ) ) {
|
||||
|
||||
// Allow a query string parameter as a fallback
|
||||
$consumer_key = $params['consumer_key'];
|
||||
|
||||
} else {
|
||||
|
||||
throw new Exception( __( 'Consumer key is missing.', 'woocommerce' ), 404 );
|
||||
}
|
||||
|
||||
// Get consumer secret
|
||||
if ( ! empty( $_SERVER['PHP_AUTH_PW'] ) ) {
|
||||
|
||||
// Should be in HTTP Auth header by default
|
||||
$consumer_secret = $_SERVER['PHP_AUTH_PW'];
|
||||
|
||||
} elseif ( ! empty( $params['consumer_secret'] ) ) {
|
||||
|
||||
// Allow a query string parameter as a fallback
|
||||
$consumer_secret = $params['consumer_secret'];
|
||||
|
||||
} else {
|
||||
|
||||
throw new Exception( __( 'Consumer secret is missing.', 'woocommerce' ), 404 );
|
||||
}
|
||||
|
||||
$keys = $this->get_keys_by_consumer_key( $consumer_key );
|
||||
|
||||
if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $consumer_secret ) ) {
|
||||
throw new Exception( __( 'Consumer secret is invalid.', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests
|
||||
*
|
||||
* This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP
|
||||
*
|
||||
* This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions:
|
||||
*
|
||||
* 1) There is no token associated with request/responses, only consumer keys/secrets are used
|
||||
*
|
||||
* 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header,
|
||||
* This is because there is no cross-OS function within PHP to get the raw Authorization header
|
||||
*
|
||||
* @link http://tools.ietf.org/html/rfc5849 for the full spec
|
||||
* @since 2.1
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
private function perform_oauth_authentication() {
|
||||
|
||||
$params = WC()->api->server->params['GET'];
|
||||
|
||||
$param_names = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' );
|
||||
|
||||
// Check for required OAuth parameters
|
||||
foreach ( $param_names as $param_name ) {
|
||||
|
||||
if ( empty( $params[ $param_name ] ) ) {
|
||||
throw new Exception( sprintf( __( '%s parameter is missing', 'woocommerce' ), $param_name ), 404 );
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch WP user by consumer key
|
||||
$keys = $this->get_keys_by_consumer_key( $params['oauth_consumer_key'] );
|
||||
|
||||
// Perform OAuth validation
|
||||
$this->check_oauth_signature( $keys, $params );
|
||||
$this->check_oauth_timestamp_and_nonce( $keys, $params['oauth_timestamp'], $params['oauth_nonce'] );
|
||||
|
||||
// Authentication successful, return user
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the keys for the given consumer key
|
||||
*
|
||||
* @since 2.4.0
|
||||
* @param string $consumer_key
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
private function get_keys_by_consumer_key( $consumer_key ) {
|
||||
global $wpdb;
|
||||
|
||||
$consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) );
|
||||
|
||||
$keys = $wpdb->get_row( $wpdb->prepare( "
|
||||
SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces
|
||||
FROM {$wpdb->prefix}woocommerce_api_keys
|
||||
WHERE consumer_key = '%s'
|
||||
", $consumer_key ), ARRAY_A );
|
||||
|
||||
if ( empty( $keys ) ) {
|
||||
throw new Exception( __( 'Consumer key is invalid.', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user by ID
|
||||
*
|
||||
* @since 2.4.0
|
||||
* @param int $user_id
|
||||
* @return WP_User
|
||||
* @throws Exception
|
||||
*/
|
||||
private function get_user_by_id( $user_id ) {
|
||||
$user = get_user_by( 'id', $user_id );
|
||||
|
||||
if ( ! $user ) {
|
||||
throw new Exception( __( 'API user is invalid', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the consumer secret provided for the given user is valid
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $keys_consumer_secret
|
||||
* @param string $consumer_secret
|
||||
* @return bool
|
||||
*/
|
||||
private function is_consumer_secret_valid( $keys_consumer_secret, $consumer_secret ) {
|
||||
return hash_equals( $keys_consumer_secret, $consumer_secret );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the consumer-provided request signature matches our generated signature, this ensures the consumer
|
||||
* has a valid key/secret
|
||||
*
|
||||
* @param array $keys
|
||||
* @param array $params the request parameters
|
||||
* @throws Exception
|
||||
*/
|
||||
private function check_oauth_signature( $keys, $params ) {
|
||||
|
||||
$http_method = strtoupper( WC()->api->server->method );
|
||||
|
||||
$base_request_uri = rawurlencode( untrailingslashit( get_woocommerce_api_url( '' ) ) . WC()->api->server->path );
|
||||
|
||||
// Get the signature provided by the consumer and remove it from the parameters prior to checking the signature
|
||||
$consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) );
|
||||
unset( $params['oauth_signature'] );
|
||||
|
||||
// Remove filters and convert them from array to strings to void normalize issues
|
||||
if ( isset( $params['filter'] ) ) {
|
||||
$filters = $params['filter'];
|
||||
unset( $params['filter'] );
|
||||
foreach ( $filters as $filter => $filter_value ) {
|
||||
$params[ 'filter[' . $filter . ']' ] = $filter_value;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize parameter key/values
|
||||
$params = $this->normalize_parameters( $params );
|
||||
|
||||
// Sort parameters
|
||||
if ( ! uksort( $params, 'strcmp' ) ) {
|
||||
throw new Exception( __( 'Invalid signature - failed to sort parameters.', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
// Form query string
|
||||
$query_params = array();
|
||||
foreach ( $params as $param_key => $param_value ) {
|
||||
|
||||
$query_params[] = $param_key . '%3D' . $param_value; // join with equals sign
|
||||
}
|
||||
$query_string = implode( '%26', $query_params ); // join with ampersand
|
||||
|
||||
$string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string;
|
||||
|
||||
if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) {
|
||||
throw new Exception( __( 'Invalid signature - signature method is invalid.', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) );
|
||||
|
||||
$signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $keys['consumer_secret'], true ) );
|
||||
|
||||
if ( ! hash_equals( $signature, $consumer_signature ) ) {
|
||||
throw new Exception( __( 'Invalid signature - provided signature does not match.', 'woocommerce' ), 401 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize each parameter by assuming each parameter may have already been
|
||||
* encoded, so attempt to decode, and then re-encode according to RFC 3986
|
||||
*
|
||||
* Note both the key and value is normalized so a filter param like:
|
||||
*
|
||||
* 'filter[period]' => 'week'
|
||||
*
|
||||
* is encoded to:
|
||||
*
|
||||
* 'filter%5Bperiod%5D' => 'week'
|
||||
*
|
||||
* This conforms to the OAuth 1.0a spec which indicates the entire query string
|
||||
* should be URL encoded
|
||||
*
|
||||
* @since 2.1
|
||||
* @see rawurlencode()
|
||||
* @param array $parameters un-normalized parameters
|
||||
* @return array normalized parameters
|
||||
*/
|
||||
private function normalize_parameters( $parameters ) {
|
||||
|
||||
$normalized_parameters = array();
|
||||
|
||||
foreach ( $parameters as $key => $value ) {
|
||||
|
||||
// Percent symbols (%) must be double-encoded
|
||||
$key = str_replace( '%', '%25', rawurlencode( rawurldecode( $key ) ) );
|
||||
$value = str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) );
|
||||
|
||||
$normalized_parameters[ $key ] = $value;
|
||||
}
|
||||
|
||||
return $normalized_parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where
|
||||
* an attacker could attempt to re-send an intercepted request at a later time.
|
||||
*
|
||||
* - A timestamp is valid if it is within 15 minutes of now
|
||||
* - A nonce is valid if it has not been used within the last 15 minutes
|
||||
*
|
||||
* @param array $keys
|
||||
* @param int $timestamp the unix timestamp for when the request was made
|
||||
* @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated
|
||||
* @throws Exception
|
||||
*/
|
||||
private function check_oauth_timestamp_and_nonce( $keys, $timestamp, $nonce ) {
|
||||
global $wpdb;
|
||||
|
||||
$valid_window = 15 * 60; // 15 minute window
|
||||
|
||||
if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) {
|
||||
throw new Exception( __( 'Invalid timestamp.', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$used_nonces = maybe_unserialize( $keys['nonces'] );
|
||||
|
||||
if ( empty( $used_nonces ) ) {
|
||||
$used_nonces = array();
|
||||
}
|
||||
|
||||
if ( in_array( $nonce, $used_nonces ) ) {
|
||||
throw new Exception( __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$used_nonces[ $timestamp ] = $nonce;
|
||||
|
||||
// Remove expired nonces
|
||||
foreach ( $used_nonces as $nonce_timestamp => $nonce ) {
|
||||
if ( $nonce_timestamp < ( time() - $valid_window ) ) {
|
||||
unset( $used_nonces[ $nonce_timestamp ] );
|
||||
}
|
||||
}
|
||||
|
||||
$used_nonces = maybe_serialize( $used_nonces );
|
||||
|
||||
$wpdb->update(
|
||||
$wpdb->prefix . 'woocommerce_api_keys',
|
||||
array( 'nonces' => $used_nonces ),
|
||||
array( 'key_id' => $keys['key_id'] ),
|
||||
array( '%s' ),
|
||||
array( '%d' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the API keys provided have the proper key-specific permissions to either read or write API resources
|
||||
*
|
||||
* @param string $key_permissions
|
||||
* @throws Exception if the permission check fails
|
||||
*/
|
||||
public function check_api_key_permissions( $key_permissions ) {
|
||||
switch ( WC()->api->server->method ) {
|
||||
|
||||
case 'HEAD':
|
||||
case 'GET':
|
||||
if ( 'read' !== $key_permissions && 'read_write' !== $key_permissions ) {
|
||||
throw new Exception( __( 'The API key provided does not have read permissions.', 'woocommerce' ), 401 );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
case 'PUT':
|
||||
case 'PATCH':
|
||||
case 'DELETE':
|
||||
if ( 'write' !== $key_permissions && 'read_write' !== $key_permissions ) {
|
||||
throw new Exception( __( 'The API key provided does not have write permissions.', 'woocommerce' ), 401 );
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updated API Key last access datetime
|
||||
*
|
||||
* @since 2.4.0
|
||||
*
|
||||
* @param int $key_id
|
||||
*/
|
||||
private function update_api_key_last_access( $key_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->update(
|
||||
$wpdb->prefix . 'woocommerce_api_keys',
|
||||
array( 'last_access' => current_time( 'mysql' ) ),
|
||||
array( 'key_id' => $key_id ),
|
||||
array( '%s' ),
|
||||
array( '%d' )
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,575 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API Coupons Class
|
||||
*
|
||||
* Handles requests to the /coupons endpoint
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_Coupons extends WC_API_Resource {
|
||||
|
||||
/** @var string $base the route base */
|
||||
protected $base = '/coupons';
|
||||
|
||||
/**
|
||||
* Register the routes for this class
|
||||
*
|
||||
* GET /coupons
|
||||
* GET /coupons/count
|
||||
* GET /coupons/<id>
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $routes
|
||||
* @return array
|
||||
*/
|
||||
public function register_routes( $routes ) {
|
||||
|
||||
# GET/POST /coupons
|
||||
$routes[ $this->base ] = array(
|
||||
array( array( $this, 'get_coupons' ), WC_API_Server::READABLE ),
|
||||
array( array( $this, 'create_coupon' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
|
||||
);
|
||||
|
||||
# GET /coupons/count
|
||||
$routes[ $this->base . '/count' ] = array(
|
||||
array( array( $this, 'get_coupons_count' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# GET/PUT/DELETE /coupons/<id>
|
||||
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
|
||||
array( array( $this, 'get_coupon' ), WC_API_Server::READABLE ),
|
||||
array( array( $this, 'edit_coupon' ), WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ),
|
||||
array( array( $this, 'delete_coupon' ), WC_API_SERVER::DELETABLE ),
|
||||
);
|
||||
|
||||
# GET /coupons/code/<code>, note that coupon codes can contain spaces, dashes and underscores
|
||||
$routes[ $this->base . '/code/(?P<code>\w[\w\s\-]*)' ] = array(
|
||||
array( array( $this, 'get_coupon_by_code' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# POST|PUT /coupons/bulk
|
||||
$routes[ $this->base . '/bulk' ] = array(
|
||||
array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
|
||||
);
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all coupons
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $fields
|
||||
* @param array $filter
|
||||
* @param int $page
|
||||
* @return array
|
||||
*/
|
||||
public function get_coupons( $fields = null, $filter = array(), $page = 1 ) {
|
||||
|
||||
$filter['page'] = $page;
|
||||
|
||||
$query = $this->query_coupons( $filter );
|
||||
|
||||
$coupons = array();
|
||||
|
||||
foreach ( $query->posts as $coupon_id ) {
|
||||
|
||||
if ( ! $this->is_readable( $coupon_id ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$coupons[] = current( $this->get_coupon( $coupon_id, $fields ) );
|
||||
}
|
||||
|
||||
$this->server->add_pagination_headers( $query );
|
||||
|
||||
return array( 'coupons' => $coupons );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the coupon for the given ID
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int $id the coupon ID
|
||||
* @param string $fields fields to include in response
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_coupon( $id, $fields = null ) {
|
||||
try {
|
||||
|
||||
$id = $this->validate_request( $id, 'shop_coupon', 'read' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$coupon = new WC_Coupon( $id );
|
||||
|
||||
if ( 0 === $coupon->get_id() ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_id', __( 'Invalid coupon ID', 'woocommerce' ), 404 );
|
||||
}
|
||||
|
||||
$coupon_data = array(
|
||||
'id' => $coupon->get_id(),
|
||||
'code' => $coupon->get_code(),
|
||||
'type' => $coupon->get_discount_type(),
|
||||
'created_at' => $this->server->format_datetime( $coupon->get_date_created() ? $coupon->get_date_created()->getTimestamp() : 0 ), // API gives UTC times.
|
||||
'updated_at' => $this->server->format_datetime( $coupon->get_date_modified() ? $coupon->get_date_modified()->getTimestamp() : 0 ), // API gives UTC times.
|
||||
'amount' => wc_format_decimal( $coupon->get_amount(), 2 ),
|
||||
'individual_use' => $coupon->get_individual_use(),
|
||||
'product_ids' => array_map( 'absint', (array) $coupon->get_product_ids() ),
|
||||
'exclude_product_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_ids() ),
|
||||
'usage_limit' => $coupon->get_usage_limit() ? $coupon->get_usage_limit() : null,
|
||||
'usage_limit_per_user' => $coupon->get_usage_limit_per_user() ? $coupon->get_usage_limit_per_user() : null,
|
||||
'limit_usage_to_x_items' => (int) $coupon->get_limit_usage_to_x_items(),
|
||||
'usage_count' => (int) $coupon->get_usage_count(),
|
||||
'expiry_date' => $coupon->get_date_expires() ? $this->server->format_datetime( $coupon->get_date_expires()->getTimestamp() ) : null, // API gives UTC times.
|
||||
'enable_free_shipping' => $coupon->get_free_shipping(),
|
||||
'product_category_ids' => array_map( 'absint', (array) $coupon->get_product_categories() ),
|
||||
'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_categories() ),
|
||||
'exclude_sale_items' => $coupon->get_exclude_sale_items(),
|
||||
'minimum_amount' => wc_format_decimal( $coupon->get_minimum_amount(), 2 ),
|
||||
'maximum_amount' => wc_format_decimal( $coupon->get_maximum_amount(), 2 ),
|
||||
'customer_emails' => $coupon->get_email_restrictions(),
|
||||
'description' => $coupon->get_description(),
|
||||
);
|
||||
|
||||
return array( 'coupon' => apply_filters( 'woocommerce_api_coupon_response', $coupon_data, $coupon, $fields, $this->server ) );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of coupons
|
||||
*
|
||||
* @since 2.1
|
||||
*
|
||||
* @param array $filter
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_coupons_count( $filter = array() ) {
|
||||
try {
|
||||
if ( ! current_user_can( 'read_private_shop_coupons' ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_coupons_count', __( 'You do not have permission to read the coupons count', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$query = $this->query_coupons( $filter );
|
||||
|
||||
return array( 'count' => (int) $query->found_posts );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the coupon for the given code
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $code the coupon code
|
||||
* @param string $fields fields to include in response
|
||||
* @return int|WP_Error
|
||||
*/
|
||||
public function get_coupon_by_code( $code, $fields = null ) {
|
||||
global $wpdb;
|
||||
|
||||
try {
|
||||
$id = $wpdb->get_var( $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 LIMIT 1;", $code ) );
|
||||
|
||||
if ( is_null( $id ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_code', __( 'Invalid coupon code', 'woocommerce' ), 404 );
|
||||
}
|
||||
|
||||
return $this->get_coupon( $id, $fields );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a coupon
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function create_coupon( $data ) {
|
||||
global $wpdb;
|
||||
|
||||
try {
|
||||
if ( ! isset( $data['coupon'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'coupon' ), 400 );
|
||||
}
|
||||
|
||||
$data = $data['coupon'];
|
||||
|
||||
// Check user permission
|
||||
if ( ! current_user_can( 'publish_shop_coupons' ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_coupon', __( 'You do not have permission to create coupons', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$data = apply_filters( 'woocommerce_api_create_coupon_data', $data, $this );
|
||||
|
||||
// Check if coupon code is specified
|
||||
if ( ! isset( $data['code'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_coupon_code', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'code' ), 400 );
|
||||
}
|
||||
|
||||
$coupon_code = wc_format_coupon_code( $data['code'] );
|
||||
$id_from_code = wc_get_coupon_id_by_code( $coupon_code );
|
||||
|
||||
if ( $id_from_code ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 );
|
||||
}
|
||||
|
||||
$defaults = array(
|
||||
'type' => 'fixed_cart',
|
||||
'amount' => 0,
|
||||
'individual_use' => false,
|
||||
'product_ids' => array(),
|
||||
'exclude_product_ids' => array(),
|
||||
'usage_limit' => '',
|
||||
'usage_limit_per_user' => '',
|
||||
'limit_usage_to_x_items' => '',
|
||||
'usage_count' => '',
|
||||
'expiry_date' => '',
|
||||
'enable_free_shipping' => false,
|
||||
'product_category_ids' => array(),
|
||||
'exclude_product_category_ids' => array(),
|
||||
'exclude_sale_items' => false,
|
||||
'minimum_amount' => '',
|
||||
'maximum_amount' => '',
|
||||
'customer_emails' => array(),
|
||||
'description' => '',
|
||||
);
|
||||
|
||||
$coupon_data = wp_parse_args( $data, $defaults );
|
||||
|
||||
// Validate coupon types
|
||||
if ( ! in_array( wc_clean( $coupon_data['type'] ), array_keys( wc_get_coupon_types() ) ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 );
|
||||
}
|
||||
|
||||
$new_coupon = array(
|
||||
'post_title' => $coupon_code,
|
||||
'post_content' => '',
|
||||
'post_status' => 'publish',
|
||||
'post_author' => get_current_user_id(),
|
||||
'post_type' => 'shop_coupon',
|
||||
'post_excerpt' => $coupon_data['description'],
|
||||
);
|
||||
|
||||
$id = wp_insert_post( $new_coupon, true );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_cannot_create_coupon', $id->get_error_message(), 400 );
|
||||
}
|
||||
|
||||
// Set coupon meta
|
||||
update_post_meta( $id, 'discount_type', $coupon_data['type'] );
|
||||
update_post_meta( $id, 'coupon_amount', wc_format_decimal( $coupon_data['amount'] ) );
|
||||
update_post_meta( $id, 'individual_use', ( true === $coupon_data['individual_use'] ) ? 'yes' : 'no' );
|
||||
update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['product_ids'] ) ) ) );
|
||||
update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['exclude_product_ids'] ) ) ) );
|
||||
update_post_meta( $id, 'usage_limit', absint( $coupon_data['usage_limit'] ) );
|
||||
update_post_meta( $id, 'usage_limit_per_user', absint( $coupon_data['usage_limit_per_user'] ) );
|
||||
update_post_meta( $id, 'limit_usage_to_x_items', absint( $coupon_data['limit_usage_to_x_items'] ) );
|
||||
update_post_meta( $id, 'usage_count', absint( $coupon_data['usage_count'] ) );
|
||||
update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ) ) );
|
||||
update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ), true ) );
|
||||
update_post_meta( $id, 'free_shipping', ( true === $coupon_data['enable_free_shipping'] ) ? 'yes' : 'no' );
|
||||
update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $coupon_data['product_category_ids'] ) ) );
|
||||
update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $coupon_data['exclude_product_category_ids'] ) ) );
|
||||
update_post_meta( $id, 'exclude_sale_items', ( true === $coupon_data['exclude_sale_items'] ) ? 'yes' : 'no' );
|
||||
update_post_meta( $id, 'minimum_amount', wc_format_decimal( $coupon_data['minimum_amount'] ) );
|
||||
update_post_meta( $id, 'maximum_amount', wc_format_decimal( $coupon_data['maximum_amount'] ) );
|
||||
update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $coupon_data['customer_emails'] ) ) );
|
||||
|
||||
do_action( 'woocommerce_api_create_coupon', $id, $data );
|
||||
do_action( 'woocommerce_new_coupon', $id );
|
||||
|
||||
$this->server->send_status( 201 );
|
||||
|
||||
return $this->get_coupon( $id );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a coupon
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @param int $id the coupon ID
|
||||
* @param array $data
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function edit_coupon( $id, $data ) {
|
||||
|
||||
try {
|
||||
if ( ! isset( $data['coupon'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'coupon' ), 400 );
|
||||
}
|
||||
|
||||
$data = $data['coupon'];
|
||||
|
||||
$id = $this->validate_request( $id, 'shop_coupon', 'edit' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$data = apply_filters( 'woocommerce_api_edit_coupon_data', $data, $id, $this );
|
||||
|
||||
if ( isset( $data['code'] ) ) {
|
||||
global $wpdb;
|
||||
|
||||
$coupon_code = wc_format_coupon_code( $data['code'] );
|
||||
$id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id );
|
||||
|
||||
if ( $id_from_code ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 );
|
||||
}
|
||||
|
||||
$updated = wp_update_post( array( 'ID' => intval( $id ), 'post_title' => $coupon_code ) );
|
||||
|
||||
if ( 0 === $updated ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $data['description'] ) ) {
|
||||
$updated = wp_update_post( array( 'ID' => intval( $id ), 'post_excerpt' => $data['description'] ) );
|
||||
|
||||
if ( 0 === $updated ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $data['type'] ) ) {
|
||||
// Validate coupon types
|
||||
if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_coupon_types() ) ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 );
|
||||
}
|
||||
update_post_meta( $id, 'discount_type', $data['type'] );
|
||||
}
|
||||
|
||||
if ( isset( $data['amount'] ) ) {
|
||||
update_post_meta( $id, 'coupon_amount', wc_format_decimal( $data['amount'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['individual_use'] ) ) {
|
||||
update_post_meta( $id, 'individual_use', ( true === $data['individual_use'] ) ? 'yes' : 'no' );
|
||||
}
|
||||
|
||||
if ( isset( $data['product_ids'] ) ) {
|
||||
update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $data['product_ids'] ) ) ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['exclude_product_ids'] ) ) {
|
||||
update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $data['exclude_product_ids'] ) ) ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['usage_limit'] ) ) {
|
||||
update_post_meta( $id, 'usage_limit', absint( $data['usage_limit'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['usage_limit_per_user'] ) ) {
|
||||
update_post_meta( $id, 'usage_limit_per_user', absint( $data['usage_limit_per_user'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['limit_usage_to_x_items'] ) ) {
|
||||
update_post_meta( $id, 'limit_usage_to_x_items', absint( $data['limit_usage_to_x_items'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['usage_count'] ) ) {
|
||||
update_post_meta( $id, 'usage_count', absint( $data['usage_count'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['expiry_date'] ) ) {
|
||||
update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ) ) );
|
||||
update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ), true ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['enable_free_shipping'] ) ) {
|
||||
update_post_meta( $id, 'free_shipping', ( true === $data['enable_free_shipping'] ) ? 'yes' : 'no' );
|
||||
}
|
||||
|
||||
if ( isset( $data['product_category_ids'] ) ) {
|
||||
update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $data['product_category_ids'] ) ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['exclude_product_category_ids'] ) ) {
|
||||
update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $data['exclude_product_category_ids'] ) ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['exclude_sale_items'] ) ) {
|
||||
update_post_meta( $id, 'exclude_sale_items', ( true === $data['exclude_sale_items'] ) ? 'yes' : 'no' );
|
||||
}
|
||||
|
||||
if ( isset( $data['minimum_amount'] ) ) {
|
||||
update_post_meta( $id, 'minimum_amount', wc_format_decimal( $data['minimum_amount'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['maximum_amount'] ) ) {
|
||||
update_post_meta( $id, 'maximum_amount', wc_format_decimal( $data['maximum_amount'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['customer_emails'] ) ) {
|
||||
update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $data['customer_emails'] ) ) );
|
||||
}
|
||||
|
||||
do_action( 'woocommerce_api_edit_coupon', $id, $data );
|
||||
do_action( 'woocommerce_update_coupon', $id );
|
||||
|
||||
return $this->get_coupon( $id );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a coupon
|
||||
*
|
||||
* @since 2.2
|
||||
* @param int $id the coupon ID
|
||||
* @param bool $force true to permanently delete coupon, false to move to trash
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function delete_coupon( $id, $force = false ) {
|
||||
|
||||
$id = $this->validate_request( $id, 'shop_coupon', 'delete' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
do_action( 'woocommerce_api_delete_coupon', $id, $this );
|
||||
|
||||
return $this->delete( $id, 'shop_coupon', ( 'true' === $force ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* expiry_date format
|
||||
*
|
||||
* @since 2.3.0
|
||||
* @param string $expiry_date
|
||||
* @param bool $as_timestamp (default: false)
|
||||
* @return string|int
|
||||
*/
|
||||
protected function get_coupon_expiry_date( $expiry_date, $as_timestamp = false ) {
|
||||
if ( '' != $expiry_date ) {
|
||||
if ( $as_timestamp ) {
|
||||
return strtotime( $expiry_date );
|
||||
}
|
||||
|
||||
return date( 'Y-m-d', strtotime( $expiry_date ) );
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get coupon post objects
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $args request arguments for filtering query
|
||||
* @return WP_Query
|
||||
*/
|
||||
private function query_coupons( $args ) {
|
||||
|
||||
// set base query arguments
|
||||
$query_args = array(
|
||||
'fields' => 'ids',
|
||||
'post_type' => 'shop_coupon',
|
||||
'post_status' => 'publish',
|
||||
);
|
||||
|
||||
$query_args = $this->merge_query_args( $query_args, $args );
|
||||
|
||||
return new WP_Query( $query_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk update or insert coupons
|
||||
* Accepts an array with coupons in the formats supported by
|
||||
* WC_API_Coupons->create_coupon() and WC_API_Coupons->edit_coupon()
|
||||
*
|
||||
* @since 2.4.0
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function bulk( $data ) {
|
||||
|
||||
try {
|
||||
if ( ! isset( $data['coupons'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_coupons_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'coupons' ), 400 );
|
||||
}
|
||||
|
||||
$data = $data['coupons'];
|
||||
$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'coupons' );
|
||||
|
||||
// Limit bulk operation
|
||||
if ( count( $data ) > $limit ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_coupons_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
|
||||
}
|
||||
|
||||
$coupons = array();
|
||||
|
||||
foreach ( $data as $_coupon ) {
|
||||
$coupon_id = 0;
|
||||
|
||||
// Try to get the coupon ID
|
||||
if ( isset( $_coupon['id'] ) ) {
|
||||
$coupon_id = intval( $_coupon['id'] );
|
||||
}
|
||||
|
||||
// Coupon exists / edit coupon
|
||||
if ( $coupon_id ) {
|
||||
$edit = $this->edit_coupon( $coupon_id, array( 'coupon' => $_coupon ) );
|
||||
|
||||
if ( is_wp_error( $edit ) ) {
|
||||
$coupons[] = array(
|
||||
'id' => $coupon_id,
|
||||
'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
|
||||
);
|
||||
} else {
|
||||
$coupons[] = $edit['coupon'];
|
||||
}
|
||||
} else {
|
||||
|
||||
// Coupon don't exists / create coupon
|
||||
$new = $this->create_coupon( array( 'coupon' => $_coupon ) );
|
||||
|
||||
if ( is_wp_error( $new ) ) {
|
||||
$coupons[] = array(
|
||||
'id' => $coupon_id,
|
||||
'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
|
||||
);
|
||||
} else {
|
||||
$coupons[] = $new['coupon'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array( 'coupons' => apply_filters( 'woocommerce_api_coupons_bulk_response', $coupons, $this ) );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,837 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API Customers Class
|
||||
*
|
||||
* Handles requests to the /customers endpoint
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.2
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_Customers extends WC_API_Resource {
|
||||
|
||||
/** @var string $base the route base */
|
||||
protected $base = '/customers';
|
||||
|
||||
/** @var string $created_at_min for date filtering */
|
||||
private $created_at_min = null;
|
||||
|
||||
/** @var string $created_at_max for date filtering */
|
||||
private $created_at_max = null;
|
||||
|
||||
/**
|
||||
* Setup class, overridden to provide customer data to order response
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WC_API_Server $server
|
||||
*/
|
||||
public function __construct( WC_API_Server $server ) {
|
||||
|
||||
parent::__construct( $server );
|
||||
|
||||
// add customer data to order responses
|
||||
add_filter( 'woocommerce_api_order_response', array( $this, 'add_customer_data' ), 10, 2 );
|
||||
|
||||
// modify WP_User_Query to support created_at date filtering
|
||||
add_action( 'pre_user_query', array( $this, 'modify_user_query' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the routes for this class
|
||||
*
|
||||
* GET /customers
|
||||
* GET /customers/count
|
||||
* GET /customers/<id>
|
||||
* GET /customers/<id>/orders
|
||||
*
|
||||
* @since 2.2
|
||||
* @param array $routes
|
||||
* @return array
|
||||
*/
|
||||
public function register_routes( $routes ) {
|
||||
|
||||
# GET/POST /customers
|
||||
$routes[ $this->base ] = array(
|
||||
array( array( $this, 'get_customers' ), WC_API_SERVER::READABLE ),
|
||||
array( array( $this, 'create_customer' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
|
||||
);
|
||||
|
||||
# GET /customers/count
|
||||
$routes[ $this->base . '/count' ] = array(
|
||||
array( array( $this, 'get_customers_count' ), WC_API_SERVER::READABLE ),
|
||||
);
|
||||
|
||||
# GET/PUT/DELETE /customers/<id>
|
||||
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
|
||||
array( array( $this, 'get_customer' ), WC_API_SERVER::READABLE ),
|
||||
array( array( $this, 'edit_customer' ), WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ),
|
||||
array( array( $this, 'delete_customer' ), WC_API_SERVER::DELETABLE ),
|
||||
);
|
||||
|
||||
# GET /customers/email/<email>
|
||||
$routes[ $this->base . '/email/(?P<email>.+)' ] = array(
|
||||
array( array( $this, 'get_customer_by_email' ), WC_API_SERVER::READABLE ),
|
||||
);
|
||||
|
||||
# GET /customers/<id>/orders
|
||||
$routes[ $this->base . '/(?P<id>\d+)/orders' ] = array(
|
||||
array( array( $this, 'get_customer_orders' ), WC_API_SERVER::READABLE ),
|
||||
);
|
||||
|
||||
# GET /customers/<id>/downloads
|
||||
$routes[ $this->base . '/(?P<id>\d+)/downloads' ] = array(
|
||||
array( array( $this, 'get_customer_downloads' ), WC_API_SERVER::READABLE ),
|
||||
);
|
||||
|
||||
# POST|PUT /customers/bulk
|
||||
$routes[ $this->base . '/bulk' ] = array(
|
||||
array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
|
||||
);
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all customers
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $fields
|
||||
* @param array $filter
|
||||
* @param int $page
|
||||
* @return array
|
||||
*/
|
||||
public function get_customers( $fields = null, $filter = array(), $page = 1 ) {
|
||||
|
||||
$filter['page'] = $page;
|
||||
|
||||
$query = $this->query_customers( $filter );
|
||||
|
||||
$customers = array();
|
||||
|
||||
foreach ( $query->get_results() as $user_id ) {
|
||||
|
||||
if ( ! $this->is_readable( $user_id ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$customers[] = current( $this->get_customer( $user_id, $fields ) );
|
||||
}
|
||||
|
||||
$this->server->add_pagination_headers( $query );
|
||||
|
||||
return array( 'customers' => $customers );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the customer for the given ID
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int $id the customer ID
|
||||
* @param array $fields
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_customer( $id, $fields = null ) {
|
||||
global $wpdb;
|
||||
|
||||
$id = $this->validate_request( $id, 'customer', 'read' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$customer = new WC_Customer( $id );
|
||||
$last_order = $customer->get_last_order();
|
||||
$customer_data = array(
|
||||
'id' => $customer->get_id(),
|
||||
'created_at' => $this->server->format_datetime( $customer->get_date_created() ? $customer->get_date_created()->getTimestamp() : 0 ), // API gives UTC times.
|
||||
'email' => $customer->get_email(),
|
||||
'first_name' => $customer->get_first_name(),
|
||||
'last_name' => $customer->get_last_name(),
|
||||
'username' => $customer->get_username(),
|
||||
'role' => $customer->get_role(),
|
||||
'last_order_id' => is_object( $last_order ) ? $last_order->get_id() : null,
|
||||
'last_order_date' => is_object( $last_order ) ? $this->server->format_datetime( $last_order->get_date_created() ? $last_order->get_date_created()->getTimestamp() : 0 ) : null, // API gives UTC times.
|
||||
'orders_count' => $customer->get_order_count(),
|
||||
'total_spent' => wc_format_decimal( $customer->get_total_spent(), 2 ),
|
||||
'avatar_url' => $customer->get_avatar_url(),
|
||||
'billing_address' => array(
|
||||
'first_name' => $customer->get_billing_first_name(),
|
||||
'last_name' => $customer->get_billing_last_name(),
|
||||
'company' => $customer->get_billing_company(),
|
||||
'address_1' => $customer->get_billing_address_1(),
|
||||
'address_2' => $customer->get_billing_address_2(),
|
||||
'city' => $customer->get_billing_city(),
|
||||
'state' => $customer->get_billing_state(),
|
||||
'postcode' => $customer->get_billing_postcode(),
|
||||
'country' => $customer->get_billing_country(),
|
||||
'email' => $customer->get_billing_email(),
|
||||
'phone' => $customer->get_billing_phone(),
|
||||
),
|
||||
'shipping_address' => array(
|
||||
'first_name' => $customer->get_shipping_first_name(),
|
||||
'last_name' => $customer->get_shipping_last_name(),
|
||||
'company' => $customer->get_shipping_company(),
|
||||
'address_1' => $customer->get_shipping_address_1(),
|
||||
'address_2' => $customer->get_shipping_address_2(),
|
||||
'city' => $customer->get_shipping_city(),
|
||||
'state' => $customer->get_shipping_state(),
|
||||
'postcode' => $customer->get_shipping_postcode(),
|
||||
'country' => $customer->get_shipping_country(),
|
||||
),
|
||||
);
|
||||
|
||||
return array( 'customer' => apply_filters( 'woocommerce_api_customer_response', $customer_data, $customer, $fields, $this->server ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the customer for the given email
|
||||
*
|
||||
* @since 2.1
|
||||
*
|
||||
* @param string $email the customer email
|
||||
* @param array $fields
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_customer_by_email( $email, $fields = null ) {
|
||||
try {
|
||||
if ( is_email( $email ) ) {
|
||||
$customer = get_user_by( 'email', $email );
|
||||
if ( ! is_object( $customer ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 );
|
||||
}
|
||||
} else {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 );
|
||||
}
|
||||
|
||||
return $this->get_customer( $customer->ID, $fields );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of customers
|
||||
*
|
||||
* @since 2.1
|
||||
*
|
||||
* @param array $filter
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_customers_count( $filter = array() ) {
|
||||
try {
|
||||
if ( ! current_user_can( 'list_users' ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customers_count', __( 'You do not have permission to read the customers count', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$query = $this->query_customers( $filter );
|
||||
|
||||
return array( 'count' => $query->get_total() );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customer billing address fields.
|
||||
*
|
||||
* @since 2.2
|
||||
* @return array
|
||||
*/
|
||||
protected function get_customer_billing_address() {
|
||||
$billing_address = apply_filters( 'woocommerce_api_customer_billing_address', array(
|
||||
'first_name',
|
||||
'last_name',
|
||||
'company',
|
||||
'address_1',
|
||||
'address_2',
|
||||
'city',
|
||||
'state',
|
||||
'postcode',
|
||||
'country',
|
||||
'email',
|
||||
'phone',
|
||||
) );
|
||||
|
||||
return $billing_address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customer shipping address fields.
|
||||
*
|
||||
* @since 2.2
|
||||
* @return array
|
||||
*/
|
||||
protected function get_customer_shipping_address() {
|
||||
$shipping_address = apply_filters( 'woocommerce_api_customer_shipping_address', array(
|
||||
'first_name',
|
||||
'last_name',
|
||||
'company',
|
||||
'address_1',
|
||||
'address_2',
|
||||
'city',
|
||||
'state',
|
||||
'postcode',
|
||||
'country',
|
||||
) );
|
||||
|
||||
return $shipping_address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add/Update customer data.
|
||||
*
|
||||
* @since 2.2
|
||||
* @param int $id the customer ID
|
||||
* @param array $data
|
||||
* @param WC_Customer $customer
|
||||
*/
|
||||
protected function update_customer_data( $id, $data, $customer ) {
|
||||
|
||||
// Customer first name.
|
||||
if ( isset( $data['first_name'] ) ) {
|
||||
$customer->set_first_name( wc_clean( $data['first_name'] ) );
|
||||
}
|
||||
|
||||
// Customer last name.
|
||||
if ( isset( $data['last_name'] ) ) {
|
||||
$customer->set_last_name( wc_clean( $data['last_name'] ) );
|
||||
}
|
||||
|
||||
// Customer billing address.
|
||||
if ( isset( $data['billing_address'] ) ) {
|
||||
foreach ( $this->get_customer_billing_address() as $field ) {
|
||||
if ( isset( $data['billing_address'][ $field ] ) ) {
|
||||
if ( is_callable( array( $customer, "set_billing_{$field}" ) ) ) {
|
||||
$customer->{"set_billing_{$field}"}( $data['billing_address'][ $field ] );
|
||||
} else {
|
||||
$customer->update_meta_data( 'billing_' . $field, wc_clean( $data['billing_address'][ $field ] ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Customer shipping address.
|
||||
if ( isset( $data['shipping_address'] ) ) {
|
||||
foreach ( $this->get_customer_shipping_address() as $field ) {
|
||||
if ( isset( $data['shipping_address'][ $field ] ) ) {
|
||||
if ( is_callable( array( $customer, "set_shipping_{$field}" ) ) ) {
|
||||
$customer->{"set_shipping_{$field}"}( $data['shipping_address'][ $field ] );
|
||||
} else {
|
||||
$customer->update_meta_data( 'shipping_' . $field, wc_clean( $data['shipping_address'][ $field ] ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
do_action( 'woocommerce_api_update_customer_data', $id, $data, $customer );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a customer
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function create_customer( $data ) {
|
||||
try {
|
||||
if ( ! isset( $data['customer'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'customer' ), 400 );
|
||||
}
|
||||
|
||||
$data = $data['customer'];
|
||||
|
||||
// Checks with can create new users.
|
||||
if ( ! current_user_can( 'create_users' ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'You do not have permission to create this customer', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$data = apply_filters( 'woocommerce_api_create_customer_data', $data, $this );
|
||||
|
||||
// Checks with the email is missing.
|
||||
if ( ! isset( $data['email'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_customer_email', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'email' ), 400 );
|
||||
}
|
||||
|
||||
// Create customer.
|
||||
$customer = new WC_Customer;
|
||||
$customer->set_username( ! empty( $data['username'] ) ? $data['username'] : '' );
|
||||
$customer->set_password( ! empty( $data['password'] ) ? $data['password'] : '' );
|
||||
$customer->set_email( $data['email'] );
|
||||
$customer->save();
|
||||
|
||||
if ( ! $customer->get_id() ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'This resource cannot be created.', 'woocommerce' ), 400 );
|
||||
}
|
||||
|
||||
// Added customer data.
|
||||
$this->update_customer_data( $customer->get_id(), $data, $customer );
|
||||
$customer->save();
|
||||
|
||||
do_action( 'woocommerce_api_create_customer', $customer->get_id(), $data );
|
||||
|
||||
$this->server->send_status( 201 );
|
||||
|
||||
return $this->get_customer( $customer->get_id() );
|
||||
} catch ( Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a customer
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @param int $id the customer ID
|
||||
* @param array $data
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function edit_customer( $id, $data ) {
|
||||
try {
|
||||
if ( ! isset( $data['customer'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'customer' ), 400 );
|
||||
}
|
||||
|
||||
$data = $data['customer'];
|
||||
|
||||
// Validate the customer ID.
|
||||
$id = $this->validate_request( $id, 'customer', 'edit' );
|
||||
|
||||
// Return the validate error.
|
||||
if ( is_wp_error( $id ) ) {
|
||||
throw new WC_API_Exception( $id->get_error_code(), $id->get_error_message(), 400 );
|
||||
}
|
||||
|
||||
$data = apply_filters( 'woocommerce_api_edit_customer_data', $data, $this );
|
||||
|
||||
$customer = new WC_Customer( $id );
|
||||
|
||||
// Customer email.
|
||||
if ( isset( $data['email'] ) ) {
|
||||
$customer->set_email( $data['email'] );
|
||||
}
|
||||
|
||||
// Customer password.
|
||||
if ( isset( $data['password'] ) ) {
|
||||
$customer->set_password( $data['password'] );
|
||||
}
|
||||
|
||||
// Update customer data.
|
||||
$this->update_customer_data( $customer->get_id(), $data, $customer );
|
||||
|
||||
$customer->save();
|
||||
|
||||
do_action( 'woocommerce_api_edit_customer', $customer->get_id(), $data );
|
||||
|
||||
return $this->get_customer( $customer->get_id() );
|
||||
} catch ( Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a customer
|
||||
*
|
||||
* @since 2.2
|
||||
* @param int $id the customer ID
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function delete_customer( $id ) {
|
||||
|
||||
// Validate the customer ID.
|
||||
$id = $this->validate_request( $id, 'customer', 'delete' );
|
||||
|
||||
// Return the validate error.
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
do_action( 'woocommerce_api_delete_customer', $id, $this );
|
||||
|
||||
return $this->delete( $id, 'customer' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the orders for a customer
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int $id the customer ID
|
||||
* @param string $fields fields to include in response
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_customer_orders( $id, $fields = null ) {
|
||||
global $wpdb;
|
||||
|
||||
$id = $this->validate_request( $id, 'customer', 'read' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$order_ids = wc_get_orders( array(
|
||||
'customer' => $id,
|
||||
'limit' => -1,
|
||||
'orderby' => 'date',
|
||||
'order' => 'ASC',
|
||||
'return' => 'ids',
|
||||
) );
|
||||
|
||||
if ( empty( $order_ids ) ) {
|
||||
return array( 'orders' => array() );
|
||||
}
|
||||
|
||||
$orders = array();
|
||||
|
||||
foreach ( $order_ids as $order_id ) {
|
||||
$orders[] = current( WC()->api->WC_API_Orders->get_order( $order_id, $fields ) );
|
||||
}
|
||||
|
||||
return array( 'orders' => apply_filters( 'woocommerce_api_customer_orders_response', $orders, $id, $fields, $order_ids, $this->server ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available downloads for a customer
|
||||
*
|
||||
* @since 2.2
|
||||
* @param int $id the customer ID
|
||||
* @param string $fields fields to include in response
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_customer_downloads( $id, $fields = null ) {
|
||||
$id = $this->validate_request( $id, 'customer', 'read' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$downloads = array();
|
||||
$_downloads = wc_get_customer_available_downloads( $id );
|
||||
|
||||
foreach ( $_downloads as $key => $download ) {
|
||||
$downloads[] = array(
|
||||
'download_url' => $download['download_url'],
|
||||
'download_id' => $download['download_id'],
|
||||
'product_id' => $download['product_id'],
|
||||
'download_name' => $download['download_name'],
|
||||
'order_id' => $download['order_id'],
|
||||
'order_key' => $download['order_key'],
|
||||
'downloads_remaining' => $download['downloads_remaining'],
|
||||
'access_expires' => $download['access_expires'] ? $this->server->format_datetime( $download['access_expires'] ) : null,
|
||||
'file' => $download['file'],
|
||||
);
|
||||
}
|
||||
|
||||
return array( 'downloads' => apply_filters( 'woocommerce_api_customer_downloads_response', $downloads, $id, $fields, $this->server ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get customer user objects
|
||||
*
|
||||
* Note that WP_User_Query does not have built-in pagination so limit & offset are used to provide limited
|
||||
* pagination support
|
||||
*
|
||||
* The filter for role can only be a single role in a string.
|
||||
*
|
||||
* @since 2.3
|
||||
* @param array $args request arguments for filtering query
|
||||
* @return WP_User_Query
|
||||
*/
|
||||
private function query_customers( $args = array() ) {
|
||||
|
||||
// default users per page
|
||||
$users_per_page = get_option( 'posts_per_page' );
|
||||
|
||||
// Set base query arguments
|
||||
$query_args = array(
|
||||
'fields' => 'ID',
|
||||
'role' => 'customer',
|
||||
'orderby' => 'registered',
|
||||
'number' => $users_per_page,
|
||||
);
|
||||
|
||||
// Custom Role
|
||||
if ( ! empty( $args['role'] ) ) {
|
||||
$query_args['role'] = $args['role'];
|
||||
}
|
||||
|
||||
// Search
|
||||
if ( ! empty( $args['q'] ) ) {
|
||||
$query_args['search'] = $args['q'];
|
||||
}
|
||||
|
||||
// Limit number of users returned
|
||||
if ( ! empty( $args['limit'] ) ) {
|
||||
if ( -1 == $args['limit'] ) {
|
||||
unset( $query_args['number'] );
|
||||
} else {
|
||||
$query_args['number'] = absint( $args['limit'] );
|
||||
$users_per_page = absint( $args['limit'] );
|
||||
}
|
||||
} else {
|
||||
$args['limit'] = $query_args['number'];
|
||||
}
|
||||
|
||||
// Page
|
||||
$page = ( isset( $args['page'] ) ) ? absint( $args['page'] ) : 1;
|
||||
|
||||
// Offset
|
||||
if ( ! empty( $args['offset'] ) ) {
|
||||
$query_args['offset'] = absint( $args['offset'] );
|
||||
} else {
|
||||
$query_args['offset'] = $users_per_page * ( $page - 1 );
|
||||
}
|
||||
|
||||
// Created date
|
||||
if ( ! empty( $args['created_at_min'] ) ) {
|
||||
$this->created_at_min = $this->server->parse_datetime( $args['created_at_min'] );
|
||||
}
|
||||
|
||||
if ( ! empty( $args['created_at_max'] ) ) {
|
||||
$this->created_at_max = $this->server->parse_datetime( $args['created_at_max'] );
|
||||
}
|
||||
|
||||
// Order (ASC or DESC, ASC by default)
|
||||
if ( ! empty( $args['order'] ) ) {
|
||||
$query_args['order'] = $args['order'];
|
||||
}
|
||||
|
||||
// Order by
|
||||
if ( ! empty( $args['orderby'] ) ) {
|
||||
$query_args['orderby'] = $args['orderby'];
|
||||
|
||||
// Allow sorting by meta value
|
||||
if ( ! empty( $args['orderby_meta_key'] ) ) {
|
||||
$query_args['meta_key'] = $args['orderby_meta_key'];
|
||||
}
|
||||
}
|
||||
|
||||
$query = new WP_User_Query( $query_args );
|
||||
|
||||
// Helper members for pagination headers
|
||||
$query->total_pages = ( -1 == $args['limit'] ) ? 1 : ceil( $query->get_total() / $users_per_page );
|
||||
$query->page = $page;
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add customer data to orders
|
||||
*
|
||||
* @since 2.1
|
||||
* @param $order_data
|
||||
* @param $order
|
||||
* @return array
|
||||
*/
|
||||
public function add_customer_data( $order_data, $order ) {
|
||||
|
||||
if ( 0 == $order->get_user_id() ) {
|
||||
|
||||
// add customer data from order
|
||||
$order_data['customer'] = array(
|
||||
'id' => 0,
|
||||
'email' => $order->get_billing_email(),
|
||||
'first_name' => $order->get_billing_first_name(),
|
||||
'last_name' => $order->get_billing_last_name(),
|
||||
'billing_address' => array(
|
||||
'first_name' => $order->get_billing_first_name(),
|
||||
'last_name' => $order->get_billing_last_name(),
|
||||
'company' => $order->get_billing_company(),
|
||||
'address_1' => $order->get_billing_address_1(),
|
||||
'address_2' => $order->get_billing_address_2(),
|
||||
'city' => $order->get_billing_city(),
|
||||
'state' => $order->get_billing_state(),
|
||||
'postcode' => $order->get_billing_postcode(),
|
||||
'country' => $order->get_billing_country(),
|
||||
'email' => $order->get_billing_email(),
|
||||
'phone' => $order->get_billing_phone(),
|
||||
),
|
||||
'shipping_address' => array(
|
||||
'first_name' => $order->get_shipping_first_name(),
|
||||
'last_name' => $order->get_shipping_last_name(),
|
||||
'company' => $order->get_shipping_company(),
|
||||
'address_1' => $order->get_shipping_address_1(),
|
||||
'address_2' => $order->get_shipping_address_2(),
|
||||
'city' => $order->get_shipping_city(),
|
||||
'state' => $order->get_shipping_state(),
|
||||
'postcode' => $order->get_shipping_postcode(),
|
||||
'country' => $order->get_shipping_country(),
|
||||
),
|
||||
);
|
||||
|
||||
} else {
|
||||
|
||||
$order_data['customer'] = current( $this->get_customer( $order->get_user_id() ) );
|
||||
}
|
||||
|
||||
return $order_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the WP_User_Query to support filtering on the date the customer was created
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WP_User_Query $query
|
||||
*/
|
||||
public function modify_user_query( $query ) {
|
||||
|
||||
if ( $this->created_at_min ) {
|
||||
$query->query_where .= sprintf( " AND user_registered >= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%H:%%i:%%s' )", esc_sql( $this->created_at_min ) );
|
||||
}
|
||||
|
||||
if ( $this->created_at_max ) {
|
||||
$query->query_where .= sprintf( " AND user_registered <= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%H:%%i:%%s' )", esc_sql( $this->created_at_max ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the request by checking:
|
||||
*
|
||||
* 1) the ID is a valid integer
|
||||
* 2) the ID returns a valid WP_User
|
||||
* 3) the current user has the proper permissions
|
||||
*
|
||||
* @since 2.1
|
||||
* @see WC_API_Resource::validate_request()
|
||||
* @param integer $id the customer ID
|
||||
* @param string $type the request type, unused because this method overrides the parent class
|
||||
* @param string $context the context of the request, either `read`, `edit` or `delete`
|
||||
* @return int|WP_Error valid user ID or WP_Error if any of the checks fails
|
||||
*/
|
||||
protected function validate_request( $id, $type, $context ) {
|
||||
|
||||
try {
|
||||
$id = absint( $id );
|
||||
|
||||
// validate ID
|
||||
if ( empty( $id ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Invalid customer ID', 'woocommerce' ), 404 );
|
||||
}
|
||||
|
||||
// non-existent IDs return a valid WP_User object with the user ID = 0
|
||||
$customer = new WP_User( $id );
|
||||
|
||||
if ( 0 === $customer->ID ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_customer', __( 'Invalid customer', 'woocommerce' ), 404 );
|
||||
}
|
||||
|
||||
// validate permissions
|
||||
switch ( $context ) {
|
||||
|
||||
case 'read':
|
||||
if ( ! current_user_can( 'list_users' ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read this customer', 'woocommerce' ), 401 );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'edit':
|
||||
if ( ! wc_rest_check_user_permissions( 'edit', $customer->ID ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_customer', __( 'You do not have permission to edit this customer', 'woocommerce' ), 401 );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
if ( ! wc_rest_check_user_permissions( 'delete', $customer->ID ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_customer', __( 'You do not have permission to delete this customer', 'woocommerce' ), 401 );
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return $id;
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user can read users
|
||||
*
|
||||
* @since 2.1
|
||||
* @see WC_API_Resource::is_readable()
|
||||
* @param int|WP_Post $post unused
|
||||
* @return bool true if the current user can read users, false otherwise
|
||||
*/
|
||||
protected function is_readable( $post ) {
|
||||
return current_user_can( 'list_users' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk update or insert customers
|
||||
* Accepts an array with customers in the formats supported by
|
||||
* WC_API_Customers->create_customer() and WC_API_Customers->edit_customer()
|
||||
*
|
||||
* @since 2.4.0
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function bulk( $data ) {
|
||||
|
||||
try {
|
||||
if ( ! isset( $data['customers'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_customers_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'customers' ), 400 );
|
||||
}
|
||||
|
||||
$data = $data['customers'];
|
||||
$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'customers' );
|
||||
|
||||
// Limit bulk operation
|
||||
if ( count( $data ) > $limit ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_customers_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
|
||||
}
|
||||
|
||||
$customers = array();
|
||||
|
||||
foreach ( $data as $_customer ) {
|
||||
$customer_id = 0;
|
||||
|
||||
// Try to get the customer ID
|
||||
if ( isset( $_customer['id'] ) ) {
|
||||
$customer_id = intval( $_customer['id'] );
|
||||
}
|
||||
|
||||
// Customer exists / edit customer
|
||||
if ( $customer_id ) {
|
||||
$edit = $this->edit_customer( $customer_id, array( 'customer' => $_customer ) );
|
||||
|
||||
if ( is_wp_error( $edit ) ) {
|
||||
$customers[] = array(
|
||||
'id' => $customer_id,
|
||||
'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
|
||||
);
|
||||
} else {
|
||||
$customers[] = $edit['customer'];
|
||||
}
|
||||
} else {
|
||||
// Customer don't exists / create customer
|
||||
$new = $this->create_customer( array( 'customer' => $_customer ) );
|
||||
|
||||
if ( is_wp_error( $new ) ) {
|
||||
$customers[] = array(
|
||||
'id' => $customer_id,
|
||||
'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
|
||||
);
|
||||
} else {
|
||||
$customers[] = $new['customer'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array( 'customers' => apply_filters( 'woocommerce_api_customers_bulk_response', $customers, $this ) );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API Exception Class
|
||||
*
|
||||
* Extends Exception to provide additional data
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.2
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_Exception extends Exception {
|
||||
|
||||
/** @var string sanitized error code */
|
||||
protected $error_code;
|
||||
|
||||
/**
|
||||
* Setup exception, requires 3 params:
|
||||
*
|
||||
* error code - machine-readable, e.g. `woocommerce_invalid_product_id`
|
||||
* error message - friendly message, e.g. 'Product ID is invalid'
|
||||
* http status code - proper HTTP status code to respond with, e.g. 400
|
||||
*
|
||||
* @since 2.2
|
||||
* @param string $error_code
|
||||
* @param string $error_message user-friendly translated error message
|
||||
* @param int $http_status_code HTTP status code to respond with
|
||||
*/
|
||||
public function __construct( $error_code, $error_message, $http_status_code ) {
|
||||
$this->error_code = $error_code;
|
||||
parent::__construct( $error_message, $http_status_code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error code
|
||||
*
|
||||
* @since 2.2
|
||||
* @return string
|
||||
*/
|
||||
public function getErrorCode() {
|
||||
return $this->error_code;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API
|
||||
*
|
||||
* Handles parsing JSON request bodies and generating JSON responses
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_JSON_Handler implements WC_API_Handler {
|
||||
|
||||
/**
|
||||
* Get the content type for the response
|
||||
*
|
||||
* @since 2.1
|
||||
* @return string
|
||||
*/
|
||||
public function get_content_type() {
|
||||
|
||||
return sprintf( '%s; charset=%s', isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json', get_option( 'blog_charset' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the raw request body entity
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $body the raw request body
|
||||
* @return array|mixed
|
||||
*/
|
||||
public function parse_body( $body ) {
|
||||
|
||||
return json_decode( $body, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a JSON response given an array of data
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $data the response data
|
||||
* @return string
|
||||
*/
|
||||
public function generate_response( $data ) {
|
||||
if ( isset( $_GET['_jsonp'] ) ) {
|
||||
|
||||
if ( ! apply_filters( 'woocommerce_api_jsonp_enabled', true ) ) {
|
||||
WC()->api->server->send_status( 400 );
|
||||
return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) ) );
|
||||
}
|
||||
|
||||
$jsonp_callback = $_GET['_jsonp'];
|
||||
|
||||
if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) {
|
||||
WC()->api->server->send_status( 400 );
|
||||
return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ) );
|
||||
}
|
||||
|
||||
WC()->api->server->header( 'X-Content-Type-Options', 'nosniff' );
|
||||
|
||||
// Prepend '/**/' to mitigate possible JSONP Flash attacks.
|
||||
// https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
|
||||
return '/**/' . $jsonp_callback . '(' . wp_json_encode( $data ) . ')';
|
||||
}
|
||||
|
||||
return wp_json_encode( $data );
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,329 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API Reports Class
|
||||
*
|
||||
* Handles requests to the /reports endpoint
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_Reports extends WC_API_Resource {
|
||||
|
||||
/** @var string $base the route base */
|
||||
protected $base = '/reports';
|
||||
|
||||
/** @var WC_Admin_Report instance */
|
||||
private $report;
|
||||
|
||||
/**
|
||||
* Register the routes for this class
|
||||
*
|
||||
* GET /reports
|
||||
* GET /reports/sales
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $routes
|
||||
* @return array
|
||||
*/
|
||||
public function register_routes( $routes ) {
|
||||
|
||||
# GET /reports
|
||||
$routes[ $this->base ] = array(
|
||||
array( array( $this, 'get_reports' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# GET /reports/sales
|
||||
$routes[ $this->base . '/sales' ] = array(
|
||||
array( array( $this, 'get_sales_report' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# GET /reports/sales/top_sellers
|
||||
$routes[ $this->base . '/sales/top_sellers' ] = array(
|
||||
array( array( $this, 'get_top_sellers_report' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a simple listing of available reports
|
||||
*
|
||||
* @since 2.1
|
||||
* @return array
|
||||
*/
|
||||
public function get_reports() {
|
||||
return array( 'reports' => array( 'sales', 'sales/top_sellers' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sales report
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $fields fields to include in response
|
||||
* @param array $filter date filtering
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_sales_report( $fields = null, $filter = array() ) {
|
||||
|
||||
// check user permissions
|
||||
$check = $this->validate_request();
|
||||
|
||||
// check for WP_Error
|
||||
if ( is_wp_error( $check ) ) {
|
||||
return $check;
|
||||
}
|
||||
|
||||
// set date filtering
|
||||
$this->setup_report( $filter );
|
||||
|
||||
// new customers
|
||||
$users_query = new WP_User_Query(
|
||||
array(
|
||||
'fields' => array( 'user_registered' ),
|
||||
'role' => 'customer',
|
||||
)
|
||||
);
|
||||
|
||||
$customers = $users_query->get_results();
|
||||
|
||||
foreach ( $customers as $key => $customer ) {
|
||||
if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) {
|
||||
unset( $customers[ $key ] );
|
||||
}
|
||||
}
|
||||
|
||||
$total_customers = count( $customers );
|
||||
$report_data = $this->report->get_report_data();
|
||||
$period_totals = array();
|
||||
|
||||
// setup period totals by ensuring each period in the interval has data
|
||||
for ( $i = 0; $i <= $this->report->chart_interval; $i ++ ) {
|
||||
|
||||
switch ( $this->report->chart_groupby ) {
|
||||
case 'day' :
|
||||
$time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) );
|
||||
break;
|
||||
default :
|
||||
$time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) );
|
||||
break;
|
||||
}
|
||||
|
||||
// set the customer signups for each period
|
||||
$customer_count = 0;
|
||||
foreach ( $customers as $customer ) {
|
||||
if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) {
|
||||
$customer_count++;
|
||||
}
|
||||
}
|
||||
|
||||
$period_totals[ $time ] = array(
|
||||
'sales' => wc_format_decimal( 0.00, 2 ),
|
||||
'orders' => 0,
|
||||
'items' => 0,
|
||||
'tax' => wc_format_decimal( 0.00, 2 ),
|
||||
'shipping' => wc_format_decimal( 0.00, 2 ),
|
||||
'discount' => wc_format_decimal( 0.00, 2 ),
|
||||
'customers' => $customer_count,
|
||||
);
|
||||
}
|
||||
|
||||
// add total sales, total order count, total tax and total shipping for each period
|
||||
foreach ( $report_data->orders as $order ) {
|
||||
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );
|
||||
|
||||
if ( ! isset( $period_totals[ $time ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$period_totals[ $time ]['sales'] = wc_format_decimal( $order->total_sales, 2 );
|
||||
$period_totals[ $time ]['tax'] = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 );
|
||||
$period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 );
|
||||
}
|
||||
|
||||
foreach ( $report_data->order_counts as $order ) {
|
||||
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );
|
||||
|
||||
if ( ! isset( $period_totals[ $time ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$period_totals[ $time ]['orders'] = (int) $order->count;
|
||||
}
|
||||
|
||||
// add total order items for each period
|
||||
foreach ( $report_data->order_items as $order_item ) {
|
||||
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) );
|
||||
|
||||
if ( ! isset( $period_totals[ $time ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$period_totals[ $time ]['items'] = (int) $order_item->order_item_count;
|
||||
}
|
||||
|
||||
// add total discount for each period
|
||||
foreach ( $report_data->coupons as $discount ) {
|
||||
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) );
|
||||
|
||||
if ( ! isset( $period_totals[ $time ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 );
|
||||
}
|
||||
|
||||
$sales_data = array(
|
||||
'total_sales' => $report_data->total_sales,
|
||||
'net_sales' => $report_data->net_sales,
|
||||
'average_sales' => $report_data->average_sales,
|
||||
'total_orders' => $report_data->total_orders,
|
||||
'total_items' => $report_data->total_items,
|
||||
'total_tax' => wc_format_decimal( $report_data->total_tax + $report_data->total_shipping_tax, 2 ),
|
||||
'total_shipping' => $report_data->total_shipping,
|
||||
'total_refunds' => $report_data->total_refunds,
|
||||
'total_discount' => $report_data->total_coupons,
|
||||
'totals_grouped_by' => $this->report->chart_groupby,
|
||||
'totals' => $period_totals,
|
||||
'total_customers' => $total_customers,
|
||||
);
|
||||
|
||||
return array( 'sales' => apply_filters( 'woocommerce_api_report_response', $sales_data, $this->report, $fields, $this->server ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the top sellers report
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $fields fields to include in response
|
||||
* @param array $filter date filtering
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_top_sellers_report( $fields = null, $filter = array() ) {
|
||||
|
||||
// check user permissions
|
||||
$check = $this->validate_request();
|
||||
|
||||
if ( is_wp_error( $check ) ) {
|
||||
return $check;
|
||||
}
|
||||
|
||||
// set date filtering
|
||||
$this->setup_report( $filter );
|
||||
|
||||
$top_sellers = $this->report->get_order_report_data( array(
|
||||
'data' => array(
|
||||
'_product_id' => array(
|
||||
'type' => 'order_item_meta',
|
||||
'order_item_type' => 'line_item',
|
||||
'function' => '',
|
||||
'name' => 'product_id',
|
||||
),
|
||||
'_qty' => array(
|
||||
'type' => 'order_item_meta',
|
||||
'order_item_type' => 'line_item',
|
||||
'function' => 'SUM',
|
||||
'name' => 'order_item_qty',
|
||||
),
|
||||
),
|
||||
'order_by' => 'order_item_qty DESC',
|
||||
'group_by' => 'product_id',
|
||||
'limit' => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12,
|
||||
'query_type' => 'get_results',
|
||||
'filter_range' => true,
|
||||
) );
|
||||
|
||||
$top_sellers_data = array();
|
||||
|
||||
foreach ( $top_sellers as $top_seller ) {
|
||||
|
||||
$product = wc_get_product( $top_seller->product_id );
|
||||
|
||||
if ( $product ) {
|
||||
$top_sellers_data[] = array(
|
||||
'title' => $product->get_name(),
|
||||
'product_id' => $top_seller->product_id,
|
||||
'quantity' => $top_seller->order_item_qty,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return array( 'top_sellers' => apply_filters( 'woocommerce_api_report_response', $top_sellers_data, $this->report, $fields, $this->server ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the report object and parse any date filtering
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $filter date filtering
|
||||
*/
|
||||
private function setup_report( $filter ) {
|
||||
|
||||
include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' );
|
||||
include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-report-sales-by-date.php' );
|
||||
|
||||
$this->report = new WC_Report_Sales_By_Date();
|
||||
|
||||
if ( empty( $filter['period'] ) ) {
|
||||
|
||||
// custom date range
|
||||
$filter['period'] = 'custom';
|
||||
|
||||
if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) {
|
||||
|
||||
// overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges
|
||||
$_GET['start_date'] = $this->server->parse_datetime( $filter['date_min'] );
|
||||
$_GET['end_date'] = isset( $filter['date_max'] ) ? $this->server->parse_datetime( $filter['date_max'] ) : null;
|
||||
|
||||
} else {
|
||||
|
||||
// default custom range to today
|
||||
$_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) );
|
||||
}
|
||||
} else {
|
||||
|
||||
// ensure period is valid
|
||||
if ( ! in_array( $filter['period'], array( 'week', 'month', 'last_month', 'year' ) ) ) {
|
||||
$filter['period'] = 'week';
|
||||
}
|
||||
|
||||
// TODO: change WC_Admin_Report class to use "week" instead, as it's more consistent with other periods
|
||||
// allow "week" for period instead of "7day"
|
||||
if ( 'week' === $filter['period'] ) {
|
||||
$filter['period'] = '7day';
|
||||
}
|
||||
}
|
||||
|
||||
$this->report->calculate_current_range( $filter['period'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the current user has permission to view reports
|
||||
*
|
||||
* @since 2.1
|
||||
* @see WC_API_Resource::validate_request()
|
||||
*
|
||||
* @param null $id unused
|
||||
* @param null $type unused
|
||||
* @param null $context unused
|
||||
*
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
protected function validate_request( $id = null, $type = null, $context = null ) {
|
||||
|
||||
if ( ! current_user_can( 'view_woocommerce_reports' ) ) {
|
||||
|
||||
return new WP_Error( 'woocommerce_api_user_cannot_read_report', __( 'You do not have permission to read this report', 'woocommerce' ), array( 'status' => 401 ) );
|
||||
|
||||
} else {
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,466 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API Resource class
|
||||
*
|
||||
* Provides shared functionality for resource-specific API classes
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_Resource {
|
||||
|
||||
/** @var WC_API_Server the API server */
|
||||
protected $server;
|
||||
|
||||
/** @var string sub-classes override this to set a resource-specific base route */
|
||||
protected $base;
|
||||
|
||||
/**
|
||||
* Setup class
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WC_API_Server $server
|
||||
*/
|
||||
public function __construct( WC_API_Server $server ) {
|
||||
|
||||
$this->server = $server;
|
||||
|
||||
// automatically register routes for sub-classes
|
||||
add_filter( 'woocommerce_api_endpoints', array( $this, 'register_routes' ) );
|
||||
|
||||
// maybe add meta to top-level resource responses
|
||||
foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) {
|
||||
add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 );
|
||||
}
|
||||
|
||||
$response_names = array(
|
||||
'order',
|
||||
'coupon',
|
||||
'customer',
|
||||
'product',
|
||||
'report',
|
||||
'customer_orders',
|
||||
'customer_downloads',
|
||||
'order_note',
|
||||
'order_refund',
|
||||
'product_reviews',
|
||||
'product_category',
|
||||
);
|
||||
|
||||
foreach ( $response_names as $name ) {
|
||||
|
||||
/**
|
||||
* Remove fields from responses when requests specify certain fields
|
||||
* note these are hooked at a later priority so data added via
|
||||
* filters (e.g. customer data to the order response) still has the
|
||||
* fields filtered properly
|
||||
*/
|
||||
add_filter( "woocommerce_api_{$name}_response", array( $this, 'filter_response_fields' ), 20, 3 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the request by checking:
|
||||
*
|
||||
* 1) the ID is a valid integer
|
||||
* 2) the ID returns a valid post object and matches the provided post type
|
||||
* 3) the current user has the proper permissions to read/edit/delete the post
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string|int $id the post ID
|
||||
* @param string $type the post type, either `shop_order`, `shop_coupon`, or `product`
|
||||
* @param string $context the context of the request, either `read`, `edit` or `delete`
|
||||
* @return int|WP_Error valid post ID or WP_Error if any of the checks fails
|
||||
*/
|
||||
protected function validate_request( $id, $type, $context ) {
|
||||
|
||||
if ( 'shop_order' === $type || 'shop_coupon' === $type || 'shop_webhook' === $type ) {
|
||||
$resource_name = str_replace( 'shop_', '', $type );
|
||||
} else {
|
||||
$resource_name = $type;
|
||||
}
|
||||
|
||||
$id = absint( $id );
|
||||
|
||||
// Validate ID
|
||||
if ( empty( $id ) ) {
|
||||
return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
// Only custom post types have per-post type/permission checks
|
||||
if ( 'customer' !== $type ) {
|
||||
|
||||
$post = get_post( $id );
|
||||
|
||||
if ( null === $post ) {
|
||||
return new WP_Error( "woocommerce_api_no_{$resource_name}_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), $resource_name, $id ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
// For checking permissions, product variations are the same as the product post type
|
||||
$post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type;
|
||||
|
||||
// Validate post type
|
||||
if ( $type !== $post_type ) {
|
||||
return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
// Validate permissions
|
||||
switch ( $context ) {
|
||||
|
||||
case 'read':
|
||||
if ( ! $this->is_readable( $post ) ) {
|
||||
return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'edit':
|
||||
if ( ! $this->is_editable( $post ) ) {
|
||||
return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
if ( ! $this->is_deletable( $post ) ) {
|
||||
return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add common request arguments to argument list before WP_Query is run
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $base_args required arguments for the query (e.g. `post_type`, etc)
|
||||
* @param array $request_args arguments provided in the request
|
||||
* @return array
|
||||
*/
|
||||
protected function merge_query_args( $base_args, $request_args ) {
|
||||
|
||||
$args = array();
|
||||
|
||||
// date
|
||||
if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) {
|
||||
|
||||
$args['date_query'] = array();
|
||||
|
||||
// resources created after specified date
|
||||
if ( ! empty( $request_args['created_at_min'] ) ) {
|
||||
$args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true );
|
||||
}
|
||||
|
||||
// resources created before specified date
|
||||
if ( ! empty( $request_args['created_at_max'] ) ) {
|
||||
$args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true );
|
||||
}
|
||||
|
||||
// resources updated after specified date
|
||||
if ( ! empty( $request_args['updated_at_min'] ) ) {
|
||||
$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true );
|
||||
}
|
||||
|
||||
// resources updated before specified date
|
||||
if ( ! empty( $request_args['updated_at_max'] ) ) {
|
||||
$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true );
|
||||
}
|
||||
}
|
||||
|
||||
// search
|
||||
if ( ! empty( $request_args['q'] ) ) {
|
||||
$args['s'] = $request_args['q'];
|
||||
}
|
||||
|
||||
// resources per response
|
||||
if ( ! empty( $request_args['limit'] ) ) {
|
||||
$args['posts_per_page'] = $request_args['limit'];
|
||||
}
|
||||
|
||||
// resource offset
|
||||
if ( ! empty( $request_args['offset'] ) ) {
|
||||
$args['offset'] = $request_args['offset'];
|
||||
}
|
||||
|
||||
// order (ASC or DESC, ASC by default)
|
||||
if ( ! empty( $request_args['order'] ) ) {
|
||||
$args['order'] = $request_args['order'];
|
||||
}
|
||||
|
||||
// orderby
|
||||
if ( ! empty( $request_args['orderby'] ) ) {
|
||||
$args['orderby'] = $request_args['orderby'];
|
||||
|
||||
// allow sorting by meta value
|
||||
if ( ! empty( $request_args['orderby_meta_key'] ) ) {
|
||||
$args['meta_key'] = $request_args['orderby_meta_key'];
|
||||
}
|
||||
}
|
||||
|
||||
// allow post status change
|
||||
if ( ! empty( $request_args['post_status'] ) ) {
|
||||
$args['post_status'] = $request_args['post_status'];
|
||||
unset( $request_args['post_status'] );
|
||||
}
|
||||
|
||||
// filter by a list of post id
|
||||
if ( ! empty( $request_args['in'] ) ) {
|
||||
$args['post__in'] = explode( ',', $request_args['in'] );
|
||||
unset( $request_args['in'] );
|
||||
}
|
||||
|
||||
// filter by a list of post id
|
||||
if ( ! empty( $request_args['in'] ) ) {
|
||||
$args['post__in'] = explode( ',', $request_args['in'] );
|
||||
unset( $request_args['in'] );
|
||||
}
|
||||
|
||||
// resource page
|
||||
$args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1;
|
||||
|
||||
$args = apply_filters( 'woocommerce_api_query_args', $args, $request_args );
|
||||
|
||||
return array_merge( $base_args, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add meta to resources when requested by the client. Meta is added as a top-level
|
||||
* `<resource_name>_meta` attribute (e.g. `order_meta`) as a list of key/value pairs
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $data the resource data
|
||||
* @param object $resource the resource object (e.g WC_Order)
|
||||
* @return mixed
|
||||
*/
|
||||
public function maybe_add_meta( $data, $resource ) {
|
||||
|
||||
if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) {
|
||||
|
||||
// don't attempt to add meta more than once
|
||||
if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
// define the top-level property name for the meta
|
||||
switch ( get_class( $resource ) ) {
|
||||
|
||||
case 'WC_Order':
|
||||
$meta_name = 'order_meta';
|
||||
break;
|
||||
|
||||
case 'WC_Coupon':
|
||||
$meta_name = 'coupon_meta';
|
||||
break;
|
||||
|
||||
case 'WP_User':
|
||||
$meta_name = 'customer_meta';
|
||||
break;
|
||||
|
||||
default:
|
||||
$meta_name = 'product_meta';
|
||||
break;
|
||||
}
|
||||
|
||||
if ( is_a( $resource, 'WP_User' ) ) {
|
||||
|
||||
// customer meta
|
||||
$meta = (array) get_user_meta( $resource->ID );
|
||||
|
||||
} else {
|
||||
|
||||
// coupon/order/product meta
|
||||
$meta = (array) get_post_meta( $resource->get_id() );
|
||||
}
|
||||
|
||||
foreach ( $meta as $meta_key => $meta_value ) {
|
||||
|
||||
// don't add hidden meta by default
|
||||
if ( ! is_protected_meta( $meta_key ) ) {
|
||||
$data[ $meta_name ][ $meta_key ] = maybe_unserialize( $meta_value[0] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restrict the fields included in the response if the request specified certain only certain fields should be returned
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $data the response data
|
||||
* @param object $resource the object that provided the response data, e.g. WC_Coupon or WC_Order
|
||||
* @param array|string the requested list of fields to include in the response
|
||||
* @return array response data
|
||||
*/
|
||||
public function filter_response_fields( $data, $resource, $fields ) {
|
||||
|
||||
if ( ! is_array( $data ) || empty( $fields ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$fields = explode( ',', $fields );
|
||||
$sub_fields = array();
|
||||
|
||||
// get sub fields
|
||||
foreach ( $fields as $field ) {
|
||||
|
||||
if ( false !== strpos( $field, '.' ) ) {
|
||||
|
||||
list( $name, $value ) = explode( '.', $field );
|
||||
|
||||
$sub_fields[ $name ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// iterate through top-level fields
|
||||
foreach ( $data as $data_field => $data_value ) {
|
||||
|
||||
// if a field has sub-fields and the top-level field has sub-fields to filter
|
||||
if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) {
|
||||
|
||||
// iterate through each sub-field
|
||||
foreach ( $data_value as $sub_field => $sub_field_value ) {
|
||||
|
||||
// remove non-matching sub-fields
|
||||
if ( ! in_array( $sub_field, $sub_fields ) ) {
|
||||
unset( $data[ $data_field ][ $sub_field ] );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// remove non-matching top-level fields
|
||||
if ( ! in_array( $data_field, $fields ) ) {
|
||||
unset( $data[ $data_field ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a given resource
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int $id the resource ID
|
||||
* @param string $type the resource post type, or `customer`
|
||||
* @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`)
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
protected function delete( $id, $type, $force = false ) {
|
||||
|
||||
if ( 'shop_order' === $type || 'shop_coupon' === $type ) {
|
||||
$resource_name = str_replace( 'shop_', '', $type );
|
||||
} else {
|
||||
$resource_name = $type;
|
||||
}
|
||||
|
||||
if ( 'customer' === $type ) {
|
||||
|
||||
$result = wp_delete_user( $id );
|
||||
|
||||
if ( $result ) {
|
||||
return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) );
|
||||
} else {
|
||||
return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) );
|
||||
}
|
||||
} else {
|
||||
|
||||
// delete order/coupon/webhook
|
||||
$result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id );
|
||||
|
||||
if ( ! $result ) {
|
||||
return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) );
|
||||
}
|
||||
|
||||
if ( $force ) {
|
||||
return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) );
|
||||
} else {
|
||||
$this->server->send_status( '202' );
|
||||
|
||||
return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the given post is readable by the current user
|
||||
*
|
||||
* @since 2.1
|
||||
* @see WC_API_Resource::check_permission()
|
||||
* @param WP_Post|int $post
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_readable( $post ) {
|
||||
|
||||
return $this->check_permission( $post, 'read' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given post is editable by the current user
|
||||
*
|
||||
* @since 2.1
|
||||
* @see WC_API_Resource::check_permission()
|
||||
* @param WP_Post|int $post
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_editable( $post ) {
|
||||
|
||||
return $this->check_permission( $post, 'edit' );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given post is deletable by the current user
|
||||
*
|
||||
* @since 2.1
|
||||
* @see WC_API_Resource::check_permission()
|
||||
* @param WP_Post|int $post
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_deletable( $post ) {
|
||||
|
||||
return $this->check_permission( $post, 'delete' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the permissions for the current user given a post and context
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WP_Post|int $post
|
||||
* @param string $context the type of permission to check, either `read`, `write`, or `delete`
|
||||
* @return bool true if the current user has the permissions to perform the context on the post
|
||||
*/
|
||||
private function check_permission( $post, $context ) {
|
||||
|
||||
if ( ! is_a( $post, 'WP_Post' ) ) {
|
||||
$post = get_post( $post );
|
||||
}
|
||||
|
||||
if ( is_null( $post ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$post_type = get_post_type_object( $post->post_type );
|
||||
|
||||
if ( 'read' === $context ) {
|
||||
return ( 'revision' !== $post->post_type && current_user_can( $post_type->cap->read_private_posts, $post->ID ) );
|
||||
} elseif ( 'edit' === $context ) {
|
||||
return current_user_can( $post_type->cap->edit_post, $post->ID );
|
||||
} elseif ( 'delete' === $context ) {
|
||||
return current_user_can( $post_type->cap->delete_post, $post->ID );
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,775 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API
|
||||
*
|
||||
* Handles REST API requests
|
||||
*
|
||||
* This class and related code (JSON response handler, resource classes) are based on WP-API v0.6 (https://github.com/WP-API/WP-API)
|
||||
* Many thanks to Ryan McCue and any other contributors!
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/admin.php';
|
||||
|
||||
class WC_API_Server {
|
||||
|
||||
const METHOD_GET = 1;
|
||||
const METHOD_POST = 2;
|
||||
const METHOD_PUT = 4;
|
||||
const METHOD_PATCH = 8;
|
||||
const METHOD_DELETE = 16;
|
||||
|
||||
const READABLE = 1; // GET
|
||||
const CREATABLE = 2; // POST
|
||||
const EDITABLE = 14; // POST | PUT | PATCH
|
||||
const DELETABLE = 16; // DELETE
|
||||
const ALLMETHODS = 31; // GET | POST | PUT | PATCH | DELETE
|
||||
|
||||
/**
|
||||
* Does the endpoint accept a raw request body?
|
||||
*/
|
||||
const ACCEPT_RAW_DATA = 64;
|
||||
|
||||
/** Does the endpoint accept a request body? (either JSON or XML) */
|
||||
const ACCEPT_DATA = 128;
|
||||
|
||||
/**
|
||||
* Should we hide this endpoint from the index?
|
||||
*/
|
||||
const HIDDEN_ENDPOINT = 256;
|
||||
|
||||
/**
|
||||
* Map of HTTP verbs to constants
|
||||
* @var array
|
||||
*/
|
||||
public static $method_map = array(
|
||||
'HEAD' => self::METHOD_GET,
|
||||
'GET' => self::METHOD_GET,
|
||||
'POST' => self::METHOD_POST,
|
||||
'PUT' => self::METHOD_PUT,
|
||||
'PATCH' => self::METHOD_PATCH,
|
||||
'DELETE' => self::METHOD_DELETE,
|
||||
);
|
||||
|
||||
/**
|
||||
* Requested path (relative to the API root, wp-json.php)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $path = '';
|
||||
|
||||
/**
|
||||
* Requested method (GET/HEAD/POST/PUT/PATCH/DELETE)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $method = 'HEAD';
|
||||
|
||||
/**
|
||||
* Request parameters
|
||||
*
|
||||
* This acts as an abstraction of the superglobals
|
||||
* (GET => $_GET, POST => $_POST)
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $params = array( 'GET' => array(), 'POST' => array() );
|
||||
|
||||
/**
|
||||
* Request headers
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $headers = array();
|
||||
|
||||
/**
|
||||
* Request files (matches $_FILES)
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $files = array();
|
||||
|
||||
/**
|
||||
* Request/Response handler, either JSON by default
|
||||
* or XML if requested by client
|
||||
*
|
||||
* @var WC_API_Handler
|
||||
*/
|
||||
public $handler;
|
||||
|
||||
|
||||
/**
|
||||
* Setup class and set request/response handler
|
||||
*
|
||||
* @since 2.1
|
||||
* @param $path
|
||||
*/
|
||||
public function __construct( $path ) {
|
||||
|
||||
if ( empty( $path ) ) {
|
||||
if ( isset( $_SERVER['PATH_INFO'] ) ) {
|
||||
$path = $_SERVER['PATH_INFO'];
|
||||
} else {
|
||||
$path = '/';
|
||||
}
|
||||
}
|
||||
|
||||
$this->path = $path;
|
||||
$this->method = $_SERVER['REQUEST_METHOD'];
|
||||
$this->params['GET'] = $_GET;
|
||||
$this->params['POST'] = $_POST;
|
||||
$this->headers = $this->get_headers( $_SERVER );
|
||||
$this->files = $_FILES;
|
||||
|
||||
// Compatibility for clients that can't use PUT/PATCH/DELETE
|
||||
if ( isset( $_GET['_method'] ) ) {
|
||||
$this->method = strtoupper( $_GET['_method'] );
|
||||
} elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
|
||||
$this->method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
|
||||
}
|
||||
|
||||
// load response handler
|
||||
$handler_class = apply_filters( 'woocommerce_api_default_response_handler', 'WC_API_JSON_Handler', $this->path, $this );
|
||||
|
||||
$this->handler = new $handler_class();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check authentication for the request
|
||||
*
|
||||
* @since 2.1
|
||||
* @return WP_User|WP_Error WP_User object indicates successful login, WP_Error indicates unsuccessful login
|
||||
*/
|
||||
public function check_authentication() {
|
||||
|
||||
// allow plugins to remove default authentication or add their own authentication
|
||||
$user = apply_filters( 'woocommerce_api_check_authentication', null, $this );
|
||||
|
||||
if ( is_a( $user, 'WP_User' ) ) {
|
||||
|
||||
// API requests run under the context of the authenticated user
|
||||
wp_set_current_user( $user->ID );
|
||||
|
||||
} elseif ( ! is_wp_error( $user ) ) {
|
||||
|
||||
// WP_Errors are handled in serve_request()
|
||||
$user = new WP_Error( 'woocommerce_api_authentication_error', __( 'Invalid authentication method', 'woocommerce' ), array( 'code' => 500 ) );
|
||||
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an error to an array
|
||||
*
|
||||
* This iterates over all error codes and messages to change it into a flat
|
||||
* array. This enables simpler client behaviour, as it is represented as a
|
||||
* list in JSON rather than an object/map
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WP_Error $error
|
||||
* @return array List of associative arrays with code and message keys
|
||||
*/
|
||||
protected function error_to_array( $error ) {
|
||||
$errors = array();
|
||||
foreach ( (array) $error->errors as $code => $messages ) {
|
||||
foreach ( (array) $messages as $message ) {
|
||||
$errors[] = array( 'code' => $code, 'message' => $message );
|
||||
}
|
||||
}
|
||||
|
||||
return array( 'errors' => $errors );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle serving an API request
|
||||
*
|
||||
* Matches the current server URI to a route and runs the first matching
|
||||
* callback then outputs a JSON representation of the returned value.
|
||||
*
|
||||
* @since 2.1
|
||||
* @uses WC_API_Server::dispatch()
|
||||
*/
|
||||
public function serve_request() {
|
||||
|
||||
do_action( 'woocommerce_api_server_before_serve', $this );
|
||||
|
||||
$this->header( 'Content-Type', $this->handler->get_content_type(), true );
|
||||
|
||||
// the API is enabled by default
|
||||
if ( ! apply_filters( 'woocommerce_api_enabled', true, $this ) || ( 'no' === get_option( 'woocommerce_api_enabled' ) ) ) {
|
||||
|
||||
$this->send_status( 404 );
|
||||
|
||||
echo $this->handler->generate_response( array( 'errors' => array( 'code' => 'woocommerce_api_disabled', 'message' => 'The WooCommerce API is disabled on this site' ) ) );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->check_authentication();
|
||||
|
||||
// if authorization check was successful, dispatch the request
|
||||
if ( ! is_wp_error( $result ) ) {
|
||||
$result = $this->dispatch();
|
||||
}
|
||||
|
||||
// handle any dispatch errors
|
||||
if ( is_wp_error( $result ) ) {
|
||||
$data = $result->get_error_data();
|
||||
if ( is_array( $data ) && isset( $data['status'] ) ) {
|
||||
$this->send_status( $data['status'] );
|
||||
}
|
||||
|
||||
$result = $this->error_to_array( $result );
|
||||
}
|
||||
|
||||
// This is a filter rather than an action, since this is designed to be
|
||||
// re-entrant if needed
|
||||
$served = apply_filters( 'woocommerce_api_serve_request', false, $result, $this );
|
||||
|
||||
if ( ! $served ) {
|
||||
|
||||
if ( 'HEAD' === $this->method ) {
|
||||
return;
|
||||
}
|
||||
|
||||
echo $this->handler->generate_response( $result );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the route map
|
||||
*
|
||||
* The route map is an associative array with path regexes as the keys. The
|
||||
* value is an indexed array with the callback function/method as the first
|
||||
* item, and a bitmask of HTTP methods as the second item (see the class
|
||||
* constants).
|
||||
*
|
||||
* Each route can be mapped to more than one callback by using an array of
|
||||
* the indexed arrays. This allows mapping e.g. GET requests to one callback
|
||||
* and POST requests to another.
|
||||
*
|
||||
* Note that the path regexes (array keys) must have @ escaped, as this is
|
||||
* used as the delimiter with preg_match()
|
||||
*
|
||||
* @since 2.1
|
||||
* @return array `'/path/regex' => array( $callback, $bitmask )` or `'/path/regex' => array( array( $callback, $bitmask ), ...)`
|
||||
*/
|
||||
public function get_routes() {
|
||||
|
||||
// index added by default
|
||||
$endpoints = array(
|
||||
|
||||
'/' => array( array( $this, 'get_index' ), self::READABLE ),
|
||||
);
|
||||
|
||||
$endpoints = apply_filters( 'woocommerce_api_endpoints', $endpoints );
|
||||
|
||||
// Normalise the endpoints
|
||||
foreach ( $endpoints as $route => &$handlers ) {
|
||||
if ( count( $handlers ) <= 2 && isset( $handlers[1] ) && ! is_array( $handlers[1] ) ) {
|
||||
$handlers = array( $handlers );
|
||||
}
|
||||
}
|
||||
|
||||
return $endpoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match the request to a callback and call it
|
||||
*
|
||||
* @since 2.1
|
||||
* @return mixed The value returned by the callback, or a WP_Error instance
|
||||
*/
|
||||
public function dispatch() {
|
||||
|
||||
switch ( $this->method ) {
|
||||
|
||||
case 'HEAD' :
|
||||
case 'GET' :
|
||||
$method = self::METHOD_GET;
|
||||
break;
|
||||
|
||||
case 'POST' :
|
||||
$method = self::METHOD_POST;
|
||||
break;
|
||||
|
||||
case 'PUT' :
|
||||
$method = self::METHOD_PUT;
|
||||
break;
|
||||
|
||||
case 'PATCH' :
|
||||
$method = self::METHOD_PATCH;
|
||||
break;
|
||||
|
||||
case 'DELETE' :
|
||||
$method = self::METHOD_DELETE;
|
||||
break;
|
||||
|
||||
default :
|
||||
return new WP_Error( 'woocommerce_api_unsupported_method', __( 'Unsupported request method', 'woocommerce' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
foreach ( $this->get_routes() as $route => $handlers ) {
|
||||
foreach ( $handlers as $handler ) {
|
||||
$callback = $handler[0];
|
||||
$supported = isset( $handler[1] ) ? $handler[1] : self::METHOD_GET;
|
||||
|
||||
if ( ! ( $supported & $method ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$match = preg_match( '@^' . $route . '$@i', urldecode( $this->path ), $args );
|
||||
|
||||
if ( ! $match ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! is_callable( $callback ) ) {
|
||||
return new WP_Error( 'woocommerce_api_invalid_handler', __( 'The handler for the route is invalid', 'woocommerce' ), array( 'status' => 500 ) );
|
||||
}
|
||||
|
||||
$args = array_merge( $args, $this->params['GET'] );
|
||||
if ( $method & self::METHOD_POST ) {
|
||||
$args = array_merge( $args, $this->params['POST'] );
|
||||
}
|
||||
if ( $supported & self::ACCEPT_DATA ) {
|
||||
$data = $this->handler->parse_body( $this->get_raw_data() );
|
||||
$args = array_merge( $args, array( 'data' => $data ) );
|
||||
} elseif ( $supported & self::ACCEPT_RAW_DATA ) {
|
||||
$data = $this->get_raw_data();
|
||||
$args = array_merge( $args, array( 'data' => $data ) );
|
||||
}
|
||||
|
||||
$args['_method'] = $method;
|
||||
$args['_route'] = $route;
|
||||
$args['_path'] = $this->path;
|
||||
$args['_headers'] = $this->headers;
|
||||
$args['_files'] = $this->files;
|
||||
|
||||
$args = apply_filters( 'woocommerce_api_dispatch_args', $args, $callback );
|
||||
|
||||
// Allow plugins to halt the request via this filter
|
||||
if ( is_wp_error( $args ) ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
$params = $this->sort_callback_params( $callback, $args );
|
||||
if ( is_wp_error( $params ) ) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
return call_user_func_array( $callback, $params );
|
||||
}
|
||||
}
|
||||
|
||||
return new WP_Error( 'woocommerce_api_no_route', __( 'No route was found matching the URL and request method', 'woocommerce' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* urldecode deep.
|
||||
*
|
||||
* @since 2.2
|
||||
* @param string|array $value Data to decode with urldecode.
|
||||
* @return string|array Decoded data.
|
||||
*/
|
||||
protected function urldecode_deep( $value ) {
|
||||
if ( is_array( $value ) ) {
|
||||
return array_map( array( $this, 'urldecode_deep' ), $value );
|
||||
} else {
|
||||
return urldecode( $value );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort parameters by order specified in method declaration
|
||||
*
|
||||
* Takes a callback and a list of available params, then filters and sorts
|
||||
* by the parameters the method actually needs, using the Reflection API
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @param callable|array $callback the endpoint callback
|
||||
* @param array $provided the provided request parameters
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
protected function sort_callback_params( $callback, $provided ) {
|
||||
if ( is_array( $callback ) ) {
|
||||
$ref_func = new ReflectionMethod( $callback[0], $callback[1] );
|
||||
} else {
|
||||
$ref_func = new ReflectionFunction( $callback );
|
||||
}
|
||||
|
||||
$wanted = $ref_func->getParameters();
|
||||
$ordered_parameters = array();
|
||||
|
||||
foreach ( $wanted as $param ) {
|
||||
if ( isset( $provided[ $param->getName() ] ) ) {
|
||||
// We have this parameters in the list to choose from
|
||||
if ( 'data' == $param->getName() ) {
|
||||
$ordered_parameters[] = $provided[ $param->getName() ];
|
||||
continue;
|
||||
}
|
||||
|
||||
$ordered_parameters[] = $this->urldecode_deep( $provided[ $param->getName() ] );
|
||||
} elseif ( $param->isDefaultValueAvailable() ) {
|
||||
// We don't have this parameter, but it's optional
|
||||
$ordered_parameters[] = $param->getDefaultValue();
|
||||
} else {
|
||||
// We don't have this parameter and it wasn't optional, abort!
|
||||
return new WP_Error( 'woocommerce_api_missing_callback_param', sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param->getName() ), array( 'status' => 400 ) );
|
||||
}
|
||||
}
|
||||
|
||||
return $ordered_parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the site index.
|
||||
*
|
||||
* This endpoint describes the capabilities of the site.
|
||||
*
|
||||
* @since 2.3
|
||||
* @return array Index entity
|
||||
*/
|
||||
public function get_index() {
|
||||
|
||||
// General site data
|
||||
$available = array(
|
||||
'store' => array(
|
||||
'name' => get_option( 'blogname' ),
|
||||
'description' => get_option( 'blogdescription' ),
|
||||
'URL' => get_option( 'siteurl' ),
|
||||
'wc_version' => WC()->version,
|
||||
'routes' => array(),
|
||||
'meta' => array(
|
||||
'timezone' => wc_timezone_string(),
|
||||
'currency' => get_woocommerce_currency(),
|
||||
'currency_format' => get_woocommerce_currency_symbol(),
|
||||
'currency_position' => get_option( 'woocommerce_currency_pos' ),
|
||||
'thousand_separator' => get_option( 'woocommerce_price_thousand_sep' ),
|
||||
'decimal_separator' => get_option( 'woocommerce_price_decimal_sep' ),
|
||||
'price_num_decimals' => wc_get_price_decimals(),
|
||||
'tax_included' => wc_prices_include_tax(),
|
||||
'weight_unit' => get_option( 'woocommerce_weight_unit' ),
|
||||
'dimension_unit' => get_option( 'woocommerce_dimension_unit' ),
|
||||
'ssl_enabled' => ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ),
|
||||
'permalinks_enabled' => ( '' !== get_option( 'permalink_structure' ) ),
|
||||
'generate_password' => ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) ),
|
||||
'links' => array(
|
||||
'help' => 'https://woocommerce.github.io/woocommerce-rest-api-docs/',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Find the available routes
|
||||
foreach ( $this->get_routes() as $route => $callbacks ) {
|
||||
$data = array();
|
||||
|
||||
$route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route );
|
||||
|
||||
foreach ( self::$method_map as $name => $bitmask ) {
|
||||
foreach ( $callbacks as $callback ) {
|
||||
// Skip to the next route if any callback is hidden
|
||||
if ( $callback[1] & self::HIDDEN_ENDPOINT ) {
|
||||
continue 3;
|
||||
}
|
||||
|
||||
if ( $callback[1] & $bitmask ) {
|
||||
$data['supports'][] = $name;
|
||||
}
|
||||
|
||||
if ( $callback[1] & self::ACCEPT_DATA ) {
|
||||
$data['accepts_data'] = true;
|
||||
}
|
||||
|
||||
// For non-variable routes, generate links
|
||||
if ( strpos( $route, '<' ) === false ) {
|
||||
$data['meta'] = array(
|
||||
'self' => get_woocommerce_api_url( $route ),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$available['store']['routes'][ $route ] = apply_filters( 'woocommerce_api_endpoints_description', $data );
|
||||
}
|
||||
|
||||
return apply_filters( 'woocommerce_api_index', $available );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a HTTP status code
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int $code HTTP status
|
||||
*/
|
||||
public function send_status( $code ) {
|
||||
status_header( $code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a HTTP header
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $key Header key
|
||||
* @param string $value Header value
|
||||
* @param boolean $replace Should we replace the existing header?
|
||||
*/
|
||||
public function header( $key, $value, $replace = true ) {
|
||||
header( sprintf( '%s: %s', $key, $value ), $replace );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a Link header
|
||||
*
|
||||
* @internal The $rel parameter is first, as this looks nicer when sending multiple
|
||||
*
|
||||
* @link http://tools.ietf.org/html/rfc5988
|
||||
* @link http://www.iana.org/assignments/link-relations/link-relations.xml
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $rel Link relation. Either a registered type, or an absolute URL
|
||||
* @param string $link Target IRI for the link
|
||||
* @param array $other Other parameters to send, as an associative array
|
||||
*/
|
||||
public function link_header( $rel, $link, $other = array() ) {
|
||||
|
||||
$header = sprintf( '<%s>; rel="%s"', $link, esc_attr( $rel ) );
|
||||
|
||||
foreach ( $other as $key => $value ) {
|
||||
|
||||
if ( 'title' == $key ) {
|
||||
|
||||
$value = '"' . $value . '"';
|
||||
}
|
||||
|
||||
$header .= '; ' . $key . '=' . $value;
|
||||
}
|
||||
|
||||
$this->header( 'Link', $header, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send pagination headers for resources
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WP_Query|WP_User_Query|stdClass $query
|
||||
*/
|
||||
public function add_pagination_headers( $query ) {
|
||||
|
||||
// WP_User_Query
|
||||
if ( is_a( $query, 'WP_User_Query' ) ) {
|
||||
|
||||
$single = count( $query->get_results() ) == 1;
|
||||
$total = $query->get_total();
|
||||
|
||||
if ( $query->get( 'number' ) > 0 ) {
|
||||
$page = ( $query->get( 'offset' ) / $query->get( 'number' ) ) + 1;
|
||||
$total_pages = ceil( $total / $query->get( 'number' ) );
|
||||
} else {
|
||||
$page = 1;
|
||||
$total_pages = 1;
|
||||
}
|
||||
} elseif ( is_a( $query, 'stdClass' ) ) {
|
||||
$page = $query->page;
|
||||
$single = $query->is_single;
|
||||
$total = $query->total;
|
||||
$total_pages = $query->total_pages;
|
||||
|
||||
// WP_Query
|
||||
} else {
|
||||
|
||||
$page = $query->get( 'paged' );
|
||||
$single = $query->is_single();
|
||||
$total = $query->found_posts;
|
||||
$total_pages = $query->max_num_pages;
|
||||
}
|
||||
|
||||
if ( ! $page ) {
|
||||
$page = 1;
|
||||
}
|
||||
|
||||
$next_page = absint( $page ) + 1;
|
||||
|
||||
if ( ! $single ) {
|
||||
|
||||
// first/prev
|
||||
if ( $page > 1 ) {
|
||||
$this->link_header( 'first', $this->get_paginated_url( 1 ) );
|
||||
$this->link_header( 'prev', $this->get_paginated_url( $page -1 ) );
|
||||
}
|
||||
|
||||
// next
|
||||
if ( $next_page <= $total_pages ) {
|
||||
$this->link_header( 'next', $this->get_paginated_url( $next_page ) );
|
||||
}
|
||||
|
||||
// last
|
||||
if ( $page != $total_pages ) {
|
||||
$this->link_header( 'last', $this->get_paginated_url( $total_pages ) );
|
||||
}
|
||||
}
|
||||
|
||||
$this->header( 'X-WC-Total', $total );
|
||||
$this->header( 'X-WC-TotalPages', $total_pages );
|
||||
|
||||
do_action( 'woocommerce_api_pagination_headers', $this, $query );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request URL with the page query parameter set to the specified page
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int $page
|
||||
* @return string
|
||||
*/
|
||||
private function get_paginated_url( $page ) {
|
||||
|
||||
// remove existing page query param
|
||||
$request = remove_query_arg( 'page' );
|
||||
|
||||
// add provided page query param
|
||||
$request = urldecode( add_query_arg( 'page', $page, $request ) );
|
||||
|
||||
// get the home host
|
||||
$host = parse_url( get_home_url(), PHP_URL_HOST );
|
||||
|
||||
return set_url_scheme( "http://{$host}{$request}" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the raw request entity (body)
|
||||
*
|
||||
* @since 2.1
|
||||
* @return string
|
||||
*/
|
||||
public function get_raw_data() {
|
||||
// @codingStandardsIgnoreStart
|
||||
// $HTTP_RAW_POST_DATA is deprecated on PHP 5.6.
|
||||
if ( function_exists( 'phpversion' ) && version_compare( phpversion(), '5.6', '>=' ) ) {
|
||||
return file_get_contents( 'php://input' );
|
||||
}
|
||||
|
||||
global $HTTP_RAW_POST_DATA;
|
||||
|
||||
// A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default,
|
||||
// but we can do it ourself.
|
||||
if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
|
||||
$HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
|
||||
}
|
||||
|
||||
return $HTTP_RAW_POST_DATA;
|
||||
// @codingStandardsIgnoreEnd
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an RFC3339 datetime into a MySQl datetime
|
||||
*
|
||||
* Invalid dates default to unix epoch
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $datetime RFC3339 datetime
|
||||
* @return string MySQl datetime (YYYY-MM-DD HH:MM:SS)
|
||||
*/
|
||||
public function parse_datetime( $datetime ) {
|
||||
|
||||
// Strip millisecond precision (a full stop followed by one or more digits)
|
||||
if ( strpos( $datetime, '.' ) !== false ) {
|
||||
$datetime = preg_replace( '/\.\d+/', '', $datetime );
|
||||
}
|
||||
|
||||
// default timezone to UTC
|
||||
$datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime );
|
||||
|
||||
try {
|
||||
|
||||
$datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) );
|
||||
|
||||
} catch ( Exception $e ) {
|
||||
|
||||
$datetime = new DateTime( '@0' );
|
||||
|
||||
}
|
||||
|
||||
return $datetime->format( 'Y-m-d H:i:s' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a unix timestamp or MySQL datetime into an RFC3339 datetime
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int|string $timestamp unix timestamp or MySQL datetime
|
||||
* @param bool $convert_to_utc
|
||||
* @param bool $convert_to_gmt Use GMT timezone.
|
||||
* @return string RFC3339 datetime
|
||||
*/
|
||||
public function format_datetime( $timestamp, $convert_to_utc = false, $convert_to_gmt = false ) {
|
||||
if ( $convert_to_gmt ) {
|
||||
if ( is_numeric( $timestamp ) ) {
|
||||
$timestamp = date( 'Y-m-d H:i:s', $timestamp );
|
||||
}
|
||||
|
||||
$timestamp = get_gmt_from_date( $timestamp );
|
||||
}
|
||||
|
||||
if ( $convert_to_utc ) {
|
||||
$timezone = new DateTimeZone( wc_timezone_string() );
|
||||
} else {
|
||||
$timezone = new DateTimeZone( 'UTC' );
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
if ( is_numeric( $timestamp ) ) {
|
||||
$date = new DateTime( "@{$timestamp}" );
|
||||
} else {
|
||||
$date = new DateTime( $timestamp, $timezone );
|
||||
}
|
||||
|
||||
// convert to UTC by adjusting the time based on the offset of the site's timezone
|
||||
if ( $convert_to_utc ) {
|
||||
$date->modify( -1 * $date->getOffset() . ' seconds' );
|
||||
}
|
||||
} catch ( Exception $e ) {
|
||||
|
||||
$date = new DateTime( '@0' );
|
||||
}
|
||||
|
||||
return $date->format( 'Y-m-d\TH:i:s\Z' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract headers from a PHP-style $_SERVER array
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $server Associative array similar to $_SERVER
|
||||
* @return array Headers extracted from the input
|
||||
*/
|
||||
public function get_headers( $server ) {
|
||||
$headers = array();
|
||||
// CONTENT_* headers are not prefixed with HTTP_
|
||||
$additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true );
|
||||
|
||||
foreach ( $server as $key => $value ) {
|
||||
if ( strpos( $key, 'HTTP_' ) === 0 ) {
|
||||
$headers[ substr( $key, 5 ) ] = $value;
|
||||
} elseif ( isset( $additional[ $key ] ) ) {
|
||||
$headers[ $key ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,509 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API Webhooks class
|
||||
*
|
||||
* Handles requests to the /webhooks endpoint
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.2
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_Webhooks extends WC_API_Resource {
|
||||
|
||||
/** @var string $base the route base */
|
||||
protected $base = '/webhooks';
|
||||
|
||||
/**
|
||||
* Register the routes for this class
|
||||
*
|
||||
* @since 2.2
|
||||
* @param array $routes
|
||||
* @return array
|
||||
*/
|
||||
public function register_routes( $routes ) {
|
||||
|
||||
# GET|POST /webhooks
|
||||
$routes[ $this->base ] = array(
|
||||
array( array( $this, 'get_webhooks' ), WC_API_Server::READABLE ),
|
||||
array( array( $this, 'create_webhook' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
|
||||
);
|
||||
|
||||
# GET /webhooks/count
|
||||
$routes[ $this->base . '/count' ] = array(
|
||||
array( array( $this, 'get_webhooks_count' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# GET|PUT|DELETE /webhooks/<id>
|
||||
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
|
||||
array( array( $this, 'get_webhook' ), WC_API_Server::READABLE ),
|
||||
array( array( $this, 'edit_webhook' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
|
||||
array( array( $this, 'delete_webhook' ), WC_API_Server::DELETABLE ),
|
||||
);
|
||||
|
||||
# GET /webhooks/<id>/deliveries
|
||||
$routes[ $this->base . '/(?P<webhook_id>\d+)/deliveries' ] = array(
|
||||
array( array( $this, 'get_webhook_deliveries' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# GET /webhooks/<webhook_id>/deliveries/<id>
|
||||
$routes[ $this->base . '/(?P<webhook_id>\d+)/deliveries/(?P<id>\d+)' ] = array(
|
||||
array( array( $this, 'get_webhook_delivery' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all webhooks
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @param array $fields
|
||||
* @param array $filter
|
||||
* @param string $status
|
||||
* @param int $page
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_webhooks( $fields = null, $filter = array(), $status = null, $page = 1 ) {
|
||||
|
||||
if ( ! empty( $status ) ) {
|
||||
$filter['status'] = $status;
|
||||
}
|
||||
|
||||
$filter['page'] = $page;
|
||||
|
||||
$query = $this->query_webhooks( $filter );
|
||||
|
||||
$webhooks = array();
|
||||
|
||||
foreach ( $query['results'] as $webhook_id ) {
|
||||
$webhooks[] = current( $this->get_webhook( $webhook_id, $fields ) );
|
||||
}
|
||||
|
||||
$this->server->add_pagination_headers( $query['headers'] );
|
||||
|
||||
return array( 'webhooks' => $webhooks );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the webhook for the given ID
|
||||
*
|
||||
* @since 2.2
|
||||
* @param int $id webhook ID
|
||||
* @param array $fields
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_webhook( $id, $fields = null ) {
|
||||
|
||||
// ensure webhook ID is valid & user has permission to read
|
||||
$id = $this->validate_request( $id, 'shop_webhook', 'read' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$webhook = wc_get_webhook( $id );
|
||||
|
||||
$webhook_data = array(
|
||||
'id' => $webhook->get_id(),
|
||||
'name' => $webhook->get_name(),
|
||||
'status' => $webhook->get_status(),
|
||||
'topic' => $webhook->get_topic(),
|
||||
'resource' => $webhook->get_resource(),
|
||||
'event' => $webhook->get_event(),
|
||||
'hooks' => $webhook->get_hooks(),
|
||||
'delivery_url' => $webhook->get_delivery_url(),
|
||||
'created_at' => $this->server->format_datetime( $webhook->get_date_created() ? $webhook->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times.
|
||||
'updated_at' => $this->server->format_datetime( $webhook->get_date_modified() ? $webhook->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times.
|
||||
);
|
||||
|
||||
return array( 'webhook' => apply_filters( 'woocommerce_api_webhook_response', $webhook_data, $webhook, $fields, $this ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of webhooks
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @param string $status
|
||||
* @param array $filter
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_webhooks_count( $status = null, $filter = array() ) {
|
||||
try {
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_webhooks_count', __( 'You do not have permission to read the webhooks count', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
if ( ! empty( $status ) ) {
|
||||
$filter['status'] = $status;
|
||||
}
|
||||
|
||||
$query = $this->query_webhooks( $filter );
|
||||
|
||||
return array( 'count' => $query['headers']->total );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an webhook
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @param array $data parsed webhook data
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function create_webhook( $data ) {
|
||||
|
||||
try {
|
||||
if ( ! isset( $data['webhook'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'webhook' ), 400 );
|
||||
}
|
||||
|
||||
$data = $data['webhook'];
|
||||
|
||||
// permission check
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_webhooks', __( 'You do not have permission to create webhooks.', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$data = apply_filters( 'woocommerce_api_create_webhook_data', $data, $this );
|
||||
|
||||
// validate topic
|
||||
if ( empty( $data['topic'] ) || ! wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic is required and must be valid.', 'woocommerce' ), 400 );
|
||||
}
|
||||
|
||||
// validate delivery URL
|
||||
if ( empty( $data['delivery_url'] ) || ! wc_is_valid_url( $data['delivery_url'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 );
|
||||
}
|
||||
|
||||
$webhook_data = apply_filters( 'woocommerce_new_webhook_data', array(
|
||||
'post_type' => 'shop_webhook',
|
||||
'post_status' => 'publish',
|
||||
'ping_status' => 'closed',
|
||||
'post_author' => get_current_user_id(),
|
||||
'post_password' => 'webhook_' . wp_generate_password(),
|
||||
'post_title' => ! empty( $data['name'] ) ? $data['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ),
|
||||
), $data, $this );
|
||||
|
||||
$webhook = new WC_Webhook();
|
||||
|
||||
$webhook->set_name( $webhook_data['post_title'] );
|
||||
$webhook->set_user_id( $webhook_data['post_author'] );
|
||||
$webhook->set_status( 'publish' === $webhook_data['post_status'] ? 'active' : 'disabled' );
|
||||
$webhook->set_topic( $data['topic'] );
|
||||
$webhook->set_delivery_url( $data['delivery_url'] );
|
||||
$webhook->set_secret( ! empty( $data['secret'] ) ? $data['secret'] : wp_generate_password( 50, true, true ) );
|
||||
$webhook->set_api_version( 'legacy_v3' );
|
||||
$webhook->save();
|
||||
|
||||
$webhook->deliver_ping();
|
||||
|
||||
// HTTP 201 Created
|
||||
$this->server->send_status( 201 );
|
||||
|
||||
do_action( 'woocommerce_api_create_webhook', $webhook->get_id(), $this );
|
||||
|
||||
return $this->get_webhook( $webhook->get_id() );
|
||||
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a webhook
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @param int $id webhook ID
|
||||
* @param array $data parsed webhook data
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function edit_webhook( $id, $data ) {
|
||||
|
||||
try {
|
||||
if ( ! isset( $data['webhook'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'webhook' ), 400 );
|
||||
}
|
||||
|
||||
$data = $data['webhook'];
|
||||
|
||||
$id = $this->validate_request( $id, 'shop_webhook', 'edit' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$data = apply_filters( 'woocommerce_api_edit_webhook_data', $data, $id, $this );
|
||||
|
||||
$webhook = wc_get_webhook( $id );
|
||||
|
||||
// update topic
|
||||
if ( ! empty( $data['topic'] ) ) {
|
||||
|
||||
if ( wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) {
|
||||
|
||||
$webhook->set_topic( $data['topic'] );
|
||||
|
||||
} else {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic must be valid.', 'woocommerce' ), 400 );
|
||||
}
|
||||
}
|
||||
|
||||
// update delivery URL
|
||||
if ( ! empty( $data['delivery_url'] ) ) {
|
||||
if ( wc_is_valid_url( $data['delivery_url'] ) ) {
|
||||
|
||||
$webhook->set_delivery_url( $data['delivery_url'] );
|
||||
|
||||
} else {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 );
|
||||
}
|
||||
}
|
||||
|
||||
// update secret
|
||||
if ( ! empty( $data['secret'] ) ) {
|
||||
$webhook->set_secret( $data['secret'] );
|
||||
}
|
||||
|
||||
// update status
|
||||
if ( ! empty( $data['status'] ) ) {
|
||||
$webhook->set_status( $data['status'] );
|
||||
}
|
||||
|
||||
// update name
|
||||
if ( ! empty( $data['name'] ) ) {
|
||||
$webhook->set_name( $data['name'] );
|
||||
}
|
||||
|
||||
$webhook->save();
|
||||
|
||||
do_action( 'woocommerce_api_edit_webhook', $webhook->get_id(), $this );
|
||||
|
||||
return $this->get_webhook( $webhook->get_id() );
|
||||
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a webhook
|
||||
*
|
||||
* @since 2.2
|
||||
* @param int $id webhook ID
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function delete_webhook( $id ) {
|
||||
|
||||
$id = $this->validate_request( $id, 'shop_webhook', 'delete' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
do_action( 'woocommerce_api_delete_webhook', $id, $this );
|
||||
|
||||
$webhook = wc_get_webhook( $id );
|
||||
|
||||
return $webhook->delete( true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get webhook post objects
|
||||
*
|
||||
* @since 2.2
|
||||
* @param array $args Request arguments for filtering query.
|
||||
* @return array
|
||||
*/
|
||||
private function query_webhooks( $args ) {
|
||||
$args = $this->merge_query_args( array(), $args );
|
||||
|
||||
$args['limit'] = isset( $args['posts_per_page'] ) ? intval( $args['posts_per_page'] ) : intval( get_option( 'posts_per_page' ) );
|
||||
|
||||
if ( empty( $args['offset'] ) ) {
|
||||
$args['offset'] = 1 < $args['paged'] ? ( $args['paged'] - 1 ) * $args['limit'] : 0;
|
||||
}
|
||||
|
||||
$page = $args['paged'];
|
||||
unset( $args['paged'], $args['posts_per_page'] );
|
||||
|
||||
if ( isset( $args['s'] ) ) {
|
||||
$args['search'] = $args['s'];
|
||||
unset( $args['s'] );
|
||||
}
|
||||
|
||||
// Post type to webhook status.
|
||||
if ( ! empty( $args['post_status'] ) ) {
|
||||
$args['status'] = $args['post_status'];
|
||||
unset( $args['post_status'] );
|
||||
}
|
||||
|
||||
if ( ! empty( $args['post__in'] ) ) {
|
||||
$args['include'] = $args['post__in'];
|
||||
unset( $args['post__in'] );
|
||||
}
|
||||
|
||||
if ( ! empty( $args['date_query'] ) ) {
|
||||
foreach ( $args['date_query'] as $date_query ) {
|
||||
if ( 'post_date_gmt' === $date_query['column'] ) {
|
||||
$args['after'] = isset( $date_query['after'] ) ? $date_query['after'] : null;
|
||||
$args['before'] = isset( $date_query['before'] ) ? $date_query['before'] : null;
|
||||
} elseif ( 'post_modified_gmt' === $date_query['column'] ) {
|
||||
$args['modified_after'] = isset( $date_query['after'] ) ? $date_query['after'] : null;
|
||||
$args['modified_before'] = isset( $date_query['before'] ) ? $date_query['before'] : null;
|
||||
}
|
||||
}
|
||||
|
||||
unset( $args['date_query'] );
|
||||
}
|
||||
|
||||
$args['paginate'] = true;
|
||||
|
||||
// Get the webhooks.
|
||||
$data_store = WC_Data_Store::load( 'webhook' );
|
||||
$results = $data_store->search_webhooks( $args );
|
||||
|
||||
// Get total items.
|
||||
$headers = new stdClass;
|
||||
$headers->page = $page;
|
||||
$headers->total = $results->total;
|
||||
$headers->is_single = $args['limit'] > $headers->total;
|
||||
$headers->total_pages = $results->max_num_pages;
|
||||
|
||||
return array(
|
||||
'results' => $results->webhooks,
|
||||
'headers' => $headers,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get deliveries for a webhook
|
||||
*
|
||||
* @since 2.2
|
||||
* @deprecated 3.3.0 Webhooks deliveries logs now uses logging system.
|
||||
* @param string $webhook_id webhook ID
|
||||
* @param string|null $fields fields to include in response
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_webhook_deliveries( $webhook_id, $fields = null ) {
|
||||
|
||||
// Ensure ID is valid webhook ID
|
||||
$webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' );
|
||||
|
||||
if ( is_wp_error( $webhook_id ) ) {
|
||||
return $webhook_id;
|
||||
}
|
||||
|
||||
return array( 'webhook_deliveries' => array() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the delivery log for the given webhook ID and delivery ID
|
||||
*
|
||||
* @since 2.2
|
||||
* @deprecated 3.3.0 Webhooks deliveries logs now uses logging system.
|
||||
* @param string $webhook_id webhook ID
|
||||
* @param string $id delivery log ID
|
||||
* @param string|null $fields fields to limit response to
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_webhook_delivery( $webhook_id, $id, $fields = null ) {
|
||||
try {
|
||||
// Validate webhook ID
|
||||
$webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' );
|
||||
|
||||
if ( is_wp_error( $webhook_id ) ) {
|
||||
return $webhook_id;
|
||||
}
|
||||
|
||||
$id = absint( $id );
|
||||
|
||||
if ( empty( $id ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery ID.', 'woocommerce' ), 404 );
|
||||
}
|
||||
|
||||
$webhook = new WC_Webhook( $webhook_id );
|
||||
|
||||
$log = 0;
|
||||
|
||||
if ( ! $log ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery.', 'woocommerce' ), 400 );
|
||||
}
|
||||
|
||||
return array( 'webhook_delivery' => apply_filters( 'woocommerce_api_webhook_delivery_response', array(), $id, $fields, $log, $webhook_id, $this ) );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the request by checking:
|
||||
*
|
||||
* 1) the ID is a valid integer.
|
||||
* 2) the ID returns a valid post object and matches the provided post type.
|
||||
* 3) the current user has the proper permissions to read/edit/delete the post.
|
||||
*
|
||||
* @since 3.3.0
|
||||
* @param string|int $id The post ID
|
||||
* @param string $type The post type, either `shop_order`, `shop_coupon`, or `product`.
|
||||
* @param string $context The context of the request, either `read`, `edit` or `delete`.
|
||||
* @return int|WP_Error Valid post ID or WP_Error if any of the checks fails.
|
||||
*/
|
||||
protected function validate_request( $id, $type, $context ) {
|
||||
$id = absint( $id );
|
||||
|
||||
// Validate ID.
|
||||
if ( empty( $id ) ) {
|
||||
return new WP_Error( "woocommerce_api_invalid_webhook_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
$webhook = wc_get_webhook( $id );
|
||||
|
||||
if ( null === $webhook ) {
|
||||
return new WP_Error( "woocommerce_api_no_webhook_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), 'webhook', $id ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
// Validate permissions.
|
||||
switch ( $context ) {
|
||||
|
||||
case 'read':
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
return new WP_Error( "woocommerce_api_user_cannot_read_webhook", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'edit':
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
return new WP_Error( "woocommerce_api_user_cannot_edit_webhook", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
return new WP_Error( "woocommerce_api_user_cannot_delete_webhook", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API
|
||||
*
|
||||
* Defines an interface that API request/response handlers should implement
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
interface WC_API_Handler {
|
||||
|
||||
/**
|
||||
* Get the content type for the response
|
||||
*
|
||||
* This should return the proper HTTP content-type for the response
|
||||
*
|
||||
* @since 2.1
|
||||
* @return string
|
||||
*/
|
||||
public function get_content_type();
|
||||
|
||||
/**
|
||||
* Parse the raw request body entity into an array
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $data
|
||||
* @return array
|
||||
*/
|
||||
public function parse_body( $data );
|
||||
|
||||
/**
|
||||
* Generate a response from an array of data
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $data
|
||||
* @return string
|
||||
*/
|
||||
public function generate_response( $data );
|
||||
|
||||
}
|
|
@ -0,0 +1,414 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API Authentication Class
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1.0
|
||||
* @version 2.4.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_Authentication {
|
||||
|
||||
/**
|
||||
* Setup class
|
||||
*
|
||||
* @since 2.1
|
||||
*/
|
||||
public function __construct() {
|
||||
|
||||
// To disable authentication, hook into this filter at a later priority and return a valid WP_User
|
||||
add_filter( 'woocommerce_api_check_authentication', array( $this, 'authenticate' ), 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the request. The authentication method varies based on whether the request was made over SSL or not.
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WP_User $user
|
||||
* @return null|WP_Error|WP_User
|
||||
*/
|
||||
public function authenticate( $user ) {
|
||||
|
||||
// Allow access to the index by default
|
||||
if ( '/' === WC()->api->server->path ) {
|
||||
return new WP_User( 0 );
|
||||
}
|
||||
|
||||
try {
|
||||
if ( is_ssl() ) {
|
||||
$keys = $this->perform_ssl_authentication();
|
||||
} else {
|
||||
$keys = $this->perform_oauth_authentication();
|
||||
}
|
||||
|
||||
// Check API key-specific permission
|
||||
$this->check_api_key_permissions( $keys['permissions'] );
|
||||
|
||||
$user = $this->get_user_by_id( $keys['user_id'] );
|
||||
|
||||
$this->update_api_key_last_access( $keys['key_id'] );
|
||||
|
||||
} catch ( Exception $e ) {
|
||||
$user = new WP_Error( 'woocommerce_api_authentication_error', $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* SSL-encrypted requests are not subject to sniffing or man-in-the-middle
|
||||
* attacks, so the request can be authenticated by simply looking up the user
|
||||
* associated with the given consumer key and confirming the consumer secret
|
||||
* provided is valid
|
||||
*
|
||||
* @since 2.1
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
private function perform_ssl_authentication() {
|
||||
$params = WC()->api->server->params['GET'];
|
||||
|
||||
// if the $_GET parameters are present, use those first
|
||||
if ( ! empty( $params['consumer_key'] ) && ! empty( $params['consumer_secret'] ) ) {
|
||||
$keys = $this->get_keys_by_consumer_key( $params['consumer_key'] );
|
||||
|
||||
if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $params['consumer_secret'] ) ) {
|
||||
throw new Exception( __( 'Consumer secret is invalid.', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
// if the above is not present, we will do full basic auth
|
||||
if ( empty( $_SERVER['PHP_AUTH_USER'] ) || empty( $_SERVER['PHP_AUTH_PW'] ) ) {
|
||||
$this->exit_with_unauthorized_headers();
|
||||
}
|
||||
|
||||
$keys = $this->get_keys_by_consumer_key( $_SERVER['PHP_AUTH_USER'] );
|
||||
|
||||
if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $_SERVER['PHP_AUTH_PW'] ) ) {
|
||||
$this->exit_with_unauthorized_headers();
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the consumer_key and consumer_secret $_GET parameters are NOT provided
|
||||
* and the Basic auth headers are either not present or the consumer secret does not match the consumer
|
||||
* key provided, then return the correct Basic headers and an error message.
|
||||
*
|
||||
* @since 2.4
|
||||
*/
|
||||
private function exit_with_unauthorized_headers() {
|
||||
$auth_message = __( 'WooCommerce API. Use a consumer key in the username field and a consumer secret in the password field.', 'woocommerce' );
|
||||
header( 'WWW-Authenticate: Basic realm="' . $auth_message . '"' );
|
||||
header( 'HTTP/1.0 401 Unauthorized' );
|
||||
throw new Exception( __( 'Consumer Secret is invalid.', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests
|
||||
*
|
||||
* This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP
|
||||
*
|
||||
* This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions:
|
||||
*
|
||||
* 1) There is no token associated with request/responses, only consumer keys/secrets are used
|
||||
*
|
||||
* 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header,
|
||||
* This is because there is no cross-OS function within PHP to get the raw Authorization header
|
||||
*
|
||||
* @link http://tools.ietf.org/html/rfc5849 for the full spec
|
||||
* @since 2.1
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
private function perform_oauth_authentication() {
|
||||
|
||||
$params = WC()->api->server->params['GET'];
|
||||
|
||||
$param_names = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' );
|
||||
|
||||
// Check for required OAuth parameters
|
||||
foreach ( $param_names as $param_name ) {
|
||||
|
||||
if ( empty( $params[ $param_name ] ) ) {
|
||||
throw new Exception( sprintf( __( '%s parameter is missing', 'woocommerce' ), $param_name ), 404 );
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch WP user by consumer key
|
||||
$keys = $this->get_keys_by_consumer_key( $params['oauth_consumer_key'] );
|
||||
|
||||
// Perform OAuth validation
|
||||
$this->check_oauth_signature( $keys, $params );
|
||||
$this->check_oauth_timestamp_and_nonce( $keys, $params['oauth_timestamp'], $params['oauth_nonce'] );
|
||||
|
||||
// Authentication successful, return user
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the keys for the given consumer key
|
||||
*
|
||||
* @since 2.4.0
|
||||
* @param string $consumer_key
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
private function get_keys_by_consumer_key( $consumer_key ) {
|
||||
global $wpdb;
|
||||
|
||||
$consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) );
|
||||
|
||||
$keys = $wpdb->get_row( $wpdb->prepare( "
|
||||
SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces
|
||||
FROM {$wpdb->prefix}woocommerce_api_keys
|
||||
WHERE consumer_key = '%s'
|
||||
", $consumer_key ), ARRAY_A );
|
||||
|
||||
if ( empty( $keys ) ) {
|
||||
throw new Exception( __( 'Consumer key is invalid.', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user by ID
|
||||
*
|
||||
* @since 2.4.0
|
||||
*
|
||||
* @param int $user_id
|
||||
*
|
||||
* @return WP_User
|
||||
* @throws Exception
|
||||
*/
|
||||
private function get_user_by_id( $user_id ) {
|
||||
$user = get_user_by( 'id', $user_id );
|
||||
|
||||
if ( ! $user ) {
|
||||
throw new Exception( __( 'API user is invalid', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the consumer secret provided for the given user is valid
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $keys_consumer_secret
|
||||
* @param string $consumer_secret
|
||||
* @return bool
|
||||
*/
|
||||
private function is_consumer_secret_valid( $keys_consumer_secret, $consumer_secret ) {
|
||||
return hash_equals( $keys_consumer_secret, $consumer_secret );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the consumer-provided request signature matches our generated signature, this ensures the consumer
|
||||
* has a valid key/secret
|
||||
*
|
||||
* @param array $keys
|
||||
* @param array $params the request parameters
|
||||
* @throws Exception
|
||||
*/
|
||||
private function check_oauth_signature( $keys, $params ) {
|
||||
$http_method = strtoupper( WC()->api->server->method );
|
||||
|
||||
$server_path = WC()->api->server->path;
|
||||
|
||||
// if the requested URL has a trailingslash, make sure our base URL does as well
|
||||
if ( isset( $_SERVER['REDIRECT_URL'] ) && '/' === substr( $_SERVER['REDIRECT_URL'], -1 ) ) {
|
||||
$server_path .= '/';
|
||||
}
|
||||
|
||||
$base_request_uri = rawurlencode( untrailingslashit( get_woocommerce_api_url( '' ) ) . $server_path );
|
||||
|
||||
// Get the signature provided by the consumer and remove it from the parameters prior to checking the signature
|
||||
$consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) );
|
||||
unset( $params['oauth_signature'] );
|
||||
|
||||
// Sort parameters
|
||||
if ( ! uksort( $params, 'strcmp' ) ) {
|
||||
throw new Exception( __( 'Invalid signature - failed to sort parameters.', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
// Normalize parameter key/values
|
||||
$params = $this->normalize_parameters( $params );
|
||||
$query_parameters = array();
|
||||
foreach ( $params as $param_key => $param_value ) {
|
||||
if ( is_array( $param_value ) ) {
|
||||
foreach ( $param_value as $param_key_inner => $param_value_inner ) {
|
||||
$query_parameters[] = $param_key . '%255B' . $param_key_inner . '%255D%3D' . $param_value_inner;
|
||||
}
|
||||
} else {
|
||||
$query_parameters[] = $param_key . '%3D' . $param_value; // join with equals sign
|
||||
}
|
||||
}
|
||||
$query_string = implode( '%26', $query_parameters ); // join with ampersand
|
||||
|
||||
$string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string;
|
||||
|
||||
if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) {
|
||||
throw new Exception( __( 'Invalid signature - signature method is invalid.', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) );
|
||||
|
||||
$secret = $keys['consumer_secret'] . '&';
|
||||
$signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $secret, true ) );
|
||||
|
||||
if ( ! hash_equals( $signature, $consumer_signature ) ) {
|
||||
throw new Exception( __( 'Invalid signature - provided signature does not match.', 'woocommerce' ), 401 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize each parameter by assuming each parameter may have already been
|
||||
* encoded, so attempt to decode, and then re-encode according to RFC 3986
|
||||
*
|
||||
* Note both the key and value is normalized so a filter param like:
|
||||
*
|
||||
* 'filter[period]' => 'week'
|
||||
*
|
||||
* is encoded to:
|
||||
*
|
||||
* 'filter%5Bperiod%5D' => 'week'
|
||||
*
|
||||
* This conforms to the OAuth 1.0a spec which indicates the entire query string
|
||||
* should be URL encoded
|
||||
*
|
||||
* @since 2.1
|
||||
* @see rawurlencode()
|
||||
* @param array $parameters un-normalized parameters
|
||||
* @return array normalized parameters
|
||||
*/
|
||||
private function normalize_parameters( $parameters ) {
|
||||
$keys = WC_API_Authentication::urlencode_rfc3986( array_keys( $parameters ) );
|
||||
$values = WC_API_Authentication::urlencode_rfc3986( array_values( $parameters ) );
|
||||
$parameters = array_combine( $keys, $values );
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a value according to RFC 3986. Supports multidimensional arrays.
|
||||
*
|
||||
* @since 2.4
|
||||
* @param string|array $value The value to encode
|
||||
* @return string|array Encoded values
|
||||
*/
|
||||
public static function urlencode_rfc3986( $value ) {
|
||||
if ( is_array( $value ) ) {
|
||||
return array_map( array( 'WC_API_Authentication', 'urlencode_rfc3986' ), $value );
|
||||
} else {
|
||||
// Percent symbols (%) must be double-encoded
|
||||
return str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where
|
||||
* an attacker could attempt to re-send an intercepted request at a later time.
|
||||
*
|
||||
* - A timestamp is valid if it is within 15 minutes of now
|
||||
* - A nonce is valid if it has not been used within the last 15 minutes
|
||||
*
|
||||
* @param array $keys
|
||||
* @param int $timestamp the unix timestamp for when the request was made
|
||||
* @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated
|
||||
* @throws Exception
|
||||
*/
|
||||
private function check_oauth_timestamp_and_nonce( $keys, $timestamp, $nonce ) {
|
||||
global $wpdb;
|
||||
|
||||
$valid_window = 15 * 60; // 15 minute window
|
||||
|
||||
if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) {
|
||||
throw new Exception( __( 'Invalid timestamp.', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$used_nonces = maybe_unserialize( $keys['nonces'] );
|
||||
|
||||
if ( empty( $used_nonces ) ) {
|
||||
$used_nonces = array();
|
||||
}
|
||||
|
||||
if ( in_array( $nonce, $used_nonces ) ) {
|
||||
throw new Exception( __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$used_nonces[ $timestamp ] = $nonce;
|
||||
|
||||
// Remove expired nonces
|
||||
foreach ( $used_nonces as $nonce_timestamp => $nonce ) {
|
||||
if ( $nonce_timestamp < ( time() - $valid_window ) ) {
|
||||
unset( $used_nonces[ $nonce_timestamp ] );
|
||||
}
|
||||
}
|
||||
|
||||
$used_nonces = maybe_serialize( $used_nonces );
|
||||
|
||||
$wpdb->update(
|
||||
$wpdb->prefix . 'woocommerce_api_keys',
|
||||
array( 'nonces' => $used_nonces ),
|
||||
array( 'key_id' => $keys['key_id'] ),
|
||||
array( '%s' ),
|
||||
array( '%d' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the API keys provided have the proper key-specific permissions to either read or write API resources
|
||||
*
|
||||
* @param string $key_permissions
|
||||
* @throws Exception if the permission check fails
|
||||
*/
|
||||
public function check_api_key_permissions( $key_permissions ) {
|
||||
switch ( WC()->api->server->method ) {
|
||||
|
||||
case 'HEAD':
|
||||
case 'GET':
|
||||
if ( 'read' !== $key_permissions && 'read_write' !== $key_permissions ) {
|
||||
throw new Exception( __( 'The API key provided does not have read permissions.', 'woocommerce' ), 401 );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
case 'PUT':
|
||||
case 'PATCH':
|
||||
case 'DELETE':
|
||||
if ( 'write' !== $key_permissions && 'read_write' !== $key_permissions ) {
|
||||
throw new Exception( __( 'The API key provided does not have write permissions.', 'woocommerce' ), 401 );
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updated API Key last access datetime
|
||||
*
|
||||
* @since 2.4.0
|
||||
*
|
||||
* @param int $key_id
|
||||
*/
|
||||
private function update_api_key_last_access( $key_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->update(
|
||||
$wpdb->prefix . 'woocommerce_api_keys',
|
||||
array( 'last_access' => current_time( 'mysql' ) ),
|
||||
array( 'key_id' => $key_id ),
|
||||
array( '%s' ),
|
||||
array( '%d' )
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,576 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API Coupons Class
|
||||
*
|
||||
* Handles requests to the /coupons endpoint
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_Coupons extends WC_API_Resource {
|
||||
|
||||
/** @var string $base the route base */
|
||||
protected $base = '/coupons';
|
||||
|
||||
/**
|
||||
* Register the routes for this class
|
||||
*
|
||||
* GET /coupons
|
||||
* GET /coupons/count
|
||||
* GET /coupons/<id>
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $routes
|
||||
* @return array
|
||||
*/
|
||||
public function register_routes( $routes ) {
|
||||
|
||||
# GET/POST /coupons
|
||||
$routes[ $this->base ] = array(
|
||||
array( array( $this, 'get_coupons' ), WC_API_Server::READABLE ),
|
||||
array( array( $this, 'create_coupon' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
|
||||
);
|
||||
|
||||
# GET /coupons/count
|
||||
$routes[ $this->base . '/count' ] = array(
|
||||
array( array( $this, 'get_coupons_count' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# GET/PUT/DELETE /coupons/<id>
|
||||
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
|
||||
array( array( $this, 'get_coupon' ), WC_API_Server::READABLE ),
|
||||
array( array( $this, 'edit_coupon' ), WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ),
|
||||
array( array( $this, 'delete_coupon' ), WC_API_SERVER::DELETABLE ),
|
||||
);
|
||||
|
||||
# GET /coupons/code/<code>, note that coupon codes can contain spaces, dashes and underscores
|
||||
$routes[ $this->base . '/code/(?P<code>\w[\w\s\-]*)' ] = array(
|
||||
array( array( $this, 'get_coupon_by_code' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# POST|PUT /coupons/bulk
|
||||
$routes[ $this->base . '/bulk' ] = array(
|
||||
array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
|
||||
);
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all coupons
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $fields
|
||||
* @param array $filter
|
||||
* @param int $page
|
||||
* @return array
|
||||
*/
|
||||
public function get_coupons( $fields = null, $filter = array(), $page = 1 ) {
|
||||
|
||||
$filter['page'] = $page;
|
||||
|
||||
$query = $this->query_coupons( $filter );
|
||||
|
||||
$coupons = array();
|
||||
|
||||
foreach ( $query->posts as $coupon_id ) {
|
||||
|
||||
if ( ! $this->is_readable( $coupon_id ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$coupons[] = current( $this->get_coupon( $coupon_id, $fields ) );
|
||||
}
|
||||
|
||||
$this->server->add_pagination_headers( $query );
|
||||
|
||||
return array( 'coupons' => $coupons );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the coupon for the given ID
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int $id the coupon ID
|
||||
* @param string $fields fields to include in response
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_coupon( $id, $fields = null ) {
|
||||
try {
|
||||
|
||||
$id = $this->validate_request( $id, 'shop_coupon', 'read' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$coupon = new WC_Coupon( $id );
|
||||
|
||||
if ( 0 === $coupon->get_id() ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_id', __( 'Invalid coupon ID', 'woocommerce' ), 404 );
|
||||
}
|
||||
|
||||
$coupon_data = array(
|
||||
'id' => $coupon->get_id(),
|
||||
'code' => $coupon->get_code(),
|
||||
'type' => $coupon->get_discount_type(),
|
||||
'created_at' => $this->server->format_datetime( $coupon->get_date_created() ? $coupon->get_date_created()->getTimestamp() : 0 ), // API gives UTC times.
|
||||
'updated_at' => $this->server->format_datetime( $coupon->get_date_modified() ? $coupon->get_date_modified()->getTimestamp() : 0 ), // API gives UTC times.
|
||||
'amount' => wc_format_decimal( $coupon->get_amount(), 2 ),
|
||||
'individual_use' => $coupon->get_individual_use(),
|
||||
'product_ids' => array_map( 'absint', (array) $coupon->get_product_ids() ),
|
||||
'exclude_product_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_ids() ),
|
||||
'usage_limit' => $coupon->get_usage_limit() ? $coupon->get_usage_limit() : null,
|
||||
'usage_limit_per_user' => $coupon->get_usage_limit_per_user() ? $coupon->get_usage_limit_per_user() : null,
|
||||
'limit_usage_to_x_items' => (int) $coupon->get_limit_usage_to_x_items(),
|
||||
'usage_count' => (int) $coupon->get_usage_count(),
|
||||
'expiry_date' => $coupon->get_date_expires() ? $this->server->format_datetime( $coupon->get_date_expires()->getTimestamp() ) : null, // API gives UTC times.
|
||||
'enable_free_shipping' => $coupon->get_free_shipping(),
|
||||
'product_category_ids' => array_map( 'absint', (array) $coupon->get_product_categories() ),
|
||||
'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_categories() ),
|
||||
'exclude_sale_items' => $coupon->get_exclude_sale_items(),
|
||||
'minimum_amount' => wc_format_decimal( $coupon->get_minimum_amount(), 2 ),
|
||||
'maximum_amount' => wc_format_decimal( $coupon->get_maximum_amount(), 2 ),
|
||||
'customer_emails' => $coupon->get_email_restrictions(),
|
||||
'description' => $coupon->get_description(),
|
||||
);
|
||||
|
||||
return array( 'coupon' => apply_filters( 'woocommerce_api_coupon_response', $coupon_data, $coupon, $fields, $this->server ) );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of coupons
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $filter
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_coupons_count( $filter = array() ) {
|
||||
try {
|
||||
if ( ! current_user_can( 'read_private_shop_coupons' ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_coupons_count', __( 'You do not have permission to read the coupons count', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$query = $this->query_coupons( $filter );
|
||||
|
||||
return array( 'count' => (int) $query->found_posts );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the coupon for the given code
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $code the coupon code
|
||||
* @param string $fields fields to include in response
|
||||
* @return int|WP_Error
|
||||
*/
|
||||
public function get_coupon_by_code( $code, $fields = null ) {
|
||||
global $wpdb;
|
||||
|
||||
try {
|
||||
$id = $wpdb->get_var( $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 LIMIT 1;", $code ) );
|
||||
|
||||
if ( is_null( $id ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_code', __( 'Invalid coupon code', 'woocommerce' ), 404 );
|
||||
}
|
||||
|
||||
return $this->get_coupon( $id, $fields );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a coupon
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function create_coupon( $data ) {
|
||||
global $wpdb;
|
||||
|
||||
try {
|
||||
if ( ! isset( $data['coupon'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'coupon' ), 400 );
|
||||
}
|
||||
|
||||
$data = $data['coupon'];
|
||||
|
||||
// Check user permission
|
||||
if ( ! current_user_can( 'publish_shop_coupons' ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_coupon', __( 'You do not have permission to create coupons', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$data = apply_filters( 'woocommerce_api_create_coupon_data', $data, $this );
|
||||
|
||||
// Check if coupon code is specified
|
||||
if ( ! isset( $data['code'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_coupon_code', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'code' ), 400 );
|
||||
}
|
||||
|
||||
$coupon_code = wc_format_coupon_code( $data['code'] );
|
||||
$id_from_code = wc_get_coupon_id_by_code( $coupon_code );
|
||||
|
||||
if ( $id_from_code ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 );
|
||||
}
|
||||
|
||||
$defaults = array(
|
||||
'type' => 'fixed_cart',
|
||||
'amount' => 0,
|
||||
'individual_use' => false,
|
||||
'product_ids' => array(),
|
||||
'exclude_product_ids' => array(),
|
||||
'usage_limit' => '',
|
||||
'usage_limit_per_user' => '',
|
||||
'limit_usage_to_x_items' => '',
|
||||
'usage_count' => '',
|
||||
'expiry_date' => '',
|
||||
'enable_free_shipping' => false,
|
||||
'product_category_ids' => array(),
|
||||
'exclude_product_category_ids' => array(),
|
||||
'exclude_sale_items' => false,
|
||||
'minimum_amount' => '',
|
||||
'maximum_amount' => '',
|
||||
'customer_emails' => array(),
|
||||
'description' => '',
|
||||
);
|
||||
|
||||
$coupon_data = wp_parse_args( $data, $defaults );
|
||||
|
||||
// Validate coupon types
|
||||
if ( ! in_array( wc_clean( $coupon_data['type'] ), array_keys( wc_get_coupon_types() ) ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 );
|
||||
}
|
||||
|
||||
$new_coupon = array(
|
||||
'post_title' => $coupon_code,
|
||||
'post_content' => '',
|
||||
'post_status' => 'publish',
|
||||
'post_author' => get_current_user_id(),
|
||||
'post_type' => 'shop_coupon',
|
||||
'post_excerpt' => $coupon_data['description'],
|
||||
);
|
||||
|
||||
$id = wp_insert_post( $new_coupon, true );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_cannot_create_coupon', $id->get_error_message(), 400 );
|
||||
}
|
||||
|
||||
// Set coupon meta
|
||||
update_post_meta( $id, 'discount_type', $coupon_data['type'] );
|
||||
update_post_meta( $id, 'coupon_amount', wc_format_decimal( $coupon_data['amount'] ) );
|
||||
update_post_meta( $id, 'individual_use', ( true === $coupon_data['individual_use'] ) ? 'yes' : 'no' );
|
||||
update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['product_ids'] ) ) ) );
|
||||
update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['exclude_product_ids'] ) ) ) );
|
||||
update_post_meta( $id, 'usage_limit', absint( $coupon_data['usage_limit'] ) );
|
||||
update_post_meta( $id, 'usage_limit_per_user', absint( $coupon_data['usage_limit_per_user'] ) );
|
||||
update_post_meta( $id, 'limit_usage_to_x_items', absint( $coupon_data['limit_usage_to_x_items'] ) );
|
||||
update_post_meta( $id, 'usage_count', absint( $coupon_data['usage_count'] ) );
|
||||
update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ) ) );
|
||||
update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ), true ) );
|
||||
update_post_meta( $id, 'free_shipping', ( true === $coupon_data['enable_free_shipping'] ) ? 'yes' : 'no' );
|
||||
update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $coupon_data['product_category_ids'] ) ) );
|
||||
update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $coupon_data['exclude_product_category_ids'] ) ) );
|
||||
update_post_meta( $id, 'exclude_sale_items', ( true === $coupon_data['exclude_sale_items'] ) ? 'yes' : 'no' );
|
||||
update_post_meta( $id, 'minimum_amount', wc_format_decimal( $coupon_data['minimum_amount'] ) );
|
||||
update_post_meta( $id, 'maximum_amount', wc_format_decimal( $coupon_data['maximum_amount'] ) );
|
||||
update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $coupon_data['customer_emails'] ) ) );
|
||||
|
||||
do_action( 'woocommerce_api_create_coupon', $id, $data );
|
||||
do_action( 'woocommerce_new_coupon', $id );
|
||||
|
||||
$this->server->send_status( 201 );
|
||||
|
||||
return $this->get_coupon( $id );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a coupon
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @param int $id the coupon ID
|
||||
* @param array $data
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function edit_coupon( $id, $data ) {
|
||||
|
||||
try {
|
||||
if ( ! isset( $data['coupon'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'coupon' ), 400 );
|
||||
}
|
||||
|
||||
$data = $data['coupon'];
|
||||
|
||||
$id = $this->validate_request( $id, 'shop_coupon', 'edit' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$data = apply_filters( 'woocommerce_api_edit_coupon_data', $data, $id, $this );
|
||||
|
||||
if ( isset( $data['code'] ) ) {
|
||||
global $wpdb;
|
||||
|
||||
$coupon_code = wc_format_coupon_code( $data['code'] );
|
||||
$id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id );
|
||||
|
||||
if ( $id_from_code ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 );
|
||||
}
|
||||
|
||||
$updated = wp_update_post( array( 'ID' => intval( $id ), 'post_title' => $coupon_code ) );
|
||||
|
||||
if ( 0 === $updated ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $data['description'] ) ) {
|
||||
$updated = wp_update_post( array( 'ID' => intval( $id ), 'post_excerpt' => $data['description'] ) );
|
||||
|
||||
if ( 0 === $updated ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $data['type'] ) ) {
|
||||
// Validate coupon types
|
||||
if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_coupon_types() ) ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 );
|
||||
}
|
||||
update_post_meta( $id, 'discount_type', $data['type'] );
|
||||
}
|
||||
|
||||
if ( isset( $data['amount'] ) ) {
|
||||
update_post_meta( $id, 'coupon_amount', wc_format_decimal( $data['amount'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['individual_use'] ) ) {
|
||||
update_post_meta( $id, 'individual_use', ( true === $data['individual_use'] ) ? 'yes' : 'no' );
|
||||
}
|
||||
|
||||
if ( isset( $data['product_ids'] ) ) {
|
||||
update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $data['product_ids'] ) ) ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['exclude_product_ids'] ) ) {
|
||||
update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $data['exclude_product_ids'] ) ) ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['usage_limit'] ) ) {
|
||||
update_post_meta( $id, 'usage_limit', absint( $data['usage_limit'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['usage_limit_per_user'] ) ) {
|
||||
update_post_meta( $id, 'usage_limit_per_user', absint( $data['usage_limit_per_user'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['limit_usage_to_x_items'] ) ) {
|
||||
update_post_meta( $id, 'limit_usage_to_x_items', absint( $data['limit_usage_to_x_items'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['usage_count'] ) ) {
|
||||
update_post_meta( $id, 'usage_count', absint( $data['usage_count'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['expiry_date'] ) ) {
|
||||
update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ) ) );
|
||||
update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ), true ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['enable_free_shipping'] ) ) {
|
||||
update_post_meta( $id, 'free_shipping', ( true === $data['enable_free_shipping'] ) ? 'yes' : 'no' );
|
||||
}
|
||||
|
||||
if ( isset( $data['product_category_ids'] ) ) {
|
||||
update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $data['product_category_ids'] ) ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['exclude_product_category_ids'] ) ) {
|
||||
update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $data['exclude_product_category_ids'] ) ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['exclude_sale_items'] ) ) {
|
||||
update_post_meta( $id, 'exclude_sale_items', ( true === $data['exclude_sale_items'] ) ? 'yes' : 'no' );
|
||||
}
|
||||
|
||||
if ( isset( $data['minimum_amount'] ) ) {
|
||||
update_post_meta( $id, 'minimum_amount', wc_format_decimal( $data['minimum_amount'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['maximum_amount'] ) ) {
|
||||
update_post_meta( $id, 'maximum_amount', wc_format_decimal( $data['maximum_amount'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['customer_emails'] ) ) {
|
||||
update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $data['customer_emails'] ) ) );
|
||||
}
|
||||
|
||||
do_action( 'woocommerce_api_edit_coupon', $id, $data );
|
||||
do_action( 'woocommerce_update_coupon', $id );
|
||||
|
||||
return $this->get_coupon( $id );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a coupon
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @param int $id the coupon ID
|
||||
* @param bool $force true to permanently delete coupon, false to move to trash
|
||||
*
|
||||
* @return array|int|WP_Error
|
||||
*/
|
||||
public function delete_coupon( $id, $force = false ) {
|
||||
|
||||
$id = $this->validate_request( $id, 'shop_coupon', 'delete' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
do_action( 'woocommerce_api_delete_coupon', $id, $this );
|
||||
|
||||
return $this->delete( $id, 'shop_coupon', ( 'true' === $force ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* expiry_date format
|
||||
*
|
||||
* @since 2.3.0
|
||||
* @param string $expiry_date
|
||||
* @param bool $as_timestamp (default: false)
|
||||
* @return string|int
|
||||
*/
|
||||
protected function get_coupon_expiry_date( $expiry_date, $as_timestamp = false ) {
|
||||
if ( '' != $expiry_date ) {
|
||||
if ( $as_timestamp ) {
|
||||
return strtotime( $expiry_date );
|
||||
}
|
||||
|
||||
return date( 'Y-m-d', strtotime( $expiry_date ) );
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get coupon post objects
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $args request arguments for filtering query
|
||||
* @return WP_Query
|
||||
*/
|
||||
private function query_coupons( $args ) {
|
||||
|
||||
// set base query arguments
|
||||
$query_args = array(
|
||||
'fields' => 'ids',
|
||||
'post_type' => 'shop_coupon',
|
||||
'post_status' => 'publish',
|
||||
);
|
||||
|
||||
$query_args = $this->merge_query_args( $query_args, $args );
|
||||
|
||||
return new WP_Query( $query_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk update or insert coupons
|
||||
* Accepts an array with coupons in the formats supported by
|
||||
* WC_API_Coupons->create_coupon() and WC_API_Coupons->edit_coupon()
|
||||
*
|
||||
* @since 2.4.0
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function bulk( $data ) {
|
||||
|
||||
try {
|
||||
if ( ! isset( $data['coupons'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_coupons_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'coupons' ), 400 );
|
||||
}
|
||||
|
||||
$data = $data['coupons'];
|
||||
$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'coupons' );
|
||||
|
||||
// Limit bulk operation
|
||||
if ( count( $data ) > $limit ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_coupons_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
|
||||
}
|
||||
|
||||
$coupons = array();
|
||||
|
||||
foreach ( $data as $_coupon ) {
|
||||
$coupon_id = 0;
|
||||
|
||||
// Try to get the coupon ID
|
||||
if ( isset( $_coupon['id'] ) ) {
|
||||
$coupon_id = intval( $_coupon['id'] );
|
||||
}
|
||||
|
||||
if ( $coupon_id ) {
|
||||
|
||||
// Coupon exists / edit coupon
|
||||
$edit = $this->edit_coupon( $coupon_id, array( 'coupon' => $_coupon ) );
|
||||
|
||||
if ( is_wp_error( $edit ) ) {
|
||||
$coupons[] = array(
|
||||
'id' => $coupon_id,
|
||||
'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
|
||||
);
|
||||
} else {
|
||||
$coupons[] = $edit['coupon'];
|
||||
}
|
||||
} else {
|
||||
|
||||
// Coupon don't exists / create coupon
|
||||
$new = $this->create_coupon( array( 'coupon' => $_coupon ) );
|
||||
|
||||
if ( is_wp_error( $new ) ) {
|
||||
$coupons[] = array(
|
||||
'id' => $coupon_id,
|
||||
'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
|
||||
);
|
||||
} else {
|
||||
$coupons[] = $new['coupon'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array( 'coupons' => apply_filters( 'woocommerce_api_coupons_bulk_response', $coupons, $this ) );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,829 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API Customers Class
|
||||
*
|
||||
* Handles requests to the /customers endpoint
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.2
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_Customers extends WC_API_Resource {
|
||||
|
||||
/** @var string $base the route base */
|
||||
protected $base = '/customers';
|
||||
|
||||
/** @var string $created_at_min for date filtering */
|
||||
private $created_at_min = null;
|
||||
|
||||
/** @var string $created_at_max for date filtering */
|
||||
private $created_at_max = null;
|
||||
|
||||
/**
|
||||
* Setup class, overridden to provide customer data to order response
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WC_API_Server $server
|
||||
*/
|
||||
public function __construct( WC_API_Server $server ) {
|
||||
|
||||
parent::__construct( $server );
|
||||
|
||||
// add customer data to order responses
|
||||
add_filter( 'woocommerce_api_order_response', array( $this, 'add_customer_data' ), 10, 2 );
|
||||
|
||||
// modify WP_User_Query to support created_at date filtering
|
||||
add_action( 'pre_user_query', array( $this, 'modify_user_query' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the routes for this class
|
||||
*
|
||||
* GET /customers
|
||||
* GET /customers/count
|
||||
* GET /customers/<id>
|
||||
* GET /customers/<id>/orders
|
||||
*
|
||||
* @since 2.2
|
||||
* @param array $routes
|
||||
* @return array
|
||||
*/
|
||||
public function register_routes( $routes ) {
|
||||
|
||||
# GET/POST /customers
|
||||
$routes[ $this->base ] = array(
|
||||
array( array( $this, 'get_customers' ), WC_API_SERVER::READABLE ),
|
||||
array( array( $this, 'create_customer' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
|
||||
);
|
||||
|
||||
# GET /customers/count
|
||||
$routes[ $this->base . '/count' ] = array(
|
||||
array( array( $this, 'get_customers_count' ), WC_API_SERVER::READABLE ),
|
||||
);
|
||||
|
||||
# GET/PUT/DELETE /customers/<id>
|
||||
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
|
||||
array( array( $this, 'get_customer' ), WC_API_SERVER::READABLE ),
|
||||
array( array( $this, 'edit_customer' ), WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ),
|
||||
array( array( $this, 'delete_customer' ), WC_API_SERVER::DELETABLE ),
|
||||
);
|
||||
|
||||
# GET /customers/email/<email>
|
||||
$routes[ $this->base . '/email/(?P<email>.+)' ] = array(
|
||||
array( array( $this, 'get_customer_by_email' ), WC_API_SERVER::READABLE ),
|
||||
);
|
||||
|
||||
# GET /customers/<id>/orders
|
||||
$routes[ $this->base . '/(?P<id>\d+)/orders' ] = array(
|
||||
array( array( $this, 'get_customer_orders' ), WC_API_SERVER::READABLE ),
|
||||
);
|
||||
|
||||
# GET /customers/<id>/downloads
|
||||
$routes[ $this->base . '/(?P<id>\d+)/downloads' ] = array(
|
||||
array( array( $this, 'get_customer_downloads' ), WC_API_SERVER::READABLE ),
|
||||
);
|
||||
|
||||
# POST|PUT /customers/bulk
|
||||
$routes[ $this->base . '/bulk' ] = array(
|
||||
array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
|
||||
);
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all customers
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $fields
|
||||
* @param array $filter
|
||||
* @param int $page
|
||||
* @return array
|
||||
*/
|
||||
public function get_customers( $fields = null, $filter = array(), $page = 1 ) {
|
||||
|
||||
$filter['page'] = $page;
|
||||
|
||||
$query = $this->query_customers( $filter );
|
||||
|
||||
$customers = array();
|
||||
|
||||
foreach ( $query->get_results() as $user_id ) {
|
||||
|
||||
if ( ! $this->is_readable( $user_id ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$customers[] = current( $this->get_customer( $user_id, $fields ) );
|
||||
}
|
||||
|
||||
$this->server->add_pagination_headers( $query );
|
||||
|
||||
return array( 'customers' => $customers );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the customer for the given ID
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int $id the customer ID
|
||||
* @param array $fields
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_customer( $id, $fields = null ) {
|
||||
global $wpdb;
|
||||
|
||||
$id = $this->validate_request( $id, 'customer', 'read' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$customer = new WC_Customer( $id );
|
||||
$last_order = $customer->get_last_order();
|
||||
$customer_data = array(
|
||||
'id' => $customer->get_id(),
|
||||
'created_at' => $this->server->format_datetime( $customer->get_date_created() ? $customer->get_date_created()->getTimestamp() : 0 ), // API gives UTC times.
|
||||
'last_update' => $this->server->format_datetime( $customer->get_date_modified() ? $customer->get_date_modified()->getTimestamp() : 0 ), // API gives UTC times.
|
||||
'email' => $customer->get_email(),
|
||||
'first_name' => $customer->get_first_name(),
|
||||
'last_name' => $customer->get_last_name(),
|
||||
'username' => $customer->get_username(),
|
||||
'role' => $customer->get_role(),
|
||||
'last_order_id' => is_object( $last_order ) ? $last_order->get_id() : null,
|
||||
'last_order_date' => is_object( $last_order ) ? $this->server->format_datetime( $last_order->get_date_created() ? $last_order->get_date_created()->getTimestamp() : 0 ) : null, // API gives UTC times.
|
||||
'orders_count' => $customer->get_order_count(),
|
||||
'total_spent' => wc_format_decimal( $customer->get_total_spent(), 2 ),
|
||||
'avatar_url' => $customer->get_avatar_url(),
|
||||
'billing_address' => array(
|
||||
'first_name' => $customer->get_billing_first_name(),
|
||||
'last_name' => $customer->get_billing_last_name(),
|
||||
'company' => $customer->get_billing_company(),
|
||||
'address_1' => $customer->get_billing_address_1(),
|
||||
'address_2' => $customer->get_billing_address_2(),
|
||||
'city' => $customer->get_billing_city(),
|
||||
'state' => $customer->get_billing_state(),
|
||||
'postcode' => $customer->get_billing_postcode(),
|
||||
'country' => $customer->get_billing_country(),
|
||||
'email' => $customer->get_billing_email(),
|
||||
'phone' => $customer->get_billing_phone(),
|
||||
),
|
||||
'shipping_address' => array(
|
||||
'first_name' => $customer->get_shipping_first_name(),
|
||||
'last_name' => $customer->get_shipping_last_name(),
|
||||
'company' => $customer->get_shipping_company(),
|
||||
'address_1' => $customer->get_shipping_address_1(),
|
||||
'address_2' => $customer->get_shipping_address_2(),
|
||||
'city' => $customer->get_shipping_city(),
|
||||
'state' => $customer->get_shipping_state(),
|
||||
'postcode' => $customer->get_shipping_postcode(),
|
||||
'country' => $customer->get_shipping_country(),
|
||||
),
|
||||
);
|
||||
|
||||
return array( 'customer' => apply_filters( 'woocommerce_api_customer_response', $customer_data, $customer, $fields, $this->server ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the customer for the given email
|
||||
*
|
||||
* @since 2.1
|
||||
*
|
||||
* @param string $email the customer email
|
||||
* @param array $fields
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_customer_by_email( $email, $fields = null ) {
|
||||
try {
|
||||
if ( is_email( $email ) ) {
|
||||
$customer = get_user_by( 'email', $email );
|
||||
if ( ! is_object( $customer ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 );
|
||||
}
|
||||
} else {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 );
|
||||
}
|
||||
|
||||
return $this->get_customer( $customer->ID, $fields );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of customers
|
||||
*
|
||||
* @since 2.1
|
||||
*
|
||||
* @param array $filter
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_customers_count( $filter = array() ) {
|
||||
try {
|
||||
if ( ! current_user_can( 'list_users' ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customers_count', __( 'You do not have permission to read the customers count', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$query = $this->query_customers( $filter );
|
||||
|
||||
return array( 'count' => $query->get_total() );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customer billing address fields.
|
||||
*
|
||||
* @since 2.2
|
||||
* @return array
|
||||
*/
|
||||
protected function get_customer_billing_address() {
|
||||
$billing_address = apply_filters( 'woocommerce_api_customer_billing_address', array(
|
||||
'first_name',
|
||||
'last_name',
|
||||
'company',
|
||||
'address_1',
|
||||
'address_2',
|
||||
'city',
|
||||
'state',
|
||||
'postcode',
|
||||
'country',
|
||||
'email',
|
||||
'phone',
|
||||
) );
|
||||
|
||||
return $billing_address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customer shipping address fields.
|
||||
*
|
||||
* @since 2.2
|
||||
* @return array
|
||||
*/
|
||||
protected function get_customer_shipping_address() {
|
||||
$shipping_address = apply_filters( 'woocommerce_api_customer_shipping_address', array(
|
||||
'first_name',
|
||||
'last_name',
|
||||
'company',
|
||||
'address_1',
|
||||
'address_2',
|
||||
'city',
|
||||
'state',
|
||||
'postcode',
|
||||
'country',
|
||||
) );
|
||||
|
||||
return $shipping_address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add/Update customer data.
|
||||
*
|
||||
* @since 2.2
|
||||
* @param int $id the customer ID
|
||||
* @param array $data
|
||||
* @param WC_Customer $customer
|
||||
*/
|
||||
protected function update_customer_data( $id, $data, $customer ) {
|
||||
|
||||
// Customer first name.
|
||||
if ( isset( $data['first_name'] ) ) {
|
||||
$customer->set_first_name( wc_clean( $data['first_name'] ) );
|
||||
}
|
||||
|
||||
// Customer last name.
|
||||
if ( isset( $data['last_name'] ) ) {
|
||||
$customer->set_last_name( wc_clean( $data['last_name'] ) );
|
||||
}
|
||||
|
||||
// Customer billing address.
|
||||
if ( isset( $data['billing_address'] ) ) {
|
||||
foreach ( $this->get_customer_billing_address() as $field ) {
|
||||
if ( isset( $data['billing_address'][ $field ] ) ) {
|
||||
if ( is_callable( array( $customer, "set_billing_{$field}" ) ) ) {
|
||||
$customer->{"set_billing_{$field}"}( $data['billing_address'][ $field ] );
|
||||
} else {
|
||||
$customer->update_meta_data( 'billing_' . $field, wc_clean( $data['billing_address'][ $field ] ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Customer shipping address.
|
||||
if ( isset( $data['shipping_address'] ) ) {
|
||||
foreach ( $this->get_customer_shipping_address() as $field ) {
|
||||
if ( isset( $data['shipping_address'][ $field ] ) ) {
|
||||
if ( is_callable( array( $customer, "set_shipping_{$field}" ) ) ) {
|
||||
$customer->{"set_shipping_{$field}"}( $data['shipping_address'][ $field ] );
|
||||
} else {
|
||||
$customer->update_meta_data( 'shipping_' . $field, wc_clean( $data['shipping_address'][ $field ] ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
do_action( 'woocommerce_api_update_customer_data', $id, $data, $customer );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a customer
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function create_customer( $data ) {
|
||||
try {
|
||||
if ( ! isset( $data['customer'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'customer' ), 400 );
|
||||
}
|
||||
|
||||
$data = $data['customer'];
|
||||
|
||||
// Checks with can create new users.
|
||||
if ( ! current_user_can( 'create_users' ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'You do not have permission to create this customer', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$data = apply_filters( 'woocommerce_api_create_customer_data', $data, $this );
|
||||
|
||||
// Checks with the email is missing.
|
||||
if ( ! isset( $data['email'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_customer_email', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'email' ), 400 );
|
||||
}
|
||||
|
||||
// Create customer.
|
||||
$customer = new WC_Customer;
|
||||
$customer->set_username( ! empty( $data['username'] ) ? $data['username'] : '' );
|
||||
$customer->set_password( ! empty( $data['password'] ) ? $data['password'] : '' );
|
||||
$customer->set_email( $data['email'] );
|
||||
$customer->save();
|
||||
|
||||
if ( ! $customer->get_id() ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'This resource cannot be created.', 'woocommerce' ), 400 );
|
||||
}
|
||||
|
||||
// Added customer data.
|
||||
$this->update_customer_data( $customer->get_id(), $data, $customer );
|
||||
$customer->save();
|
||||
|
||||
do_action( 'woocommerce_api_create_customer', $customer->get_id(), $data );
|
||||
|
||||
$this->server->send_status( 201 );
|
||||
|
||||
return $this->get_customer( $customer->get_id() );
|
||||
} catch ( Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a customer
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @param int $id the customer ID
|
||||
* @param array $data
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function edit_customer( $id, $data ) {
|
||||
try {
|
||||
if ( ! isset( $data['customer'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'customer' ), 400 );
|
||||
}
|
||||
|
||||
$data = $data['customer'];
|
||||
|
||||
// Validate the customer ID.
|
||||
$id = $this->validate_request( $id, 'customer', 'edit' );
|
||||
|
||||
// Return the validate error.
|
||||
if ( is_wp_error( $id ) ) {
|
||||
throw new WC_API_Exception( $id->get_error_code(), $id->get_error_message(), 400 );
|
||||
}
|
||||
|
||||
$data = apply_filters( 'woocommerce_api_edit_customer_data', $data, $this );
|
||||
|
||||
$customer = new WC_Customer( $id );
|
||||
|
||||
// Customer email.
|
||||
if ( isset( $data['email'] ) ) {
|
||||
$customer->set_email( $data['email'] );
|
||||
}
|
||||
|
||||
// Customer password.
|
||||
if ( isset( $data['password'] ) ) {
|
||||
$customer->set_password( $data['password'] );
|
||||
}
|
||||
|
||||
// Update customer data.
|
||||
$this->update_customer_data( $customer->get_id(), $data, $customer );
|
||||
|
||||
$customer->save();
|
||||
|
||||
do_action( 'woocommerce_api_edit_customer', $customer->get_id(), $data );
|
||||
|
||||
return $this->get_customer( $customer->get_id() );
|
||||
} catch ( Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a customer
|
||||
*
|
||||
* @since 2.2
|
||||
* @param int $id the customer ID
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function delete_customer( $id ) {
|
||||
|
||||
// Validate the customer ID.
|
||||
$id = $this->validate_request( $id, 'customer', 'delete' );
|
||||
|
||||
// Return the validate error.
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
do_action( 'woocommerce_api_delete_customer', $id, $this );
|
||||
|
||||
return $this->delete( $id, 'customer' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the orders for a customer
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int $id the customer ID
|
||||
* @param string $fields fields to include in response
|
||||
* @param array $filter filters
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_customer_orders( $id, $fields = null, $filter = array() ) {
|
||||
$id = $this->validate_request( $id, 'customer', 'read' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$filter['customer_id'] = $id;
|
||||
$orders = WC()->api->WC_API_Orders->get_orders( $fields, $filter, null, -1 );
|
||||
|
||||
return $orders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available downloads for a customer
|
||||
*
|
||||
* @since 2.2
|
||||
* @param int $id the customer ID
|
||||
* @param string $fields fields to include in response
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_customer_downloads( $id, $fields = null ) {
|
||||
$id = $this->validate_request( $id, 'customer', 'read' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$downloads = array();
|
||||
$_downloads = wc_get_customer_available_downloads( $id );
|
||||
|
||||
foreach ( $_downloads as $key => $download ) {
|
||||
$downloads[] = array(
|
||||
'download_url' => $download['download_url'],
|
||||
'download_id' => $download['download_id'],
|
||||
'product_id' => $download['product_id'],
|
||||
'download_name' => $download['download_name'],
|
||||
'order_id' => $download['order_id'],
|
||||
'order_key' => $download['order_key'],
|
||||
'downloads_remaining' => $download['downloads_remaining'],
|
||||
'access_expires' => $download['access_expires'] ? $this->server->format_datetime( $download['access_expires'] ) : null,
|
||||
'file' => $download['file'],
|
||||
);
|
||||
}
|
||||
|
||||
return array( 'downloads' => apply_filters( 'woocommerce_api_customer_downloads_response', $downloads, $id, $fields, $this->server ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get customer user objects
|
||||
*
|
||||
* Note that WP_User_Query does not have built-in pagination so limit & offset are used to provide limited
|
||||
* pagination support
|
||||
*
|
||||
* The filter for role can only be a single role in a string.
|
||||
*
|
||||
* @since 2.3
|
||||
* @param array $args request arguments for filtering query
|
||||
* @return WP_User_Query
|
||||
*/
|
||||
private function query_customers( $args = array() ) {
|
||||
|
||||
// default users per page
|
||||
$users_per_page = get_option( 'posts_per_page' );
|
||||
|
||||
// Set base query arguments
|
||||
$query_args = array(
|
||||
'fields' => 'ID',
|
||||
'role' => 'customer',
|
||||
'orderby' => 'registered',
|
||||
'number' => $users_per_page,
|
||||
);
|
||||
|
||||
// Custom Role
|
||||
if ( ! empty( $args['role'] ) ) {
|
||||
$query_args['role'] = $args['role'];
|
||||
|
||||
// Show users on all roles
|
||||
if ( 'all' === $query_args['role'] ) {
|
||||
unset( $query_args['role'] );
|
||||
}
|
||||
}
|
||||
|
||||
// Search
|
||||
if ( ! empty( $args['q'] ) ) {
|
||||
$query_args['search'] = $args['q'];
|
||||
}
|
||||
|
||||
// Limit number of users returned
|
||||
if ( ! empty( $args['limit'] ) ) {
|
||||
if ( -1 == $args['limit'] ) {
|
||||
unset( $query_args['number'] );
|
||||
} else {
|
||||
$query_args['number'] = absint( $args['limit'] );
|
||||
$users_per_page = absint( $args['limit'] );
|
||||
}
|
||||
} else {
|
||||
$args['limit'] = $query_args['number'];
|
||||
}
|
||||
|
||||
// Page
|
||||
$page = ( isset( $args['page'] ) ) ? absint( $args['page'] ) : 1;
|
||||
|
||||
// Offset
|
||||
if ( ! empty( $args['offset'] ) ) {
|
||||
$query_args['offset'] = absint( $args['offset'] );
|
||||
} else {
|
||||
$query_args['offset'] = $users_per_page * ( $page - 1 );
|
||||
}
|
||||
|
||||
// Created date
|
||||
if ( ! empty( $args['created_at_min'] ) ) {
|
||||
$this->created_at_min = $this->server->parse_datetime( $args['created_at_min'] );
|
||||
}
|
||||
|
||||
if ( ! empty( $args['created_at_max'] ) ) {
|
||||
$this->created_at_max = $this->server->parse_datetime( $args['created_at_max'] );
|
||||
}
|
||||
|
||||
// Order (ASC or DESC, ASC by default)
|
||||
if ( ! empty( $args['order'] ) ) {
|
||||
$query_args['order'] = $args['order'];
|
||||
}
|
||||
|
||||
// Order by
|
||||
if ( ! empty( $args['orderby'] ) ) {
|
||||
$query_args['orderby'] = $args['orderby'];
|
||||
|
||||
// Allow sorting by meta value
|
||||
if ( ! empty( $args['orderby_meta_key'] ) ) {
|
||||
$query_args['meta_key'] = $args['orderby_meta_key'];
|
||||
}
|
||||
}
|
||||
|
||||
$query = new WP_User_Query( $query_args );
|
||||
|
||||
// Helper members for pagination headers
|
||||
$query->total_pages = ( -1 == $args['limit'] ) ? 1 : ceil( $query->get_total() / $users_per_page );
|
||||
$query->page = $page;
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add customer data to orders
|
||||
*
|
||||
* @since 2.1
|
||||
* @param $order_data
|
||||
* @param $order
|
||||
* @return array
|
||||
*/
|
||||
public function add_customer_data( $order_data, $order ) {
|
||||
|
||||
if ( 0 == $order->get_user_id() ) {
|
||||
|
||||
// add customer data from order
|
||||
$order_data['customer'] = array(
|
||||
'id' => 0,
|
||||
'email' => $order->get_billing_email(),
|
||||
'first_name' => $order->get_billing_first_name(),
|
||||
'last_name' => $order->get_billing_last_name(),
|
||||
'billing_address' => array(
|
||||
'first_name' => $order->get_billing_first_name(),
|
||||
'last_name' => $order->get_billing_last_name(),
|
||||
'company' => $order->get_billing_company(),
|
||||
'address_1' => $order->get_billing_address_1(),
|
||||
'address_2' => $order->get_billing_address_2(),
|
||||
'city' => $order->get_billing_city(),
|
||||
'state' => $order->get_billing_state(),
|
||||
'postcode' => $order->get_billing_postcode(),
|
||||
'country' => $order->get_billing_country(),
|
||||
'email' => $order->get_billing_email(),
|
||||
'phone' => $order->get_billing_phone(),
|
||||
),
|
||||
'shipping_address' => array(
|
||||
'first_name' => $order->get_shipping_first_name(),
|
||||
'last_name' => $order->get_shipping_last_name(),
|
||||
'company' => $order->get_shipping_company(),
|
||||
'address_1' => $order->get_shipping_address_1(),
|
||||
'address_2' => $order->get_shipping_address_2(),
|
||||
'city' => $order->get_shipping_city(),
|
||||
'state' => $order->get_shipping_state(),
|
||||
'postcode' => $order->get_shipping_postcode(),
|
||||
'country' => $order->get_shipping_country(),
|
||||
),
|
||||
);
|
||||
|
||||
} else {
|
||||
|
||||
$order_data['customer'] = current( $this->get_customer( $order->get_user_id() ) );
|
||||
}
|
||||
|
||||
return $order_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the WP_User_Query to support filtering on the date the customer was created
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WP_User_Query $query
|
||||
*/
|
||||
public function modify_user_query( $query ) {
|
||||
|
||||
if ( $this->created_at_min ) {
|
||||
$query->query_where .= sprintf( " AND user_registered >= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%H:%%i:%%s' )", esc_sql( $this->created_at_min ) );
|
||||
}
|
||||
|
||||
if ( $this->created_at_max ) {
|
||||
$query->query_where .= sprintf( " AND user_registered <= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%H:%%i:%%s' )", esc_sql( $this->created_at_max ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the request by checking:
|
||||
*
|
||||
* 1) the ID is a valid integer
|
||||
* 2) the ID returns a valid WP_User
|
||||
* 3) the current user has the proper permissions
|
||||
*
|
||||
* @since 2.1
|
||||
* @see WC_API_Resource::validate_request()
|
||||
* @param integer $id the customer ID
|
||||
* @param string $type the request type, unused because this method overrides the parent class
|
||||
* @param string $context the context of the request, either `read`, `edit` or `delete`
|
||||
* @return int|WP_Error valid user ID or WP_Error if any of the checks fails
|
||||
*/
|
||||
protected function validate_request( $id, $type, $context ) {
|
||||
|
||||
try {
|
||||
$id = absint( $id );
|
||||
|
||||
// validate ID
|
||||
if ( empty( $id ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Invalid customer ID', 'woocommerce' ), 404 );
|
||||
}
|
||||
|
||||
// non-existent IDs return a valid WP_User object with the user ID = 0
|
||||
$customer = new WP_User( $id );
|
||||
|
||||
if ( 0 === $customer->ID ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_customer', __( 'Invalid customer', 'woocommerce' ), 404 );
|
||||
}
|
||||
|
||||
// validate permissions
|
||||
switch ( $context ) {
|
||||
|
||||
case 'read':
|
||||
if ( ! current_user_can( 'list_users' ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read this customer', 'woocommerce' ), 401 );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'edit':
|
||||
if ( ! wc_rest_check_user_permissions( 'edit', $customer->ID ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_customer', __( 'You do not have permission to edit this customer', 'woocommerce' ), 401 );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
if ( ! wc_rest_check_user_permissions( 'delete', $customer->ID ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_customer', __( 'You do not have permission to delete this customer', 'woocommerce' ), 401 );
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return $id;
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user can read users
|
||||
*
|
||||
* @since 2.1
|
||||
* @see WC_API_Resource::is_readable()
|
||||
* @param int|WP_Post $post unused
|
||||
* @return bool true if the current user can read users, false otherwise
|
||||
*/
|
||||
protected function is_readable( $post ) {
|
||||
return current_user_can( 'list_users' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk update or insert customers
|
||||
* Accepts an array with customers in the formats supported by
|
||||
* WC_API_Customers->create_customer() and WC_API_Customers->edit_customer()
|
||||
*
|
||||
* @since 2.4.0
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function bulk( $data ) {
|
||||
|
||||
try {
|
||||
if ( ! isset( $data['customers'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_customers_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'customers' ), 400 );
|
||||
}
|
||||
|
||||
$data = $data['customers'];
|
||||
$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'customers' );
|
||||
|
||||
// Limit bulk operation
|
||||
if ( count( $data ) > $limit ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_customers_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
|
||||
}
|
||||
|
||||
$customers = array();
|
||||
|
||||
foreach ( $data as $_customer ) {
|
||||
$customer_id = 0;
|
||||
|
||||
// Try to get the customer ID
|
||||
if ( isset( $_customer['id'] ) ) {
|
||||
$customer_id = intval( $_customer['id'] );
|
||||
}
|
||||
|
||||
if ( $customer_id ) {
|
||||
|
||||
// Customer exists / edit customer
|
||||
$edit = $this->edit_customer( $customer_id, array( 'customer' => $_customer ) );
|
||||
|
||||
if ( is_wp_error( $edit ) ) {
|
||||
$customers[] = array(
|
||||
'id' => $customer_id,
|
||||
'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
|
||||
);
|
||||
} else {
|
||||
$customers[] = $edit['customer'];
|
||||
}
|
||||
} else {
|
||||
|
||||
// Customer don't exists / create customer
|
||||
$new = $this->create_customer( array( 'customer' => $_customer ) );
|
||||
|
||||
if ( is_wp_error( $new ) ) {
|
||||
$customers[] = array(
|
||||
'id' => $customer_id,
|
||||
'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
|
||||
);
|
||||
} else {
|
||||
$customers[] = $new['customer'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array( 'customers' => apply_filters( 'woocommerce_api_customers_bulk_response', $customers, $this ) );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API Exception Class
|
||||
*
|
||||
* Extends Exception to provide additional data
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.2
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_Exception extends Exception {
|
||||
|
||||
/** @var string sanitized error code */
|
||||
protected $error_code;
|
||||
|
||||
/**
|
||||
* Setup exception, requires 3 params:
|
||||
*
|
||||
* error code - machine-readable, e.g. `woocommerce_invalid_product_id`
|
||||
* error message - friendly message, e.g. 'Product ID is invalid'
|
||||
* http status code - proper HTTP status code to respond with, e.g. 400
|
||||
*
|
||||
* @since 2.2
|
||||
* @param string $error_code
|
||||
* @param string $error_message user-friendly translated error message
|
||||
* @param int $http_status_code HTTP status code to respond with
|
||||
*/
|
||||
public function __construct( $error_code, $error_message, $http_status_code ) {
|
||||
$this->error_code = $error_code;
|
||||
parent::__construct( $error_message, $http_status_code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error code
|
||||
*
|
||||
* @since 2.2
|
||||
* @return string
|
||||
*/
|
||||
public function getErrorCode() {
|
||||
return $this->error_code;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API
|
||||
*
|
||||
* Handles parsing JSON request bodies and generating JSON responses
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_JSON_Handler implements WC_API_Handler {
|
||||
|
||||
/**
|
||||
* Get the content type for the response
|
||||
*
|
||||
* @since 2.1
|
||||
* @return string
|
||||
*/
|
||||
public function get_content_type() {
|
||||
|
||||
return sprintf( '%s; charset=%s', isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json', get_option( 'blog_charset' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the raw request body entity
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $body the raw request body
|
||||
* @return array|mixed
|
||||
*/
|
||||
public function parse_body( $body ) {
|
||||
|
||||
return json_decode( $body, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a JSON response given an array of data
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $data the response data
|
||||
* @return string
|
||||
*/
|
||||
public function generate_response( $data ) {
|
||||
if ( isset( $_GET['_jsonp'] ) ) {
|
||||
|
||||
if ( ! apply_filters( 'woocommerce_api_jsonp_enabled', true ) ) {
|
||||
WC()->api->server->send_status( 400 );
|
||||
return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) ) );
|
||||
}
|
||||
|
||||
$jsonp_callback = $_GET['_jsonp'];
|
||||
|
||||
if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) {
|
||||
WC()->api->server->send_status( 400 );
|
||||
return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ) );
|
||||
}
|
||||
|
||||
WC()->api->server->header( 'X-Content-Type-Options', 'nosniff' );
|
||||
|
||||
// Prepend '/**/' to mitigate possible JSONP Flash attacks.
|
||||
// https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
|
||||
return '/**/' . $jsonp_callback . '(' . wp_json_encode( $data ) . ')';
|
||||
}
|
||||
|
||||
return wp_json_encode( $data );
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,330 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API Reports Class
|
||||
*
|
||||
* Handles requests to the /reports endpoint
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_Reports extends WC_API_Resource {
|
||||
|
||||
/** @var string $base the route base */
|
||||
protected $base = '/reports';
|
||||
|
||||
/** @var WC_Admin_Report instance */
|
||||
private $report;
|
||||
|
||||
/**
|
||||
* Register the routes for this class
|
||||
*
|
||||
* GET /reports
|
||||
* GET /reports/sales
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $routes
|
||||
* @return array
|
||||
*/
|
||||
public function register_routes( $routes ) {
|
||||
|
||||
# GET /reports
|
||||
$routes[ $this->base ] = array(
|
||||
array( array( $this, 'get_reports' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# GET /reports/sales
|
||||
$routes[ $this->base . '/sales' ] = array(
|
||||
array( array( $this, 'get_sales_report' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# GET /reports/sales/top_sellers
|
||||
$routes[ $this->base . '/sales/top_sellers' ] = array(
|
||||
array( array( $this, 'get_top_sellers_report' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a simple listing of available reports
|
||||
*
|
||||
* @since 2.1
|
||||
* @return array
|
||||
*/
|
||||
public function get_reports() {
|
||||
return array( 'reports' => array( 'sales', 'sales/top_sellers' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sales report
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $fields fields to include in response
|
||||
* @param array $filter date filtering
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_sales_report( $fields = null, $filter = array() ) {
|
||||
|
||||
// check user permissions
|
||||
$check = $this->validate_request();
|
||||
|
||||
// check for WP_Error
|
||||
if ( is_wp_error( $check ) ) {
|
||||
return $check;
|
||||
}
|
||||
|
||||
// set date filtering
|
||||
$this->setup_report( $filter );
|
||||
|
||||
// new customers
|
||||
$users_query = new WP_User_Query(
|
||||
array(
|
||||
'fields' => array( 'user_registered' ),
|
||||
'role' => 'customer',
|
||||
)
|
||||
);
|
||||
|
||||
$customers = $users_query->get_results();
|
||||
|
||||
foreach ( $customers as $key => $customer ) {
|
||||
if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) {
|
||||
unset( $customers[ $key ] );
|
||||
}
|
||||
}
|
||||
|
||||
$total_customers = count( $customers );
|
||||
$report_data = $this->report->get_report_data();
|
||||
$period_totals = array();
|
||||
|
||||
// setup period totals by ensuring each period in the interval has data
|
||||
for ( $i = 0; $i <= $this->report->chart_interval; $i ++ ) {
|
||||
|
||||
switch ( $this->report->chart_groupby ) {
|
||||
case 'day' :
|
||||
$time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) );
|
||||
break;
|
||||
default :
|
||||
$time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) );
|
||||
break;
|
||||
}
|
||||
|
||||
// set the customer signups for each period
|
||||
$customer_count = 0;
|
||||
foreach ( $customers as $customer ) {
|
||||
if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) {
|
||||
$customer_count++;
|
||||
}
|
||||
}
|
||||
|
||||
$period_totals[ $time ] = array(
|
||||
'sales' => wc_format_decimal( 0.00, 2 ),
|
||||
'orders' => 0,
|
||||
'items' => 0,
|
||||
'tax' => wc_format_decimal( 0.00, 2 ),
|
||||
'shipping' => wc_format_decimal( 0.00, 2 ),
|
||||
'discount' => wc_format_decimal( 0.00, 2 ),
|
||||
'customers' => $customer_count,
|
||||
);
|
||||
}
|
||||
|
||||
// add total sales, total order count, total tax and total shipping for each period
|
||||
foreach ( $report_data->orders as $order ) {
|
||||
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );
|
||||
|
||||
if ( ! isset( $period_totals[ $time ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$period_totals[ $time ]['sales'] = wc_format_decimal( $order->total_sales, 2 );
|
||||
$period_totals[ $time ]['tax'] = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 );
|
||||
$period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 );
|
||||
}
|
||||
|
||||
foreach ( $report_data->order_counts as $order ) {
|
||||
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );
|
||||
|
||||
if ( ! isset( $period_totals[ $time ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$period_totals[ $time ]['orders'] = (int) $order->count;
|
||||
}
|
||||
|
||||
// add total order items for each period
|
||||
foreach ( $report_data->order_items as $order_item ) {
|
||||
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) );
|
||||
|
||||
if ( ! isset( $period_totals[ $time ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$period_totals[ $time ]['items'] = (int) $order_item->order_item_count;
|
||||
}
|
||||
|
||||
// add total discount for each period
|
||||
foreach ( $report_data->coupons as $discount ) {
|
||||
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) );
|
||||
|
||||
if ( ! isset( $period_totals[ $time ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 );
|
||||
}
|
||||
|
||||
$sales_data = array(
|
||||
'total_sales' => $report_data->total_sales,
|
||||
'net_sales' => $report_data->net_sales,
|
||||
'average_sales' => $report_data->average_sales,
|
||||
'total_orders' => $report_data->total_orders,
|
||||
'total_items' => $report_data->total_items,
|
||||
'total_tax' => wc_format_decimal( $report_data->total_tax + $report_data->total_shipping_tax, 2 ),
|
||||
'total_shipping' => $report_data->total_shipping,
|
||||
'total_refunds' => $report_data->total_refunds,
|
||||
'total_discount' => $report_data->total_coupons,
|
||||
'totals_grouped_by' => $this->report->chart_groupby,
|
||||
'totals' => $period_totals,
|
||||
'total_customers' => $total_customers,
|
||||
);
|
||||
|
||||
return array( 'sales' => apply_filters( 'woocommerce_api_report_response', $sales_data, $this->report, $fields, $this->server ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the top sellers report
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $fields fields to include in response
|
||||
* @param array $filter date filtering
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_top_sellers_report( $fields = null, $filter = array() ) {
|
||||
|
||||
// check user permissions
|
||||
$check = $this->validate_request();
|
||||
|
||||
if ( is_wp_error( $check ) ) {
|
||||
return $check;
|
||||
}
|
||||
|
||||
// set date filtering
|
||||
$this->setup_report( $filter );
|
||||
|
||||
$top_sellers = $this->report->get_order_report_data( array(
|
||||
'data' => array(
|
||||
'_product_id' => array(
|
||||
'type' => 'order_item_meta',
|
||||
'order_item_type' => 'line_item',
|
||||
'function' => '',
|
||||
'name' => 'product_id',
|
||||
),
|
||||
'_qty' => array(
|
||||
'type' => 'order_item_meta',
|
||||
'order_item_type' => 'line_item',
|
||||
'function' => 'SUM',
|
||||
'name' => 'order_item_qty',
|
||||
),
|
||||
),
|
||||
'order_by' => 'order_item_qty DESC',
|
||||
'group_by' => 'product_id',
|
||||
'limit' => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12,
|
||||
'query_type' => 'get_results',
|
||||
'filter_range' => true,
|
||||
) );
|
||||
|
||||
$top_sellers_data = array();
|
||||
|
||||
foreach ( $top_sellers as $top_seller ) {
|
||||
|
||||
$product = wc_get_product( $top_seller->product_id );
|
||||
|
||||
if ( $product ) {
|
||||
$top_sellers_data[] = array(
|
||||
'title' => $product->get_name(),
|
||||
'product_id' => $top_seller->product_id,
|
||||
'quantity' => $top_seller->order_item_qty,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return array( 'top_sellers' => apply_filters( 'woocommerce_api_report_response', $top_sellers_data, $this->report, $fields, $this->server ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the report object and parse any date filtering
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $filter date filtering
|
||||
*/
|
||||
private function setup_report( $filter ) {
|
||||
|
||||
include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' );
|
||||
include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-report-sales-by-date.php' );
|
||||
|
||||
$this->report = new WC_Report_Sales_By_Date();
|
||||
|
||||
if ( empty( $filter['period'] ) ) {
|
||||
|
||||
// custom date range
|
||||
$filter['period'] = 'custom';
|
||||
|
||||
if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) {
|
||||
|
||||
// overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges
|
||||
$_GET['start_date'] = $this->server->parse_datetime( $filter['date_min'] );
|
||||
$_GET['end_date'] = isset( $filter['date_max'] ) ? $this->server->parse_datetime( $filter['date_max'] ) : null;
|
||||
|
||||
} else {
|
||||
|
||||
// default custom range to today
|
||||
$_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) );
|
||||
}
|
||||
} else {
|
||||
|
||||
// ensure period is valid
|
||||
if ( ! in_array( $filter['period'], array( 'week', 'month', 'last_month', 'year' ) ) ) {
|
||||
$filter['period'] = 'week';
|
||||
}
|
||||
|
||||
// TODO: change WC_Admin_Report class to use "week" instead, as it's more consistent with other periods
|
||||
// allow "week" for period instead of "7day"
|
||||
if ( 'week' === $filter['period'] ) {
|
||||
$filter['period'] = '7day';
|
||||
}
|
||||
}
|
||||
|
||||
$this->report->calculate_current_range( $filter['period'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the current user has permission to view reports
|
||||
*
|
||||
* @since 2.1
|
||||
* @see WC_API_Resource::validate_request()
|
||||
*
|
||||
* @param null $id unused
|
||||
* @param null $type unused
|
||||
* @param null $context unused
|
||||
*
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
protected function validate_request( $id = null, $type = null, $context = null ) {
|
||||
|
||||
if ( current_user_can( 'view_woocommerce_reports' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'woocommerce_api_user_cannot_read_report',
|
||||
__( 'You do not have permission to read this report', 'woocommerce' ),
|
||||
array( 'status' => 401 )
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,471 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API Resource class
|
||||
*
|
||||
* Provides shared functionality for resource-specific API classes
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_Resource {
|
||||
|
||||
/** @var WC_API_Server the API server */
|
||||
protected $server;
|
||||
|
||||
/** @var string sub-classes override this to set a resource-specific base route */
|
||||
protected $base;
|
||||
|
||||
/**
|
||||
* Setup class
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WC_API_Server $server
|
||||
*/
|
||||
public function __construct( WC_API_Server $server ) {
|
||||
|
||||
$this->server = $server;
|
||||
|
||||
// automatically register routes for sub-classes
|
||||
add_filter( 'woocommerce_api_endpoints', array( $this, 'register_routes' ) );
|
||||
|
||||
// maybe add meta to top-level resource responses
|
||||
foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) {
|
||||
add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 );
|
||||
}
|
||||
|
||||
$response_names = array(
|
||||
'order',
|
||||
'coupon',
|
||||
'customer',
|
||||
'product',
|
||||
'report',
|
||||
'customer_orders',
|
||||
'customer_downloads',
|
||||
'order_note',
|
||||
'order_refund',
|
||||
'product_reviews',
|
||||
'product_category',
|
||||
'tax',
|
||||
'tax_class',
|
||||
);
|
||||
|
||||
foreach ( $response_names as $name ) {
|
||||
|
||||
/**
|
||||
* Remove fields from responses when requests specify certain fields
|
||||
* note these are hooked at a later priority so data added via
|
||||
* filters (e.g. customer data to the order response) still has the
|
||||
* fields filtered properly
|
||||
*/
|
||||
add_filter( "woocommerce_api_{$name}_response", array( $this, 'filter_response_fields' ), 20, 3 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the request by checking:
|
||||
*
|
||||
* 1) the ID is a valid integer
|
||||
* 2) the ID returns a valid post object and matches the provided post type
|
||||
* 3) the current user has the proper permissions to read/edit/delete the post
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string|int $id the post ID
|
||||
* @param string $type the post type, either `shop_order`, `shop_coupon`, or `product`
|
||||
* @param string $context the context of the request, either `read`, `edit` or `delete`
|
||||
* @return int|WP_Error valid post ID or WP_Error if any of the checks fails
|
||||
*/
|
||||
protected function validate_request( $id, $type, $context ) {
|
||||
|
||||
if ( 'shop_order' === $type || 'shop_coupon' === $type || 'shop_webhook' === $type ) {
|
||||
$resource_name = str_replace( 'shop_', '', $type );
|
||||
} else {
|
||||
$resource_name = $type;
|
||||
}
|
||||
|
||||
$id = absint( $id );
|
||||
|
||||
// Validate ID
|
||||
if ( empty( $id ) ) {
|
||||
return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
// Only custom post types have per-post type/permission checks
|
||||
if ( 'customer' !== $type ) {
|
||||
|
||||
$post = get_post( $id );
|
||||
|
||||
if ( null === $post ) {
|
||||
return new WP_Error( "woocommerce_api_no_{$resource_name}_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), $resource_name, $id ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
// For checking permissions, product variations are the same as the product post type
|
||||
$post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type;
|
||||
|
||||
// Validate post type
|
||||
if ( $type !== $post_type ) {
|
||||
return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
// Validate permissions
|
||||
switch ( $context ) {
|
||||
|
||||
case 'read':
|
||||
if ( ! $this->is_readable( $post ) ) {
|
||||
return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'edit':
|
||||
if ( ! $this->is_editable( $post ) ) {
|
||||
return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
if ( ! $this->is_deletable( $post ) ) {
|
||||
return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add common request arguments to argument list before WP_Query is run
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $base_args required arguments for the query (e.g. `post_type`, etc)
|
||||
* @param array $request_args arguments provided in the request
|
||||
* @return array
|
||||
*/
|
||||
protected function merge_query_args( $base_args, $request_args ) {
|
||||
|
||||
$args = array();
|
||||
|
||||
// date
|
||||
if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) {
|
||||
|
||||
$args['date_query'] = array();
|
||||
|
||||
// resources created after specified date
|
||||
if ( ! empty( $request_args['created_at_min'] ) ) {
|
||||
$args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true );
|
||||
}
|
||||
|
||||
// resources created before specified date
|
||||
if ( ! empty( $request_args['created_at_max'] ) ) {
|
||||
$args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true );
|
||||
}
|
||||
|
||||
// resources updated after specified date
|
||||
if ( ! empty( $request_args['updated_at_min'] ) ) {
|
||||
$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true );
|
||||
}
|
||||
|
||||
// resources updated before specified date
|
||||
if ( ! empty( $request_args['updated_at_max'] ) ) {
|
||||
$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true );
|
||||
}
|
||||
}
|
||||
|
||||
// search
|
||||
if ( ! empty( $request_args['q'] ) ) {
|
||||
$args['s'] = $request_args['q'];
|
||||
}
|
||||
|
||||
// resources per response
|
||||
if ( ! empty( $request_args['limit'] ) ) {
|
||||
$args['posts_per_page'] = $request_args['limit'];
|
||||
}
|
||||
|
||||
// resource offset
|
||||
if ( ! empty( $request_args['offset'] ) ) {
|
||||
$args['offset'] = $request_args['offset'];
|
||||
}
|
||||
|
||||
// order (ASC or DESC, ASC by default)
|
||||
if ( ! empty( $request_args['order'] ) ) {
|
||||
$args['order'] = $request_args['order'];
|
||||
}
|
||||
|
||||
// orderby
|
||||
if ( ! empty( $request_args['orderby'] ) ) {
|
||||
$args['orderby'] = $request_args['orderby'];
|
||||
|
||||
// allow sorting by meta value
|
||||
if ( ! empty( $request_args['orderby_meta_key'] ) ) {
|
||||
$args['meta_key'] = $request_args['orderby_meta_key'];
|
||||
}
|
||||
}
|
||||
|
||||
// allow post status change
|
||||
if ( ! empty( $request_args['post_status'] ) ) {
|
||||
$args['post_status'] = $request_args['post_status'];
|
||||
unset( $request_args['post_status'] );
|
||||
}
|
||||
|
||||
// filter by a list of post id
|
||||
if ( ! empty( $request_args['in'] ) ) {
|
||||
$args['post__in'] = explode( ',', $request_args['in'] );
|
||||
unset( $request_args['in'] );
|
||||
}
|
||||
|
||||
// exclude by a list of post id
|
||||
if ( ! empty( $request_args['not_in'] ) ) {
|
||||
$args['post__not_in'] = explode( ',', $request_args['not_in'] );
|
||||
unset( $request_args['not_in'] );
|
||||
}
|
||||
|
||||
// resource page
|
||||
$args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1;
|
||||
|
||||
$args = apply_filters( 'woocommerce_api_query_args', $args, $request_args );
|
||||
|
||||
return array_merge( $base_args, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add meta to resources when requested by the client. Meta is added as a top-level
|
||||
* `<resource_name>_meta` attribute (e.g. `order_meta`) as a list of key/value pairs
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $data the resource data
|
||||
* @param object $resource the resource object (e.g WC_Order)
|
||||
* @return mixed
|
||||
*/
|
||||
public function maybe_add_meta( $data, $resource ) {
|
||||
|
||||
if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) {
|
||||
|
||||
// don't attempt to add meta more than once
|
||||
if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
// define the top-level property name for the meta
|
||||
switch ( get_class( $resource ) ) {
|
||||
|
||||
case 'WC_Order':
|
||||
$meta_name = 'order_meta';
|
||||
break;
|
||||
|
||||
case 'WC_Coupon':
|
||||
$meta_name = 'coupon_meta';
|
||||
break;
|
||||
|
||||
case 'WP_User':
|
||||
$meta_name = 'customer_meta';
|
||||
break;
|
||||
|
||||
default:
|
||||
$meta_name = 'product_meta';
|
||||
break;
|
||||
}
|
||||
|
||||
if ( is_a( $resource, 'WP_User' ) ) {
|
||||
|
||||
// customer meta
|
||||
$meta = (array) get_user_meta( $resource->ID );
|
||||
|
||||
} else {
|
||||
|
||||
// coupon/order/product meta
|
||||
$meta = (array) get_post_meta( $resource->get_id() );
|
||||
}
|
||||
|
||||
foreach ( $meta as $meta_key => $meta_value ) {
|
||||
|
||||
// don't add hidden meta by default
|
||||
if ( ! is_protected_meta( $meta_key ) ) {
|
||||
$data[ $meta_name ][ $meta_key ] = maybe_unserialize( $meta_value[0] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restrict the fields included in the response if the request specified certain only certain fields should be returned
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $data the response data
|
||||
* @param object $resource the object that provided the response data, e.g. WC_Coupon or WC_Order
|
||||
* @param array|string the requested list of fields to include in the response
|
||||
* @return array response data
|
||||
*/
|
||||
public function filter_response_fields( $data, $resource, $fields ) {
|
||||
|
||||
if ( ! is_array( $data ) || empty( $fields ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$fields = explode( ',', $fields );
|
||||
$sub_fields = array();
|
||||
|
||||
// get sub fields
|
||||
foreach ( $fields as $field ) {
|
||||
|
||||
if ( false !== strpos( $field, '.' ) ) {
|
||||
|
||||
list( $name, $value ) = explode( '.', $field );
|
||||
|
||||
$sub_fields[ $name ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// iterate through top-level fields
|
||||
foreach ( $data as $data_field => $data_value ) {
|
||||
|
||||
// if a field has sub-fields and the top-level field has sub-fields to filter
|
||||
if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) {
|
||||
|
||||
// iterate through each sub-field
|
||||
foreach ( $data_value as $sub_field => $sub_field_value ) {
|
||||
|
||||
// remove non-matching sub-fields
|
||||
if ( ! in_array( $sub_field, $sub_fields ) ) {
|
||||
unset( $data[ $data_field ][ $sub_field ] );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// remove non-matching top-level fields
|
||||
if ( ! in_array( $data_field, $fields ) ) {
|
||||
unset( $data[ $data_field ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a given resource
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int $id the resource ID
|
||||
* @param string $type the resource post type, or `customer`
|
||||
* @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`)
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
protected function delete( $id, $type, $force = false ) {
|
||||
|
||||
if ( 'shop_order' === $type || 'shop_coupon' === $type ) {
|
||||
$resource_name = str_replace( 'shop_', '', $type );
|
||||
} else {
|
||||
$resource_name = $type;
|
||||
}
|
||||
|
||||
if ( 'customer' === $type ) {
|
||||
|
||||
$result = wp_delete_user( $id );
|
||||
|
||||
if ( $result ) {
|
||||
return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) );
|
||||
} else {
|
||||
return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) );
|
||||
}
|
||||
} else {
|
||||
|
||||
// delete order/coupon/product/webhook
|
||||
$result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id );
|
||||
|
||||
if ( ! $result ) {
|
||||
return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) );
|
||||
}
|
||||
|
||||
if ( $force ) {
|
||||
return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) );
|
||||
|
||||
} else {
|
||||
|
||||
$this->server->send_status( '202' );
|
||||
|
||||
return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the given post is readable by the current user
|
||||
*
|
||||
* @since 2.1
|
||||
* @see WC_API_Resource::check_permission()
|
||||
* @param WP_Post|int $post
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_readable( $post ) {
|
||||
|
||||
return $this->check_permission( $post, 'read' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given post is editable by the current user
|
||||
*
|
||||
* @since 2.1
|
||||
* @see WC_API_Resource::check_permission()
|
||||
* @param WP_Post|int $post
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_editable( $post ) {
|
||||
|
||||
return $this->check_permission( $post, 'edit' );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given post is deletable by the current user
|
||||
*
|
||||
* @since 2.1
|
||||
* @see WC_API_Resource::check_permission()
|
||||
* @param WP_Post|int $post
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_deletable( $post ) {
|
||||
|
||||
return $this->check_permission( $post, 'delete' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the permissions for the current user given a post and context
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WP_Post|int $post
|
||||
* @param string $context the type of permission to check, either `read`, `write`, or `delete`
|
||||
* @return bool true if the current user has the permissions to perform the context on the post
|
||||
*/
|
||||
private function check_permission( $post, $context ) {
|
||||
$permission = false;
|
||||
|
||||
if ( ! is_a( $post, 'WP_Post' ) ) {
|
||||
$post = get_post( $post );
|
||||
}
|
||||
|
||||
if ( is_null( $post ) ) {
|
||||
return $permission;
|
||||
}
|
||||
|
||||
$post_type = get_post_type_object( $post->post_type );
|
||||
|
||||
if ( 'read' === $context ) {
|
||||
$permission = 'revision' !== $post->post_type && current_user_can( $post_type->cap->read_private_posts, $post->ID );
|
||||
} elseif ( 'edit' === $context ) {
|
||||
$permission = current_user_can( $post_type->cap->edit_post, $post->ID );
|
||||
} elseif ( 'delete' === $context ) {
|
||||
$permission = current_user_can( $post_type->cap->delete_post, $post->ID );
|
||||
}
|
||||
|
||||
return apply_filters( 'woocommerce_api_check_permission', $permission, $context, $post, $post_type );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,777 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API
|
||||
*
|
||||
* Handles REST API requests
|
||||
*
|
||||
* This class and related code (JSON response handler, resource classes) are based on WP-API v0.6 (https://github.com/WP-API/WP-API)
|
||||
* Many thanks to Ryan McCue and any other contributors!
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/admin.php';
|
||||
|
||||
class WC_API_Server {
|
||||
|
||||
const METHOD_GET = 1;
|
||||
const METHOD_POST = 2;
|
||||
const METHOD_PUT = 4;
|
||||
const METHOD_PATCH = 8;
|
||||
const METHOD_DELETE = 16;
|
||||
|
||||
const READABLE = 1; // GET
|
||||
const CREATABLE = 2; // POST
|
||||
const EDITABLE = 14; // POST | PUT | PATCH
|
||||
const DELETABLE = 16; // DELETE
|
||||
const ALLMETHODS = 31; // GET | POST | PUT | PATCH | DELETE
|
||||
|
||||
/**
|
||||
* Does the endpoint accept a raw request body?
|
||||
*/
|
||||
const ACCEPT_RAW_DATA = 64;
|
||||
|
||||
/** Does the endpoint accept a request body? (either JSON or XML) */
|
||||
const ACCEPT_DATA = 128;
|
||||
|
||||
/**
|
||||
* Should we hide this endpoint from the index?
|
||||
*/
|
||||
const HIDDEN_ENDPOINT = 256;
|
||||
|
||||
/**
|
||||
* Map of HTTP verbs to constants
|
||||
* @var array
|
||||
*/
|
||||
public static $method_map = array(
|
||||
'HEAD' => self::METHOD_GET,
|
||||
'GET' => self::METHOD_GET,
|
||||
'POST' => self::METHOD_POST,
|
||||
'PUT' => self::METHOD_PUT,
|
||||
'PATCH' => self::METHOD_PATCH,
|
||||
'DELETE' => self::METHOD_DELETE,
|
||||
);
|
||||
|
||||
/**
|
||||
* Requested path (relative to the API root, wp-json.php)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $path = '';
|
||||
|
||||
/**
|
||||
* Requested method (GET/HEAD/POST/PUT/PATCH/DELETE)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $method = 'HEAD';
|
||||
|
||||
/**
|
||||
* Request parameters
|
||||
*
|
||||
* This acts as an abstraction of the superglobals
|
||||
* (GET => $_GET, POST => $_POST)
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $params = array( 'GET' => array(), 'POST' => array() );
|
||||
|
||||
/**
|
||||
* Request headers
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $headers = array();
|
||||
|
||||
/**
|
||||
* Request files (matches $_FILES)
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $files = array();
|
||||
|
||||
/**
|
||||
* Request/Response handler, either JSON by default
|
||||
* or XML if requested by client
|
||||
*
|
||||
* @var WC_API_Handler
|
||||
*/
|
||||
public $handler;
|
||||
|
||||
|
||||
/**
|
||||
* Setup class and set request/response handler
|
||||
*
|
||||
* @since 2.1
|
||||
* @param $path
|
||||
*/
|
||||
public function __construct( $path ) {
|
||||
|
||||
if ( empty( $path ) ) {
|
||||
if ( isset( $_SERVER['PATH_INFO'] ) ) {
|
||||
$path = $_SERVER['PATH_INFO'];
|
||||
} else {
|
||||
$path = '/';
|
||||
}
|
||||
}
|
||||
|
||||
$this->path = $path;
|
||||
$this->method = $_SERVER['REQUEST_METHOD'];
|
||||
$this->params['GET'] = $_GET;
|
||||
$this->params['POST'] = $_POST;
|
||||
$this->headers = $this->get_headers( $_SERVER );
|
||||
$this->files = $_FILES;
|
||||
|
||||
// Compatibility for clients that can't use PUT/PATCH/DELETE
|
||||
if ( isset( $_GET['_method'] ) ) {
|
||||
$this->method = strtoupper( $_GET['_method'] );
|
||||
} elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
|
||||
$this->method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
|
||||
}
|
||||
|
||||
// load response handler
|
||||
$handler_class = apply_filters( 'woocommerce_api_default_response_handler', 'WC_API_JSON_Handler', $this->path, $this );
|
||||
|
||||
$this->handler = new $handler_class();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check authentication for the request
|
||||
*
|
||||
* @since 2.1
|
||||
* @return WP_User|WP_Error WP_User object indicates successful login, WP_Error indicates unsuccessful login
|
||||
*/
|
||||
public function check_authentication() {
|
||||
|
||||
// allow plugins to remove default authentication or add their own authentication
|
||||
$user = apply_filters( 'woocommerce_api_check_authentication', null, $this );
|
||||
|
||||
if ( is_a( $user, 'WP_User' ) ) {
|
||||
|
||||
// API requests run under the context of the authenticated user
|
||||
wp_set_current_user( $user->ID );
|
||||
|
||||
} elseif ( ! is_wp_error( $user ) ) {
|
||||
|
||||
// WP_Errors are handled in serve_request()
|
||||
$user = new WP_Error( 'woocommerce_api_authentication_error', __( 'Invalid authentication method', 'woocommerce' ), array( 'code' => 500 ) );
|
||||
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an error to an array
|
||||
*
|
||||
* This iterates over all error codes and messages to change it into a flat
|
||||
* array. This enables simpler client behaviour, as it is represented as a
|
||||
* list in JSON rather than an object/map
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WP_Error $error
|
||||
* @return array List of associative arrays with code and message keys
|
||||
*/
|
||||
protected function error_to_array( $error ) {
|
||||
$errors = array();
|
||||
foreach ( (array) $error->errors as $code => $messages ) {
|
||||
foreach ( (array) $messages as $message ) {
|
||||
$errors[] = array( 'code' => $code, 'message' => $message );
|
||||
}
|
||||
}
|
||||
|
||||
return array( 'errors' => $errors );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle serving an API request
|
||||
*
|
||||
* Matches the current server URI to a route and runs the first matching
|
||||
* callback then outputs a JSON representation of the returned value.
|
||||
*
|
||||
* @since 2.1
|
||||
* @uses WC_API_Server::dispatch()
|
||||
*/
|
||||
public function serve_request() {
|
||||
|
||||
do_action( 'woocommerce_api_server_before_serve', $this );
|
||||
|
||||
$this->header( 'Content-Type', $this->handler->get_content_type(), true );
|
||||
|
||||
// the API is enabled by default
|
||||
if ( ! apply_filters( 'woocommerce_api_enabled', true, $this ) || ( 'no' === get_option( 'woocommerce_api_enabled' ) ) ) {
|
||||
|
||||
$this->send_status( 404 );
|
||||
|
||||
echo $this->handler->generate_response( array( 'errors' => array( 'code' => 'woocommerce_api_disabled', 'message' => 'The WooCommerce API is disabled on this site' ) ) );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->check_authentication();
|
||||
|
||||
// if authorization check was successful, dispatch the request
|
||||
if ( ! is_wp_error( $result ) ) {
|
||||
$result = $this->dispatch();
|
||||
}
|
||||
|
||||
// handle any dispatch errors
|
||||
if ( is_wp_error( $result ) ) {
|
||||
$data = $result->get_error_data();
|
||||
if ( is_array( $data ) && isset( $data['status'] ) ) {
|
||||
$this->send_status( $data['status'] );
|
||||
}
|
||||
|
||||
$result = $this->error_to_array( $result );
|
||||
}
|
||||
|
||||
// This is a filter rather than an action, since this is designed to be
|
||||
// re-entrant if needed
|
||||
$served = apply_filters( 'woocommerce_api_serve_request', false, $result, $this );
|
||||
|
||||
if ( ! $served ) {
|
||||
|
||||
if ( 'HEAD' === $this->method ) {
|
||||
return;
|
||||
}
|
||||
|
||||
echo $this->handler->generate_response( $result );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the route map
|
||||
*
|
||||
* The route map is an associative array with path regexes as the keys. The
|
||||
* value is an indexed array with the callback function/method as the first
|
||||
* item, and a bitmask of HTTP methods as the second item (see the class
|
||||
* constants).
|
||||
*
|
||||
* Each route can be mapped to more than one callback by using an array of
|
||||
* the indexed arrays. This allows mapping e.g. GET requests to one callback
|
||||
* and POST requests to another.
|
||||
*
|
||||
* Note that the path regexes (array keys) must have @ escaped, as this is
|
||||
* used as the delimiter with preg_match()
|
||||
*
|
||||
* @since 2.1
|
||||
* @return array `'/path/regex' => array( $callback, $bitmask )` or `'/path/regex' => array( array( $callback, $bitmask ), ...)`
|
||||
*/
|
||||
public function get_routes() {
|
||||
|
||||
// index added by default
|
||||
$endpoints = array(
|
||||
|
||||
'/' => array( array( $this, 'get_index' ), self::READABLE ),
|
||||
);
|
||||
|
||||
$endpoints = apply_filters( 'woocommerce_api_endpoints', $endpoints );
|
||||
|
||||
// Normalise the endpoints
|
||||
foreach ( $endpoints as $route => &$handlers ) {
|
||||
if ( count( $handlers ) <= 2 && isset( $handlers[1] ) && ! is_array( $handlers[1] ) ) {
|
||||
$handlers = array( $handlers );
|
||||
}
|
||||
}
|
||||
|
||||
return $endpoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match the request to a callback and call it
|
||||
*
|
||||
* @since 2.1
|
||||
* @return mixed The value returned by the callback, or a WP_Error instance
|
||||
*/
|
||||
public function dispatch() {
|
||||
|
||||
switch ( $this->method ) {
|
||||
|
||||
case 'HEAD' :
|
||||
case 'GET' :
|
||||
$method = self::METHOD_GET;
|
||||
break;
|
||||
|
||||
case 'POST' :
|
||||
$method = self::METHOD_POST;
|
||||
break;
|
||||
|
||||
case 'PUT' :
|
||||
$method = self::METHOD_PUT;
|
||||
break;
|
||||
|
||||
case 'PATCH' :
|
||||
$method = self::METHOD_PATCH;
|
||||
break;
|
||||
|
||||
case 'DELETE' :
|
||||
$method = self::METHOD_DELETE;
|
||||
break;
|
||||
|
||||
default :
|
||||
return new WP_Error( 'woocommerce_api_unsupported_method', __( 'Unsupported request method', 'woocommerce' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
foreach ( $this->get_routes() as $route => $handlers ) {
|
||||
foreach ( $handlers as $handler ) {
|
||||
$callback = $handler[0];
|
||||
$supported = isset( $handler[1] ) ? $handler[1] : self::METHOD_GET;
|
||||
|
||||
if ( ! ( $supported & $method ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$match = preg_match( '@^' . $route . '$@i', urldecode( $this->path ), $args );
|
||||
|
||||
if ( ! $match ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! is_callable( $callback ) ) {
|
||||
return new WP_Error( 'woocommerce_api_invalid_handler', __( 'The handler for the route is invalid', 'woocommerce' ), array( 'status' => 500 ) );
|
||||
}
|
||||
|
||||
$args = array_merge( $args, $this->params['GET'] );
|
||||
if ( $method & self::METHOD_POST ) {
|
||||
$args = array_merge( $args, $this->params['POST'] );
|
||||
}
|
||||
if ( $supported & self::ACCEPT_DATA ) {
|
||||
$data = $this->handler->parse_body( $this->get_raw_data() );
|
||||
$args = array_merge( $args, array( 'data' => $data ) );
|
||||
} elseif ( $supported & self::ACCEPT_RAW_DATA ) {
|
||||
$data = $this->get_raw_data();
|
||||
$args = array_merge( $args, array( 'data' => $data ) );
|
||||
}
|
||||
|
||||
$args['_method'] = $method;
|
||||
$args['_route'] = $route;
|
||||
$args['_path'] = $this->path;
|
||||
$args['_headers'] = $this->headers;
|
||||
$args['_files'] = $this->files;
|
||||
|
||||
$args = apply_filters( 'woocommerce_api_dispatch_args', $args, $callback );
|
||||
|
||||
// Allow plugins to halt the request via this filter
|
||||
if ( is_wp_error( $args ) ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
$params = $this->sort_callback_params( $callback, $args );
|
||||
if ( is_wp_error( $params ) ) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
return call_user_func_array( $callback, $params );
|
||||
}
|
||||
}
|
||||
|
||||
return new WP_Error( 'woocommerce_api_no_route', __( 'No route was found matching the URL and request method', 'woocommerce' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* urldecode deep.
|
||||
*
|
||||
* @since 2.2
|
||||
* @param string|array $value Data to decode with urldecode.
|
||||
*
|
||||
* @return string|array Decoded data.
|
||||
*/
|
||||
protected function urldecode_deep( $value ) {
|
||||
if ( is_array( $value ) ) {
|
||||
return array_map( array( $this, 'urldecode_deep' ), $value );
|
||||
} else {
|
||||
return urldecode( $value );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort parameters by order specified in method declaration
|
||||
*
|
||||
* Takes a callback and a list of available params, then filters and sorts
|
||||
* by the parameters the method actually needs, using the Reflection API
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @param callable|array $callback the endpoint callback
|
||||
* @param array $provided the provided request parameters
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
protected function sort_callback_params( $callback, $provided ) {
|
||||
if ( is_array( $callback ) ) {
|
||||
$ref_func = new ReflectionMethod( $callback[0], $callback[1] );
|
||||
} else {
|
||||
$ref_func = new ReflectionFunction( $callback );
|
||||
}
|
||||
|
||||
$wanted = $ref_func->getParameters();
|
||||
$ordered_parameters = array();
|
||||
|
||||
foreach ( $wanted as $param ) {
|
||||
if ( isset( $provided[ $param->getName() ] ) ) {
|
||||
// We have this parameters in the list to choose from
|
||||
if ( 'data' == $param->getName() ) {
|
||||
$ordered_parameters[] = $provided[ $param->getName() ];
|
||||
continue;
|
||||
}
|
||||
|
||||
$ordered_parameters[] = $this->urldecode_deep( $provided[ $param->getName() ] );
|
||||
} elseif ( $param->isDefaultValueAvailable() ) {
|
||||
// We don't have this parameter, but it's optional
|
||||
$ordered_parameters[] = $param->getDefaultValue();
|
||||
} else {
|
||||
// We don't have this parameter and it wasn't optional, abort!
|
||||
return new WP_Error( 'woocommerce_api_missing_callback_param', sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param->getName() ), array( 'status' => 400 ) );
|
||||
}
|
||||
}
|
||||
|
||||
return $ordered_parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the site index.
|
||||
*
|
||||
* This endpoint describes the capabilities of the site.
|
||||
*
|
||||
* @since 2.3
|
||||
* @return array Index entity
|
||||
*/
|
||||
public function get_index() {
|
||||
|
||||
// General site data
|
||||
$available = array(
|
||||
'store' => array(
|
||||
'name' => get_option( 'blogname' ),
|
||||
'description' => get_option( 'blogdescription' ),
|
||||
'URL' => get_option( 'siteurl' ),
|
||||
'wc_version' => WC()->version,
|
||||
'version' => WC_API::VERSION,
|
||||
'routes' => array(),
|
||||
'meta' => array(
|
||||
'timezone' => wc_timezone_string(),
|
||||
'currency' => get_woocommerce_currency(),
|
||||
'currency_format' => get_woocommerce_currency_symbol(),
|
||||
'currency_position' => get_option( 'woocommerce_currency_pos' ),
|
||||
'thousand_separator' => get_option( 'woocommerce_price_thousand_sep' ),
|
||||
'decimal_separator' => get_option( 'woocommerce_price_decimal_sep' ),
|
||||
'price_num_decimals' => wc_get_price_decimals(),
|
||||
'tax_included' => wc_prices_include_tax(),
|
||||
'weight_unit' => get_option( 'woocommerce_weight_unit' ),
|
||||
'dimension_unit' => get_option( 'woocommerce_dimension_unit' ),
|
||||
'ssl_enabled' => ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) || wc_site_is_https() ),
|
||||
'permalinks_enabled' => ( '' !== get_option( 'permalink_structure' ) ),
|
||||
'generate_password' => ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) ),
|
||||
'links' => array(
|
||||
'help' => 'https://woocommerce.github.io/woocommerce-rest-api-docs/',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Find the available routes
|
||||
foreach ( $this->get_routes() as $route => $callbacks ) {
|
||||
$data = array();
|
||||
|
||||
$route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route );
|
||||
|
||||
foreach ( self::$method_map as $name => $bitmask ) {
|
||||
foreach ( $callbacks as $callback ) {
|
||||
// Skip to the next route if any callback is hidden
|
||||
if ( $callback[1] & self::HIDDEN_ENDPOINT ) {
|
||||
continue 3;
|
||||
}
|
||||
|
||||
if ( $callback[1] & $bitmask ) {
|
||||
$data['supports'][] = $name;
|
||||
}
|
||||
|
||||
if ( $callback[1] & self::ACCEPT_DATA ) {
|
||||
$data['accepts_data'] = true;
|
||||
}
|
||||
|
||||
// For non-variable routes, generate links
|
||||
if ( strpos( $route, '<' ) === false ) {
|
||||
$data['meta'] = array(
|
||||
'self' => get_woocommerce_api_url( $route ),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$available['store']['routes'][ $route ] = apply_filters( 'woocommerce_api_endpoints_description', $data );
|
||||
}
|
||||
|
||||
return apply_filters( 'woocommerce_api_index', $available );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a HTTP status code
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int $code HTTP status
|
||||
*/
|
||||
public function send_status( $code ) {
|
||||
status_header( $code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a HTTP header
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $key Header key
|
||||
* @param string $value Header value
|
||||
* @param boolean $replace Should we replace the existing header?
|
||||
*/
|
||||
public function header( $key, $value, $replace = true ) {
|
||||
header( sprintf( '%s: %s', $key, $value ), $replace );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a Link header
|
||||
*
|
||||
* @internal The $rel parameter is first, as this looks nicer when sending multiple
|
||||
*
|
||||
* @link http://tools.ietf.org/html/rfc5988
|
||||
* @link http://www.iana.org/assignments/link-relations/link-relations.xml
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $rel Link relation. Either a registered type, or an absolute URL
|
||||
* @param string $link Target IRI for the link
|
||||
* @param array $other Other parameters to send, as an associative array
|
||||
*/
|
||||
public function link_header( $rel, $link, $other = array() ) {
|
||||
|
||||
$header = sprintf( '<%s>; rel="%s"', $link, esc_attr( $rel ) );
|
||||
|
||||
foreach ( $other as $key => $value ) {
|
||||
|
||||
if ( 'title' == $key ) {
|
||||
|
||||
$value = '"' . $value . '"';
|
||||
}
|
||||
|
||||
$header .= '; ' . $key . '=' . $value;
|
||||
}
|
||||
|
||||
$this->header( 'Link', $header, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send pagination headers for resources
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WP_Query|WP_User_Query|stdClass $query
|
||||
*/
|
||||
public function add_pagination_headers( $query ) {
|
||||
|
||||
// WP_User_Query
|
||||
if ( is_a( $query, 'WP_User_Query' ) ) {
|
||||
|
||||
$single = count( $query->get_results() ) == 1;
|
||||
$total = $query->get_total();
|
||||
|
||||
if ( $query->get( 'number' ) > 0 ) {
|
||||
$page = ( $query->get( 'offset' ) / $query->get( 'number' ) ) + 1;
|
||||
$total_pages = ceil( $total / $query->get( 'number' ) );
|
||||
} else {
|
||||
$page = 1;
|
||||
$total_pages = 1;
|
||||
}
|
||||
} elseif ( is_a( $query, 'stdClass' ) ) {
|
||||
$page = $query->page;
|
||||
$single = $query->is_single;
|
||||
$total = $query->total;
|
||||
$total_pages = $query->total_pages;
|
||||
|
||||
// WP_Query
|
||||
} else {
|
||||
|
||||
$page = $query->get( 'paged' );
|
||||
$single = $query->is_single();
|
||||
$total = $query->found_posts;
|
||||
$total_pages = $query->max_num_pages;
|
||||
}
|
||||
|
||||
if ( ! $page ) {
|
||||
$page = 1;
|
||||
}
|
||||
|
||||
$next_page = absint( $page ) + 1;
|
||||
|
||||
if ( ! $single ) {
|
||||
|
||||
// first/prev
|
||||
if ( $page > 1 ) {
|
||||
$this->link_header( 'first', $this->get_paginated_url( 1 ) );
|
||||
$this->link_header( 'prev', $this->get_paginated_url( $page -1 ) );
|
||||
}
|
||||
|
||||
// next
|
||||
if ( $next_page <= $total_pages ) {
|
||||
$this->link_header( 'next', $this->get_paginated_url( $next_page ) );
|
||||
}
|
||||
|
||||
// last
|
||||
if ( $page != $total_pages ) {
|
||||
$this->link_header( 'last', $this->get_paginated_url( $total_pages ) );
|
||||
}
|
||||
}
|
||||
|
||||
$this->header( 'X-WC-Total', $total );
|
||||
$this->header( 'X-WC-TotalPages', $total_pages );
|
||||
|
||||
do_action( 'woocommerce_api_pagination_headers', $this, $query );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request URL with the page query parameter set to the specified page
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int $page
|
||||
* @return string
|
||||
*/
|
||||
private function get_paginated_url( $page ) {
|
||||
|
||||
// remove existing page query param
|
||||
$request = remove_query_arg( 'page' );
|
||||
|
||||
// add provided page query param
|
||||
$request = urldecode( add_query_arg( 'page', $page, $request ) );
|
||||
|
||||
// get the home host
|
||||
$host = parse_url( get_home_url(), PHP_URL_HOST );
|
||||
|
||||
return set_url_scheme( "http://{$host}{$request}" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the raw request entity (body)
|
||||
*
|
||||
* @since 2.1
|
||||
* @return string
|
||||
*/
|
||||
public function get_raw_data() {
|
||||
// @codingStandardsIgnoreStart
|
||||
// $HTTP_RAW_POST_DATA is deprecated on PHP 5.6.
|
||||
if ( function_exists( 'phpversion' ) && version_compare( phpversion(), '5.6', '>=' ) ) {
|
||||
return file_get_contents( 'php://input' );
|
||||
}
|
||||
|
||||
global $HTTP_RAW_POST_DATA;
|
||||
|
||||
// A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default,
|
||||
// but we can do it ourself.
|
||||
if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
|
||||
$HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
|
||||
}
|
||||
|
||||
return $HTTP_RAW_POST_DATA;
|
||||
// @codingStandardsIgnoreEnd
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an RFC3339 datetime into a MySQl datetime
|
||||
*
|
||||
* Invalid dates default to unix epoch
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $datetime RFC3339 datetime
|
||||
* @return string MySQl datetime (YYYY-MM-DD HH:MM:SS)
|
||||
*/
|
||||
public function parse_datetime( $datetime ) {
|
||||
|
||||
// Strip millisecond precision (a full stop followed by one or more digits)
|
||||
if ( strpos( $datetime, '.' ) !== false ) {
|
||||
$datetime = preg_replace( '/\.\d+/', '', $datetime );
|
||||
}
|
||||
|
||||
// default timezone to UTC
|
||||
$datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime );
|
||||
|
||||
try {
|
||||
|
||||
$datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) );
|
||||
|
||||
} catch ( Exception $e ) {
|
||||
|
||||
$datetime = new DateTime( '@0' );
|
||||
|
||||
}
|
||||
|
||||
return $datetime->format( 'Y-m-d H:i:s' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a unix timestamp or MySQL datetime into an RFC3339 datetime
|
||||
*
|
||||
* @since 2.1
|
||||
* @param int|string $timestamp unix timestamp or MySQL datetime
|
||||
* @param bool $convert_to_utc
|
||||
* @param bool $convert_to_gmt Use GMT timezone.
|
||||
* @return string RFC3339 datetime
|
||||
*/
|
||||
public function format_datetime( $timestamp, $convert_to_utc = false, $convert_to_gmt = false ) {
|
||||
if ( $convert_to_gmt ) {
|
||||
if ( is_numeric( $timestamp ) ) {
|
||||
$timestamp = date( 'Y-m-d H:i:s', $timestamp );
|
||||
}
|
||||
|
||||
$timestamp = get_gmt_from_date( $timestamp );
|
||||
}
|
||||
|
||||
if ( $convert_to_utc ) {
|
||||
$timezone = new DateTimeZone( wc_timezone_string() );
|
||||
} else {
|
||||
$timezone = new DateTimeZone( 'UTC' );
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
if ( is_numeric( $timestamp ) ) {
|
||||
$date = new DateTime( "@{$timestamp}" );
|
||||
} else {
|
||||
$date = new DateTime( $timestamp, $timezone );
|
||||
}
|
||||
|
||||
// convert to UTC by adjusting the time based on the offset of the site's timezone
|
||||
if ( $convert_to_utc ) {
|
||||
$date->modify( -1 * $date->getOffset() . ' seconds' );
|
||||
}
|
||||
} catch ( Exception $e ) {
|
||||
|
||||
$date = new DateTime( '@0' );
|
||||
}
|
||||
|
||||
return $date->format( 'Y-m-d\TH:i:s\Z' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract headers from a PHP-style $_SERVER array
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $server Associative array similar to $_SERVER
|
||||
* @return array Headers extracted from the input
|
||||
*/
|
||||
public function get_headers( $server ) {
|
||||
$headers = array();
|
||||
// CONTENT_* headers are not prefixed with HTTP_
|
||||
$additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true );
|
||||
|
||||
foreach ( $server as $key => $value ) {
|
||||
if ( strpos( $key, 'HTTP_' ) === 0 ) {
|
||||
$headers[ substr( $key, 5 ) ] = $value;
|
||||
} elseif ( isset( $additional[ $key ] ) ) {
|
||||
$headers[ $key ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,691 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API Taxes Class
|
||||
*
|
||||
* Handles requests to the /taxes endpoint
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.5.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_Taxes extends WC_API_Resource {
|
||||
|
||||
/** @var string $base the route base */
|
||||
protected $base = '/taxes';
|
||||
|
||||
/**
|
||||
* Register the routes for this class
|
||||
*
|
||||
* GET /taxes
|
||||
* GET /taxes/count
|
||||
* GET /taxes/<id>
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $routes
|
||||
* @return array
|
||||
*/
|
||||
public function register_routes( $routes ) {
|
||||
|
||||
# GET/POST /taxes
|
||||
$routes[ $this->base ] = array(
|
||||
array( array( $this, 'get_taxes' ), WC_API_Server::READABLE ),
|
||||
array( array( $this, 'create_tax' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
|
||||
);
|
||||
|
||||
# GET /taxes/count
|
||||
$routes[ $this->base . '/count' ] = array(
|
||||
array( array( $this, 'get_taxes_count' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# GET/PUT/DELETE /taxes/<id>
|
||||
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
|
||||
array( array( $this, 'get_tax' ), WC_API_Server::READABLE ),
|
||||
array( array( $this, 'edit_tax' ), WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ),
|
||||
array( array( $this, 'delete_tax' ), WC_API_SERVER::DELETABLE ),
|
||||
);
|
||||
|
||||
# GET/POST /taxes/classes
|
||||
$routes[ $this->base . '/classes' ] = array(
|
||||
array( array( $this, 'get_tax_classes' ), WC_API_Server::READABLE ),
|
||||
array( array( $this, 'create_tax_class' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
|
||||
);
|
||||
|
||||
# GET /taxes/classes/count
|
||||
$routes[ $this->base . '/classes/count' ] = array(
|
||||
array( array( $this, 'get_tax_classes_count' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# GET /taxes/classes/<slug>
|
||||
$routes[ $this->base . '/classes/(?P<slug>\w[\w\s\-]*)' ] = array(
|
||||
array( array( $this, 'delete_tax_class' ), WC_API_SERVER::DELETABLE ),
|
||||
);
|
||||
|
||||
# POST|PUT /taxes/bulk
|
||||
$routes[ $this->base . '/bulk' ] = array(
|
||||
array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
|
||||
);
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all taxes
|
||||
*
|
||||
* @since 2.5.0
|
||||
*
|
||||
* @param string $fields
|
||||
* @param array $filter
|
||||
* @param string $class
|
||||
* @param int $page
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_taxes( $fields = null, $filter = array(), $class = null, $page = 1 ) {
|
||||
if ( ! empty( $class ) ) {
|
||||
$filter['tax_rate_class'] = $class;
|
||||
}
|
||||
|
||||
$filter['page'] = $page;
|
||||
|
||||
$query = $this->query_tax_rates( $filter );
|
||||
|
||||
$taxes = array();
|
||||
|
||||
foreach ( $query['results'] as $tax ) {
|
||||
$taxes[] = current( $this->get_tax( $tax->tax_rate_id, $fields ) );
|
||||
}
|
||||
|
||||
// Set pagination headers
|
||||
$this->server->add_pagination_headers( $query['headers'] );
|
||||
|
||||
return array( 'taxes' => $taxes );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tax for the given ID
|
||||
*
|
||||
* @since 2.5.0
|
||||
*
|
||||
* @param int $id The tax ID
|
||||
* @param string $fields fields to include in response
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_tax( $id, $fields = null ) {
|
||||
global $wpdb;
|
||||
|
||||
try {
|
||||
$id = absint( $id );
|
||||
|
||||
// Permissions check
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax', __( 'You do not have permission to read tax rate', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
// Get tax rate details
|
||||
$tax = WC_Tax::_get_tax_rate( $id );
|
||||
|
||||
if ( is_wp_error( $tax ) || empty( $tax ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_tax_id', __( 'A tax rate with the provided ID could not be found', 'woocommerce' ), 404 );
|
||||
}
|
||||
|
||||
$tax_data = array(
|
||||
'id' => (int) $tax['tax_rate_id'],
|
||||
'country' => $tax['tax_rate_country'],
|
||||
'state' => $tax['tax_rate_state'],
|
||||
'postcode' => '',
|
||||
'city' => '',
|
||||
'rate' => $tax['tax_rate'],
|
||||
'name' => $tax['tax_rate_name'],
|
||||
'priority' => (int) $tax['tax_rate_priority'],
|
||||
'compound' => (bool) $tax['tax_rate_compound'],
|
||||
'shipping' => (bool) $tax['tax_rate_shipping'],
|
||||
'order' => (int) $tax['tax_rate_order'],
|
||||
'class' => $tax['tax_rate_class'] ? $tax['tax_rate_class'] : 'standard',
|
||||
);
|
||||
|
||||
// Get locales from a tax rate
|
||||
$locales = $wpdb->get_results( $wpdb->prepare( "
|
||||
SELECT location_code, location_type
|
||||
FROM {$wpdb->prefix}woocommerce_tax_rate_locations
|
||||
WHERE tax_rate_id = %d
|
||||
", $id ) );
|
||||
|
||||
if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) {
|
||||
foreach ( $locales as $locale ) {
|
||||
$tax_data[ $locale->location_type ] = $locale->location_code;
|
||||
}
|
||||
}
|
||||
|
||||
return array( 'tax' => apply_filters( 'woocommerce_api_tax_response', $tax_data, $tax, $fields, $this ) );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tax
|
||||
*
|
||||
* @since 2.5.0
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function create_tax( $data ) {
|
||||
try {
|
||||
if ( ! isset( $data['tax'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_tax_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'tax' ), 400 );
|
||||
}
|
||||
|
||||
// Check permissions
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_tax', __( 'You do not have permission to create tax rates', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$data = apply_filters( 'woocommerce_api_create_tax_data', $data['tax'], $this );
|
||||
|
||||
$tax_data = array(
|
||||
'tax_rate_country' => '',
|
||||
'tax_rate_state' => '',
|
||||
'tax_rate' => '',
|
||||
'tax_rate_name' => '',
|
||||
'tax_rate_priority' => 1,
|
||||
'tax_rate_compound' => 0,
|
||||
'tax_rate_shipping' => 1,
|
||||
'tax_rate_order' => 0,
|
||||
'tax_rate_class' => '',
|
||||
);
|
||||
|
||||
foreach ( $tax_data as $key => $value ) {
|
||||
$new_key = str_replace( 'tax_rate_', '', $key );
|
||||
$new_key = 'tax_rate' === $new_key ? 'rate' : $new_key;
|
||||
|
||||
if ( isset( $data[ $new_key ] ) ) {
|
||||
if ( in_array( $new_key, array( 'compound', 'shipping' ) ) ) {
|
||||
$tax_data[ $key ] = $data[ $new_key ] ? 1 : 0;
|
||||
} else {
|
||||
$tax_data[ $key ] = $data[ $new_key ];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create tax rate
|
||||
$id = WC_Tax::_insert_tax_rate( $tax_data );
|
||||
|
||||
// Add locales
|
||||
if ( ! empty( $data['postcode'] ) ) {
|
||||
WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $data['postcode'] ) );
|
||||
}
|
||||
|
||||
if ( ! empty( $data['city'] ) ) {
|
||||
WC_Tax::_update_tax_rate_cities( $id, wc_clean( $data['city'] ) );
|
||||
}
|
||||
|
||||
do_action( 'woocommerce_api_create_tax', $id, $data );
|
||||
|
||||
$this->server->send_status( 201 );
|
||||
|
||||
return $this->get_tax( $id );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a tax
|
||||
*
|
||||
* @since 2.5.0
|
||||
*
|
||||
* @param int $id The tax ID
|
||||
* @param array $data
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function edit_tax( $id, $data ) {
|
||||
try {
|
||||
if ( ! isset( $data['tax'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_tax_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'tax' ), 400 );
|
||||
}
|
||||
|
||||
// Check permissions
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_tax', __( 'You do not have permission to edit tax rates', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$data = $data['tax'];
|
||||
|
||||
// Get current tax rate data
|
||||
$tax = $this->get_tax( $id );
|
||||
|
||||
if ( is_wp_error( $tax ) ) {
|
||||
$error_data = $tax->get_error_data();
|
||||
throw new WC_API_Exception( $tax->get_error_code(), $tax->get_error_message(), $error_data['status'] );
|
||||
}
|
||||
|
||||
$current_data = $tax['tax'];
|
||||
$data = apply_filters( 'woocommerce_api_edit_tax_data', $data, $this );
|
||||
$tax_data = array();
|
||||
$default_fields = array(
|
||||
'tax_rate_country',
|
||||
'tax_rate_state',
|
||||
'tax_rate',
|
||||
'tax_rate_name',
|
||||
'tax_rate_priority',
|
||||
'tax_rate_compound',
|
||||
'tax_rate_shipping',
|
||||
'tax_rate_order',
|
||||
'tax_rate_class',
|
||||
);
|
||||
|
||||
foreach ( $data as $key => $value ) {
|
||||
$new_key = 'rate' === $key ? 'tax_rate' : 'tax_rate_' . $key;
|
||||
|
||||
// Check if the key is valid
|
||||
if ( ! in_array( $new_key, $default_fields ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Test new data against current data
|
||||
if ( $value === $current_data[ $key ] ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fix compound and shipping values
|
||||
if ( in_array( $key, array( 'compound', 'shipping' ) ) ) {
|
||||
$value = $value ? 1 : 0;
|
||||
}
|
||||
|
||||
$tax_data[ $new_key ] = $value;
|
||||
}
|
||||
|
||||
// Update tax rate
|
||||
WC_Tax::_update_tax_rate( $id, $tax_data );
|
||||
|
||||
// Update locales
|
||||
if ( ! empty( $data['postcode'] ) && $current_data['postcode'] != $data['postcode'] ) {
|
||||
WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $data['postcode'] ) );
|
||||
}
|
||||
|
||||
if ( ! empty( $data['city'] ) && $current_data['city'] != $data['city'] ) {
|
||||
WC_Tax::_update_tax_rate_cities( $id, wc_clean( $data['city'] ) );
|
||||
}
|
||||
|
||||
do_action( 'woocommerce_api_edit_tax_rate', $id, $data );
|
||||
|
||||
return $this->get_tax( $id );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a tax
|
||||
*
|
||||
* @since 2.5.0
|
||||
*
|
||||
* @param int $id The tax ID
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function delete_tax( $id ) {
|
||||
global $wpdb;
|
||||
|
||||
try {
|
||||
// Check permissions
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_tax', __( 'You do not have permission to delete tax rates', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$id = absint( $id );
|
||||
|
||||
WC_Tax::_delete_tax_rate( $id );
|
||||
|
||||
if ( 0 === $wpdb->rows_affected ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_cannot_delete_tax', __( 'Could not delete the tax rate', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'tax' ) );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of taxes
|
||||
*
|
||||
* @since 2.5.0
|
||||
*
|
||||
* @param string $class
|
||||
* @param array $filter
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_taxes_count( $class = null, $filter = array() ) {
|
||||
try {
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_taxes_count', __( 'You do not have permission to read the taxes count', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
if ( ! empty( $class ) ) {
|
||||
$filter['tax_rate_class'] = $class;
|
||||
}
|
||||
|
||||
$query = $this->query_tax_rates( $filter, true );
|
||||
|
||||
return array( 'count' => (int) $query['headers']->total );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get tax rates objects
|
||||
*
|
||||
* @since 2.5.0
|
||||
*
|
||||
* @param array $args
|
||||
* @param bool $count_only
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function query_tax_rates( $args, $count_only = false ) {
|
||||
global $wpdb;
|
||||
|
||||
$results = '';
|
||||
|
||||
// Set args
|
||||
$args = $this->merge_query_args( $args, array() );
|
||||
|
||||
$query = "
|
||||
SELECT tax_rate_id
|
||||
FROM {$wpdb->prefix}woocommerce_tax_rates
|
||||
WHERE 1 = 1
|
||||
";
|
||||
|
||||
// Filter by tax class
|
||||
if ( ! empty( $args['tax_rate_class'] ) ) {
|
||||
$tax_rate_class = 'standard' !== $args['tax_rate_class'] ? sanitize_title( $args['tax_rate_class'] ) : '';
|
||||
$query .= " AND tax_rate_class = '$tax_rate_class'";
|
||||
}
|
||||
|
||||
// Order tax rates
|
||||
$order_by = ' ORDER BY tax_rate_order';
|
||||
|
||||
// Pagination
|
||||
$per_page = isset( $args['posts_per_page'] ) ? $args['posts_per_page'] : get_option( 'posts_per_page' );
|
||||
$offset = 1 < $args['paged'] ? ( $args['paged'] - 1 ) * $per_page : 0;
|
||||
$pagination = sprintf( ' LIMIT %d, %d', $offset, $per_page );
|
||||
|
||||
if ( ! $count_only ) {
|
||||
$results = $wpdb->get_results( $query . $order_by . $pagination );
|
||||
}
|
||||
|
||||
$wpdb->get_results( $query );
|
||||
$headers = new stdClass;
|
||||
$headers->page = $args['paged'];
|
||||
$headers->total = (int) $wpdb->num_rows;
|
||||
$headers->is_single = $per_page > $headers->total;
|
||||
$headers->total_pages = ceil( $headers->total / $per_page );
|
||||
|
||||
return array(
|
||||
'results' => $results,
|
||||
'headers' => $headers,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk update or insert taxes
|
||||
* Accepts an array with taxes in the formats supported by
|
||||
* WC_API_Taxes->create_tax() and WC_API_Taxes->edit_tax()
|
||||
*
|
||||
* @since 2.5.0
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function bulk( $data ) {
|
||||
try {
|
||||
if ( ! isset( $data['taxes'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_taxes_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'taxes' ), 400 );
|
||||
}
|
||||
|
||||
$data = $data['taxes'];
|
||||
$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'taxes' );
|
||||
|
||||
// Limit bulk operation
|
||||
if ( count( $data ) > $limit ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_taxes_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
|
||||
}
|
||||
|
||||
$taxes = array();
|
||||
|
||||
foreach ( $data as $_tax ) {
|
||||
$tax_id = 0;
|
||||
|
||||
// Try to get the tax rate ID
|
||||
if ( isset( $_tax['id'] ) ) {
|
||||
$tax_id = intval( $_tax['id'] );
|
||||
}
|
||||
|
||||
if ( $tax_id ) {
|
||||
|
||||
// Tax rate exists / edit tax rate
|
||||
$edit = $this->edit_tax( $tax_id, array( 'tax' => $_tax ) );
|
||||
|
||||
if ( is_wp_error( $edit ) ) {
|
||||
$taxes[] = array(
|
||||
'id' => $tax_id,
|
||||
'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
|
||||
);
|
||||
} else {
|
||||
$taxes[] = $edit['tax'];
|
||||
}
|
||||
} else {
|
||||
|
||||
// Tax rate don't exists / create tax rate
|
||||
$new = $this->create_tax( array( 'tax' => $_tax ) );
|
||||
|
||||
if ( is_wp_error( $new ) ) {
|
||||
$taxes[] = array(
|
||||
'id' => $tax_id,
|
||||
'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
|
||||
);
|
||||
} else {
|
||||
$taxes[] = $new['tax'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array( 'taxes' => apply_filters( 'woocommerce_api_taxes_bulk_response', $taxes, $this ) );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tax classes
|
||||
*
|
||||
* @since 2.5.0
|
||||
*
|
||||
* @param string $fields
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_tax_classes( $fields = null ) {
|
||||
try {
|
||||
// Permissions check
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax_classes', __( 'You do not have permission to read tax classes', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$tax_classes = array();
|
||||
|
||||
// Add standard class
|
||||
$tax_classes[] = array(
|
||||
'slug' => 'standard',
|
||||
'name' => __( 'Standard rate', 'woocommerce' ),
|
||||
);
|
||||
|
||||
$classes = WC_Tax::get_tax_classes();
|
||||
|
||||
foreach ( $classes as $class ) {
|
||||
$tax_classes[] = apply_filters( 'woocommerce_api_tax_class_response', array(
|
||||
'slug' => sanitize_title( $class ),
|
||||
'name' => $class,
|
||||
), $class, $fields, $this );
|
||||
}
|
||||
|
||||
return array( 'tax_classes' => apply_filters( 'woocommerce_api_tax_classes_response', $tax_classes, $classes, $fields, $this ) );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tax class.
|
||||
*
|
||||
* @since 2.5.0
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function create_tax_class( $data ) {
|
||||
try {
|
||||
if ( ! isset( $data['tax_class'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_tax_class_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'tax_class' ), 400 );
|
||||
}
|
||||
|
||||
// Check permissions
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_tax_class', __( 'You do not have permission to create tax classes', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$data = $data['tax_class'];
|
||||
|
||||
if ( empty( $data['name'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_tax_class_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 );
|
||||
}
|
||||
|
||||
$name = sanitize_text_field( $data['name'] );
|
||||
$slug = sanitize_title( $name );
|
||||
$classes = WC_Tax::get_tax_classes();
|
||||
$exists = false;
|
||||
|
||||
// Check if class exists.
|
||||
foreach ( $classes as $key => $class ) {
|
||||
if ( sanitize_title( $class ) === $slug ) {
|
||||
$exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Return error if tax class already exists.
|
||||
if ( $exists ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_cannot_create_tax_class', __( 'Tax class already exists', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
// Add the new class.
|
||||
$classes[] = $name;
|
||||
|
||||
update_option( 'woocommerce_tax_classes', implode( "\n", $classes ) );
|
||||
|
||||
do_action( 'woocommerce_api_create_tax_class', $slug, $data );
|
||||
|
||||
$this->server->send_status( 201 );
|
||||
|
||||
return array(
|
||||
'tax_class' => array(
|
||||
'slug' => $slug,
|
||||
'name' => $name,
|
||||
),
|
||||
);
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a tax class
|
||||
*
|
||||
* @since 2.5.0
|
||||
*
|
||||
* @param int $slug The tax class slug
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function delete_tax_class( $slug ) {
|
||||
global $wpdb;
|
||||
|
||||
try {
|
||||
// Check permissions
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_tax_class', __( 'You do not have permission to delete tax classes', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$slug = sanitize_title( $slug );
|
||||
$classes = WC_Tax::get_tax_classes();
|
||||
$deleted = false;
|
||||
|
||||
foreach ( $classes as $key => $class ) {
|
||||
if ( sanitize_title( $class ) === $slug ) {
|
||||
unset( $classes[ $key ] );
|
||||
$deleted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $deleted ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_cannot_delete_tax_class', __( 'Could not delete the tax class', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
update_option( 'woocommerce_tax_classes', implode( "\n", $classes ) );
|
||||
|
||||
// Delete tax rate locations locations from the selected class.
|
||||
$wpdb->query( $wpdb->prepare( "
|
||||
DELETE locations.*
|
||||
FROM {$wpdb->prefix}woocommerce_tax_rate_locations AS locations
|
||||
INNER JOIN
|
||||
{$wpdb->prefix}woocommerce_tax_rates AS rates
|
||||
ON rates.tax_rate_id = locations.tax_rate_id
|
||||
WHERE rates.tax_rate_class = '%s'
|
||||
", $slug ) );
|
||||
|
||||
// Delete tax rates in the selected class.
|
||||
$wpdb->delete( $wpdb->prefix . 'woocommerce_tax_rates', array( 'tax_rate_class' => $slug ), array( '%s' ) );
|
||||
|
||||
return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'tax_class' ) );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of tax classes
|
||||
*
|
||||
* @since 2.5.0
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_tax_classes_count() {
|
||||
try {
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax_classes_count', __( 'You do not have permission to read the tax classes count', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$total = count( WC_Tax::get_tax_classes() ) + 1; // +1 for Standard Rate
|
||||
|
||||
return array( 'count' => $total );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,509 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API Webhooks class
|
||||
*
|
||||
* Handles requests to the /webhooks endpoint
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.2
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_API_Webhooks extends WC_API_Resource {
|
||||
|
||||
/** @var string $base the route base */
|
||||
protected $base = '/webhooks';
|
||||
|
||||
/**
|
||||
* Register the routes for this class
|
||||
*
|
||||
* @since 2.2
|
||||
* @param array $routes
|
||||
* @return array
|
||||
*/
|
||||
public function register_routes( $routes ) {
|
||||
|
||||
# GET|POST /webhooks
|
||||
$routes[ $this->base ] = array(
|
||||
array( array( $this, 'get_webhooks' ), WC_API_Server::READABLE ),
|
||||
array( array( $this, 'create_webhook' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
|
||||
);
|
||||
|
||||
# GET /webhooks/count
|
||||
$routes[ $this->base . '/count' ] = array(
|
||||
array( array( $this, 'get_webhooks_count' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# GET|PUT|DELETE /webhooks/<id>
|
||||
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
|
||||
array( array( $this, 'get_webhook' ), WC_API_Server::READABLE ),
|
||||
array( array( $this, 'edit_webhook' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
|
||||
array( array( $this, 'delete_webhook' ), WC_API_Server::DELETABLE ),
|
||||
);
|
||||
|
||||
# GET /webhooks/<id>/deliveries
|
||||
$routes[ $this->base . '/(?P<webhook_id>\d+)/deliveries' ] = array(
|
||||
array( array( $this, 'get_webhook_deliveries' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
# GET /webhooks/<webhook_id>/deliveries/<id>
|
||||
$routes[ $this->base . '/(?P<webhook_id>\d+)/deliveries/(?P<id>\d+)' ] = array(
|
||||
array( array( $this, 'get_webhook_delivery' ), WC_API_Server::READABLE ),
|
||||
);
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all webhooks
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @param array $fields
|
||||
* @param array $filter
|
||||
* @param string $status
|
||||
* @param int $page
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_webhooks( $fields = null, $filter = array(), $status = null, $page = 1 ) {
|
||||
|
||||
if ( ! empty( $status ) ) {
|
||||
$filter['status'] = $status;
|
||||
}
|
||||
|
||||
$filter['page'] = $page;
|
||||
|
||||
$query = $this->query_webhooks( $filter );
|
||||
|
||||
$webhooks = array();
|
||||
|
||||
foreach ( $query['results'] as $webhook_id ) {
|
||||
$webhooks[] = current( $this->get_webhook( $webhook_id, $fields ) );
|
||||
}
|
||||
|
||||
$this->server->add_pagination_headers( $query['headers'] );
|
||||
|
||||
return array( 'webhooks' => $webhooks );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the webhook for the given ID
|
||||
*
|
||||
* @since 2.2
|
||||
* @param int $id webhook ID
|
||||
* @param array $fields
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_webhook( $id, $fields = null ) {
|
||||
|
||||
// ensure webhook ID is valid & user has permission to read
|
||||
$id = $this->validate_request( $id, 'shop_webhook', 'read' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$webhook = wc_get_webhook( $id );
|
||||
|
||||
$webhook_data = array(
|
||||
'id' => $webhook->get_id(),
|
||||
'name' => $webhook->get_name(),
|
||||
'status' => $webhook->get_status(),
|
||||
'topic' => $webhook->get_topic(),
|
||||
'resource' => $webhook->get_resource(),
|
||||
'event' => $webhook->get_event(),
|
||||
'hooks' => $webhook->get_hooks(),
|
||||
'delivery_url' => $webhook->get_delivery_url(),
|
||||
'created_at' => $this->server->format_datetime( $webhook->get_date_created() ? $webhook->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times.
|
||||
'updated_at' => $this->server->format_datetime( $webhook->get_date_modified() ? $webhook->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times.
|
||||
);
|
||||
|
||||
return array( 'webhook' => apply_filters( 'woocommerce_api_webhook_response', $webhook_data, $webhook, $fields, $this ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of webhooks
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @param string $status
|
||||
* @param array $filter
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_webhooks_count( $status = null, $filter = array() ) {
|
||||
try {
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_webhooks_count', __( 'You do not have permission to read the webhooks count', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
if ( ! empty( $status ) ) {
|
||||
$filter['status'] = $status;
|
||||
}
|
||||
|
||||
$query = $this->query_webhooks( $filter );
|
||||
|
||||
return array( 'count' => $query['headers']->total );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an webhook
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @param array $data parsed webhook data
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function create_webhook( $data ) {
|
||||
|
||||
try {
|
||||
if ( ! isset( $data['webhook'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'webhook' ), 400 );
|
||||
}
|
||||
|
||||
$data = $data['webhook'];
|
||||
|
||||
// permission check
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_webhooks', __( 'You do not have permission to create webhooks.', 'woocommerce' ), 401 );
|
||||
}
|
||||
|
||||
$data = apply_filters( 'woocommerce_api_create_webhook_data', $data, $this );
|
||||
|
||||
// validate topic
|
||||
if ( empty( $data['topic'] ) || ! wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic is required and must be valid.', 'woocommerce' ), 400 );
|
||||
}
|
||||
|
||||
// validate delivery URL
|
||||
if ( empty( $data['delivery_url'] ) || ! wc_is_valid_url( $data['delivery_url'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 );
|
||||
}
|
||||
|
||||
$webhook_data = apply_filters( 'woocommerce_new_webhook_data', array(
|
||||
'post_type' => 'shop_webhook',
|
||||
'post_status' => 'publish',
|
||||
'ping_status' => 'closed',
|
||||
'post_author' => get_current_user_id(),
|
||||
'post_password' => 'webhook_' . wp_generate_password(),
|
||||
'post_title' => ! empty( $data['name'] ) ? $data['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ),
|
||||
), $data, $this );
|
||||
|
||||
$webhook = new WC_Webhook();
|
||||
|
||||
$webhook->set_name( $webhook_data['post_title'] );
|
||||
$webhook->set_user_id( $webhook_data['post_author'] );
|
||||
$webhook->set_status( 'publish' === $webhook_data['post_status'] ? 'active' : 'disabled' );
|
||||
$webhook->set_topic( $data['topic'] );
|
||||
$webhook->set_delivery_url( $data['delivery_url'] );
|
||||
$webhook->set_secret( ! empty( $data['secret'] ) ? $data['secret'] : wp_generate_password( 50, true, true ) );
|
||||
$webhook->set_api_version( 'legacy_v3' );
|
||||
$webhook->save();
|
||||
|
||||
$webhook->deliver_ping();
|
||||
|
||||
// HTTP 201 Created
|
||||
$this->server->send_status( 201 );
|
||||
|
||||
do_action( 'woocommerce_api_create_webhook', $webhook->get_id(), $this );
|
||||
|
||||
return $this->get_webhook( $webhook->get_id() );
|
||||
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a webhook
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @param int $id webhook ID
|
||||
* @param array $data parsed webhook data
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function edit_webhook( $id, $data ) {
|
||||
|
||||
try {
|
||||
if ( ! isset( $data['webhook'] ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'webhook' ), 400 );
|
||||
}
|
||||
|
||||
$data = $data['webhook'];
|
||||
|
||||
$id = $this->validate_request( $id, 'shop_webhook', 'edit' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$data = apply_filters( 'woocommerce_api_edit_webhook_data', $data, $id, $this );
|
||||
|
||||
$webhook = wc_get_webhook( $id );
|
||||
|
||||
// update topic
|
||||
if ( ! empty( $data['topic'] ) ) {
|
||||
|
||||
if ( wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) {
|
||||
|
||||
$webhook->set_topic( $data['topic'] );
|
||||
|
||||
} else {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic must be valid.', 'woocommerce' ), 400 );
|
||||
}
|
||||
}
|
||||
|
||||
// update delivery URL
|
||||
if ( ! empty( $data['delivery_url'] ) ) {
|
||||
if ( wc_is_valid_url( $data['delivery_url'] ) ) {
|
||||
|
||||
$webhook->set_delivery_url( $data['delivery_url'] );
|
||||
|
||||
} else {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 );
|
||||
}
|
||||
}
|
||||
|
||||
// update secret
|
||||
if ( ! empty( $data['secret'] ) ) {
|
||||
$webhook->set_secret( $data['secret'] );
|
||||
}
|
||||
|
||||
// update status
|
||||
if ( ! empty( $data['status'] ) ) {
|
||||
$webhook->set_status( $data['status'] );
|
||||
}
|
||||
|
||||
// update name
|
||||
if ( ! empty( $data['name'] ) ) {
|
||||
$webhook->set_name( $data['name'] );
|
||||
}
|
||||
|
||||
$webhook->save();
|
||||
|
||||
do_action( 'woocommerce_api_edit_webhook', $webhook->get_id(), $this );
|
||||
|
||||
return $this->get_webhook( $webhook->get_id() );
|
||||
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a webhook
|
||||
*
|
||||
* @since 2.2
|
||||
* @param int $id webhook ID
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function delete_webhook( $id ) {
|
||||
|
||||
$id = $this->validate_request( $id, 'shop_webhook', 'delete' );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
do_action( 'woocommerce_api_delete_webhook', $id, $this );
|
||||
|
||||
$webhook = wc_get_webhook( $id );
|
||||
|
||||
return $webhook->delete( true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get webhook post objects
|
||||
*
|
||||
* @since 2.2
|
||||
* @param array $args Request arguments for filtering query.
|
||||
* @return array
|
||||
*/
|
||||
private function query_webhooks( $args ) {
|
||||
$args = $this->merge_query_args( array(), $args );
|
||||
|
||||
$args['limit'] = isset( $args['posts_per_page'] ) ? intval( $args['posts_per_page'] ) : intval( get_option( 'posts_per_page' ) );
|
||||
|
||||
if ( empty( $args['offset'] ) ) {
|
||||
$args['offset'] = 1 < $args['paged'] ? ( $args['paged'] - 1 ) * $args['limit'] : 0;
|
||||
}
|
||||
|
||||
$page = $args['paged'];
|
||||
unset( $args['paged'], $args['posts_per_page'] );
|
||||
|
||||
if ( isset( $args['s'] ) ) {
|
||||
$args['search'] = $args['s'];
|
||||
unset( $args['s'] );
|
||||
}
|
||||
|
||||
// Post type to webhook status.
|
||||
if ( ! empty( $args['post_status'] ) ) {
|
||||
$args['status'] = $args['post_status'];
|
||||
unset( $args['post_status'] );
|
||||
}
|
||||
|
||||
if ( ! empty( $args['post__in'] ) ) {
|
||||
$args['include'] = $args['post__in'];
|
||||
unset( $args['post__in'] );
|
||||
}
|
||||
|
||||
if ( ! empty( $args['date_query'] ) ) {
|
||||
foreach ( $args['date_query'] as $date_query ) {
|
||||
if ( 'post_date_gmt' === $date_query['column'] ) {
|
||||
$args['after'] = isset( $date_query['after'] ) ? $date_query['after'] : null;
|
||||
$args['before'] = isset( $date_query['before'] ) ? $date_query['before'] : null;
|
||||
} elseif ( 'post_modified_gmt' === $date_query['column'] ) {
|
||||
$args['modified_after'] = isset( $date_query['after'] ) ? $date_query['after'] : null;
|
||||
$args['modified_before'] = isset( $date_query['before'] ) ? $date_query['before'] : null;
|
||||
}
|
||||
}
|
||||
|
||||
unset( $args['date_query'] );
|
||||
}
|
||||
|
||||
$args['paginate'] = true;
|
||||
|
||||
// Get the webhooks.
|
||||
$data_store = WC_Data_Store::load( 'webhook' );
|
||||
$results = $data_store->search_webhooks( $args );
|
||||
|
||||
// Get total items.
|
||||
$headers = new stdClass;
|
||||
$headers->page = $page;
|
||||
$headers->total = $results->total;
|
||||
$headers->is_single = $args['limit'] > $headers->total;
|
||||
$headers->total_pages = $results->max_num_pages;
|
||||
|
||||
return array(
|
||||
'results' => $results->webhooks,
|
||||
'headers' => $headers,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get deliveries for a webhook
|
||||
*
|
||||
* @since 2.2
|
||||
* @deprecated 3.3.0 Webhooks deliveries logs now uses logging system.
|
||||
* @param string $webhook_id webhook ID
|
||||
* @param string|null $fields fields to include in response
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_webhook_deliveries( $webhook_id, $fields = null ) {
|
||||
|
||||
// Ensure ID is valid webhook ID
|
||||
$webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' );
|
||||
|
||||
if ( is_wp_error( $webhook_id ) ) {
|
||||
return $webhook_id;
|
||||
}
|
||||
|
||||
return array( 'webhook_deliveries' => array() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the delivery log for the given webhook ID and delivery ID
|
||||
*
|
||||
* @since 2.2
|
||||
* @deprecated 3.3.0 Webhooks deliveries logs now uses logging system.
|
||||
* @param string $webhook_id webhook ID
|
||||
* @param string $id delivery log ID
|
||||
* @param string|null $fields fields to limit response to
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_webhook_delivery( $webhook_id, $id, $fields = null ) {
|
||||
try {
|
||||
// Validate webhook ID
|
||||
$webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' );
|
||||
|
||||
if ( is_wp_error( $webhook_id ) ) {
|
||||
return $webhook_id;
|
||||
}
|
||||
|
||||
$id = absint( $id );
|
||||
|
||||
if ( empty( $id ) ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery ID.', 'woocommerce' ), 404 );
|
||||
}
|
||||
|
||||
$webhook = new WC_Webhook( $webhook_id );
|
||||
|
||||
$log = 0;
|
||||
|
||||
if ( ! $log ) {
|
||||
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery.', 'woocommerce' ), 400 );
|
||||
}
|
||||
|
||||
return array( 'webhook_delivery' => apply_filters( 'woocommerce_api_webhook_delivery_response', array(), $id, $fields, $log, $webhook_id, $this ) );
|
||||
} catch ( WC_API_Exception $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the request by checking:
|
||||
*
|
||||
* 1) the ID is a valid integer.
|
||||
* 2) the ID returns a valid post object and matches the provided post type.
|
||||
* 3) the current user has the proper permissions to read/edit/delete the post.
|
||||
*
|
||||
* @since 3.3.0
|
||||
* @param string|int $id The post ID
|
||||
* @param string $type The post type, either `shop_order`, `shop_coupon`, or `product`.
|
||||
* @param string $context The context of the request, either `read`, `edit` or `delete`.
|
||||
* @return int|WP_Error Valid post ID or WP_Error if any of the checks fails.
|
||||
*/
|
||||
protected function validate_request( $id, $type, $context ) {
|
||||
$id = absint( $id );
|
||||
|
||||
// Validate ID.
|
||||
if ( empty( $id ) ) {
|
||||
return new WP_Error( "woocommerce_api_invalid_webhook_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
$webhook = wc_get_webhook( $id );
|
||||
|
||||
if ( null === $webhook ) {
|
||||
return new WP_Error( "woocommerce_api_no_webhook_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), 'webhook', $id ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
// Validate permissions.
|
||||
switch ( $context ) {
|
||||
|
||||
case 'read':
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
return new WP_Error( "woocommerce_api_user_cannot_read_webhook", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'edit':
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
return new WP_Error( "woocommerce_api_user_cannot_edit_webhook", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
return new WP_Error( "woocommerce_api_user_cannot_delete_webhook", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce API
|
||||
*
|
||||
* Defines an interface that API request/response handlers should implement
|
||||
*
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
interface WC_API_Handler {
|
||||
|
||||
/**
|
||||
* Get the content type for the response
|
||||
*
|
||||
* This should return the proper HTTP content-type for the response
|
||||
*
|
||||
* @since 2.1
|
||||
* @return string
|
||||
*/
|
||||
public function get_content_type();
|
||||
|
||||
/**
|
||||
* Parse the raw request body entity into an array
|
||||
*
|
||||
* @since 2.1
|
||||
* @param string $data
|
||||
* @return array
|
||||
*/
|
||||
public function parse_body( $data );
|
||||
|
||||
/**
|
||||
* Generate a response from an array of data
|
||||
*
|
||||
* @since 2.1
|
||||
* @param array $data
|
||||
* @return string
|
||||
*/
|
||||
public function generate_response( $data );
|
||||
|
||||
}
|
Loading…
Reference in New Issue