From 00a8ef5d4d91bf61bd837d1719021bf17765b856 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Wed, 19 Jun 2019 11:23:47 +0100 Subject: [PATCH 1/5] Move legacy API files from /api/ to /legacy/api --- ...lass-wc-rest-legacy-coupons-controller.php | 164 + ...class-wc-rest-legacy-orders-controller.php | 306 ++ ...ass-wc-rest-legacy-products-controller.php | 804 ++++ .../api/v1/class-wc-api-authentication.php | 410 ++ .../legacy/api/v1/class-wc-api-coupons.php | 247 ++ .../legacy/api/v1/class-wc-api-customers.php | 481 +++ .../api/v1/class-wc-api-json-handler.php | 74 + .../legacy/api/v1/class-wc-api-orders.php | 396 ++ .../legacy/api/v1/class-wc-api-products.php | 548 +++ .../legacy/api/v1/class-wc-api-reports.php | 482 +++ .../legacy/api/v1/class-wc-api-resource.php | 409 ++ .../legacy/api/v1/class-wc-api-server.php | 782 ++++ .../api/v1/class-wc-api-xml-handler.php | 308 ++ .../api/v1/interface-wc-api-handler.php | 48 + .../api/v2/class-wc-api-authentication.php | 408 ++ .../legacy/api/v2/class-wc-api-coupons.php | 575 +++ .../legacy/api/v2/class-wc-api-customers.php | 837 +++++ .../legacy/api/v2/class-wc-api-exception.php | 48 + .../api/v2/class-wc-api-json-handler.php | 73 + .../legacy/api/v2/class-wc-api-orders.php | 1830 +++++++++ .../legacy/api/v2/class-wc-api-products.php | 2312 ++++++++++++ .../legacy/api/v2/class-wc-api-reports.php | 329 ++ .../legacy/api/v2/class-wc-api-resource.php | 466 +++ .../legacy/api/v2/class-wc-api-server.php | 775 ++++ .../legacy/api/v2/class-wc-api-webhooks.php | 509 +++ .../api/v2/interface-wc-api-handler.php | 47 + .../api/v3/class-wc-api-authentication.php | 414 +++ .../legacy/api/v3/class-wc-api-coupons.php | 576 +++ .../legacy/api/v3/class-wc-api-customers.php | 829 +++++ .../legacy/api/v3/class-wc-api-exception.php | 48 + .../api/v3/class-wc-api-json-handler.php | 73 + .../legacy/api/v3/class-wc-api-orders.php | 1877 ++++++++++ .../legacy/api/v3/class-wc-api-products.php | 3308 +++++++++++++++++ .../legacy/api/v3/class-wc-api-reports.php | 330 ++ .../legacy/api/v3/class-wc-api-resource.php | 471 +++ .../legacy/api/v3/class-wc-api-server.php | 777 ++++ includes/legacy/api/v3/class-wc-api-taxes.php | 691 ++++ .../legacy/api/v3/class-wc-api-webhooks.php | 509 +++ .../api/v3/interface-wc-api-handler.php | 47 + 39 files changed, 23618 insertions(+) create mode 100644 includes/legacy/api/class-wc-rest-legacy-coupons-controller.php create mode 100644 includes/legacy/api/class-wc-rest-legacy-orders-controller.php create mode 100644 includes/legacy/api/class-wc-rest-legacy-products-controller.php create mode 100644 includes/legacy/api/v1/class-wc-api-authentication.php create mode 100644 includes/legacy/api/v1/class-wc-api-coupons.php create mode 100644 includes/legacy/api/v1/class-wc-api-customers.php create mode 100644 includes/legacy/api/v1/class-wc-api-json-handler.php create mode 100644 includes/legacy/api/v1/class-wc-api-orders.php create mode 100644 includes/legacy/api/v1/class-wc-api-products.php create mode 100644 includes/legacy/api/v1/class-wc-api-reports.php create mode 100644 includes/legacy/api/v1/class-wc-api-resource.php create mode 100644 includes/legacy/api/v1/class-wc-api-server.php create mode 100644 includes/legacy/api/v1/class-wc-api-xml-handler.php create mode 100644 includes/legacy/api/v1/interface-wc-api-handler.php create mode 100644 includes/legacy/api/v2/class-wc-api-authentication.php create mode 100644 includes/legacy/api/v2/class-wc-api-coupons.php create mode 100644 includes/legacy/api/v2/class-wc-api-customers.php create mode 100644 includes/legacy/api/v2/class-wc-api-exception.php create mode 100644 includes/legacy/api/v2/class-wc-api-json-handler.php create mode 100644 includes/legacy/api/v2/class-wc-api-orders.php create mode 100644 includes/legacy/api/v2/class-wc-api-products.php create mode 100644 includes/legacy/api/v2/class-wc-api-reports.php create mode 100644 includes/legacy/api/v2/class-wc-api-resource.php create mode 100644 includes/legacy/api/v2/class-wc-api-server.php create mode 100644 includes/legacy/api/v2/class-wc-api-webhooks.php create mode 100644 includes/legacy/api/v2/interface-wc-api-handler.php create mode 100644 includes/legacy/api/v3/class-wc-api-authentication.php create mode 100644 includes/legacy/api/v3/class-wc-api-coupons.php create mode 100644 includes/legacy/api/v3/class-wc-api-customers.php create mode 100644 includes/legacy/api/v3/class-wc-api-exception.php create mode 100644 includes/legacy/api/v3/class-wc-api-json-handler.php create mode 100644 includes/legacy/api/v3/class-wc-api-orders.php create mode 100644 includes/legacy/api/v3/class-wc-api-products.php create mode 100644 includes/legacy/api/v3/class-wc-api-reports.php create mode 100644 includes/legacy/api/v3/class-wc-api-resource.php create mode 100644 includes/legacy/api/v3/class-wc-api-server.php create mode 100644 includes/legacy/api/v3/class-wc-api-taxes.php create mode 100644 includes/legacy/api/v3/class-wc-api-webhooks.php create mode 100644 includes/legacy/api/v3/interface-wc-api-handler.php diff --git a/includes/legacy/api/class-wc-rest-legacy-coupons-controller.php b/includes/legacy/api/class-wc-rest-legacy-coupons-controller.php new file mode 100644 index 00000000000..3378de41a40 --- /dev/null +++ b/includes/legacy/api/class-wc-rest-legacy-coupons-controller.php @@ -0,0 +1,164 @@ +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 ); + } +} diff --git a/includes/legacy/api/class-wc-rest-legacy-orders-controller.php b/includes/legacy/api/class-wc-rest-legacy-orders-controller.php new file mode 100644 index 00000000000..ccc9db267f3 --- /dev/null +++ b/includes/legacy/api/class-wc-rest-legacy-orders-controller.php @@ -0,0 +1,306 @@ + '_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() ) ); + } + } +} diff --git a/includes/legacy/api/class-wc-rest-legacy-products-controller.php b/includes/legacy/api/class-wc-rest-legacy-products-controller.php new file mode 100644 index 00000000000..9096cbd0f98 --- /dev/null +++ b/includes/legacy/api/class-wc-rest-legacy-products-controller.php @@ -0,0 +1,804 @@ + '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 ); + } +} diff --git a/includes/legacy/api/v1/class-wc-api-authentication.php b/includes/legacy/api/v1/class-wc-api-authentication.php new file mode 100644 index 00000000000..1d26ae80ff6 --- /dev/null +++ b/includes/legacy/api/v1/class-wc-api-authentication.php @@ -0,0 +1,410 @@ +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' ) + ); + } +} diff --git a/includes/legacy/api/v1/class-wc-api-coupons.php b/includes/legacy/api/v1/class-wc-api-coupons.php new file mode 100644 index 00000000000..244b5efd3b0 --- /dev/null +++ b/includes/legacy/api/v1/class-wc-api-coupons.php @@ -0,0 +1,247 @@ + + * + * @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/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_coupon' ), WC_API_Server::READABLE ), + ); + + # GET /coupons/code/, note that coupon codes can contain spaces, dashes and underscores + $routes[ $this->base . '/code/(?P\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 ); + } +} diff --git a/includes/legacy/api/v1/class-wc-api-customers.php b/includes/legacy/api/v1/class-wc-api-customers.php new file mode 100644 index 00000000000..d5edb1503a2 --- /dev/null +++ b/includes/legacy/api/v1/class-wc-api-customers.php @@ -0,0 +1,481 @@ + + * GET /customers//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/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_customer' ), WC_API_SERVER::READABLE ), + ); + + # GET /customers//orders + $routes[ $this->base . '/(?P\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' ); + } +} diff --git a/includes/legacy/api/v1/class-wc-api-json-handler.php b/includes/legacy/api/v1/class-wc-api-json-handler.php new file mode 100644 index 00000000000..691bbb9663c --- /dev/null +++ b/includes/legacy/api/v1/class-wc-api-json-handler.php @@ -0,0 +1,74 @@ +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 ); + } +} diff --git a/includes/legacy/api/v1/class-wc-api-orders.php b/includes/legacy/api/v1/class-wc-api-orders.php new file mode 100644 index 00000000000..c4a391d5bdb --- /dev/null +++ b/includes/legacy/api/v1/class-wc-api-orders.php @@ -0,0 +1,396 @@ + + * GET /orders//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/ + $routes[ $this->base . '/(?P\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//notes + $routes[ $this->base . '/(?P\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; + } +} diff --git a/includes/legacy/api/v1/class-wc-api-products.php b/includes/legacy/api/v1/class-wc-api-products.php new file mode 100644 index 00000000000..b608a3f326c --- /dev/null +++ b/includes/legacy/api/v1/class-wc-api-products.php @@ -0,0 +1,548 @@ + + * GET /products//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/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_product' ), WC_API_Server::READABLE ), + ); + + # GET /products//reviews + $routes[ $this->base . '/(?P\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; + } +} diff --git a/includes/legacy/api/v1/class-wc-api-reports.php b/includes/legacy/api/v1/class-wc-api-reports.php new file mode 100644 index 00000000000..527ea64bf94 --- /dev/null +++ b/includes/legacy/api/v1/class-wc-api-reports.php @@ -0,0 +1,482 @@ +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; + } + } +} diff --git a/includes/legacy/api/v1/class-wc-api-resource.php b/includes/legacy/api/v1/class-wc-api-resource.php new file mode 100644 index 00000000000..d419f8346e9 --- /dev/null +++ b/includes/legacy/api/v1/class-wc-api-resource.php @@ -0,0 +1,409 @@ +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 + * `_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; + } + } +} diff --git a/includes/legacy/api/v1/class-wc-api-server.php b/includes/legacy/api/v1/class-wc-api-server.php new file mode 100644 index 00000000000..182d482b51f --- /dev/null +++ b/includes/legacy/api/v1/class-wc-api-server.php @@ -0,0 +1,782 @@ + 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; + } +} diff --git a/includes/legacy/api/v1/class-wc-api-xml-handler.php b/includes/legacy/api/v1/class-wc-api-xml-handler.php new file mode 100644 index 00000000000..04f47e669e4 --- /dev/null +++ b/includes/legacy/api/v1/class-wc-api-xml-handler.php @@ -0,0 +1,308 @@ +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. + * @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 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; + } +} diff --git a/includes/legacy/api/v1/interface-wc-api-handler.php b/includes/legacy/api/v1/interface-wc-api-handler.php new file mode 100644 index 00000000000..464d9cb73cb --- /dev/null +++ b/includes/legacy/api/v1/interface-wc-api-handler.php @@ -0,0 +1,48 @@ +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' ) + ); + } +} diff --git a/includes/legacy/api/v2/class-wc-api-coupons.php b/includes/legacy/api/v2/class-wc-api-coupons.php new file mode 100644 index 00000000000..ca57fb4670e --- /dev/null +++ b/includes/legacy/api/v2/class-wc-api-coupons.php @@ -0,0 +1,575 @@ + + * + * @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/ + $routes[ $this->base . '/(?P\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/, note that coupon codes can contain spaces, dashes and underscores + $routes[ $this->base . '/code/(?P\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() ) ); + } + } +} diff --git a/includes/legacy/api/v2/class-wc-api-customers.php b/includes/legacy/api/v2/class-wc-api-customers.php new file mode 100644 index 00000000000..4912be3b34e --- /dev/null +++ b/includes/legacy/api/v2/class-wc-api-customers.php @@ -0,0 +1,837 @@ + + * GET /customers//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/ + $routes[ $this->base . '/(?P\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/ + $routes[ $this->base . '/email/(?P.+)' ] = array( + array( array( $this, 'get_customer_by_email' ), WC_API_SERVER::READABLE ), + ); + + # GET /customers//orders + $routes[ $this->base . '/(?P\d+)/orders' ] = array( + array( array( $this, 'get_customer_orders' ), WC_API_SERVER::READABLE ), + ); + + # GET /customers//downloads + $routes[ $this->base . '/(?P\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() ) ); + } + } +} diff --git a/includes/legacy/api/v2/class-wc-api-exception.php b/includes/legacy/api/v2/class-wc-api-exception.php new file mode 100644 index 00000000000..834ed04d6eb --- /dev/null +++ b/includes/legacy/api/v2/class-wc-api-exception.php @@ -0,0 +1,48 @@ +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; + } +} diff --git a/includes/legacy/api/v2/class-wc-api-json-handler.php b/includes/legacy/api/v2/class-wc-api-json-handler.php new file mode 100644 index 00000000000..672aa8850c2 --- /dev/null +++ b/includes/legacy/api/v2/class-wc-api-json-handler.php @@ -0,0 +1,73 @@ +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 ); + } +} diff --git a/includes/legacy/api/v2/class-wc-api-orders.php b/includes/legacy/api/v2/class-wc-api-orders.php new file mode 100644 index 00000000000..67fc745d364 --- /dev/null +++ b/includes/legacy/api/v2/class-wc-api-orders.php @@ -0,0 +1,1830 @@ + + * GET /orders//notes + * + * @since 2.1 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET|POST /orders + $routes[ $this->base ] = array( + array( array( $this, 'get_orders' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /orders/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ), + ); + + # GET /orders/statuses + $routes[ $this->base . '/statuses' ] = array( + array( array( $this, 'get_order_statuses' ), WC_API_Server::READABLE ), + ); + + # GET|PUT|DELETE /orders/ + $routes[ $this->base . '/(?P\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 ), + array( array( $this, 'delete_order' ), WC_API_Server::DELETABLE ), + ); + + # GET|POST /orders//notes + $routes[ $this->base . '/(?P\d+)/notes' ] = array( + array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order_note' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET|PUT|DELETE /orders//notes/ + $routes[ $this->base . '/(?P\d+)/notes/(?P\d+)' ] = array( + array( array( $this, 'get_order_note' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_order_note' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_order_note' ), WC_API_SERVER::DELETABLE ), + ); + + # GET|POST /orders//refunds + $routes[ $this->base . '/(?P\d+)/refunds' ] = array( + array( array( $this, 'get_order_refunds' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order_refund' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET|PUT|DELETE /orders//refunds/ + $routes[ $this->base . '/(?P\d+)/refunds/(?P\d+)' ] = array( + array( array( $this, 'get_order_refund' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_order_refund' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_order_refund' ), WC_API_SERVER::DELETABLE ), + ); + + # POST|PUT /orders/bulk + $routes[ $this->base . '/bulk' ] = array( + array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + ); + + 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, $filter ) ); + } + + $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 + * @param array $filter + * @return array|WP_Error + */ + public function get_order( $id, $fields = null, $filter = array() ) { + + // ensure order ID is valid & user has permission to read + $id = $this->validate_request( $id, $this->post_type, 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + // Get the decimal precession + $dp = ( isset( $filter['dp'] ) ? intval( $filter['dp'] ) : 2 ); + $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(), $dp ), + 'subtotal' => wc_format_decimal( $order->get_subtotal(), $dp ), + 'total_line_items_quantity' => $order->get_item_count(), + 'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ), + 'total_shipping' => wc_format_decimal( $order->get_shipping_total(), $dp ), + 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ), + 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ), + 'total_discount' => wc_format_decimal( $order->get_total_discount(), $dp ), + '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(); + $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; + $item_meta = $item->get_formatted_meta_data( $hideprefix ); + + foreach ( $item_meta as $key => $values ) { + $item_meta[ $key ]->label = $values->display_key; + unset( $item_meta[ $key ]->display_key ); + unset( $item_meta[ $key ]->display_value ); + } + + $order_data['line_items'][] = array( + 'id' => $item_id, + 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ), + 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), $dp ), + 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), + 'total_tax' => wc_format_decimal( $item->get_total_tax(), $dp ), + 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), + '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, + 'meta' => array_values( $item_meta ), + ); + } + + // 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(), $dp ), + ); + } + + // add taxes + foreach ( $order->get_tax_totals() as $tax_code => $tax ) { + $order_data['tax_lines'][] = array( + 'id' => $tax->id, + 'rate_id' => $tax->rate_id, + 'code' => $tax_code, + 'title' => $tax->label, + 'total' => wc_format_decimal( $tax->amount, $dp ), + '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 ), $dp ), + 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ), + ); + } + + // 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(), $dp ), + ); + } + + return array( 'order' => apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server ) ); + } + + /** + * Get the total number of orders + * + * @since 2.4 + * + * @param string $status + * @param array $filter + * + * @return array|WP_Error + */ + public function get_orders_count( $status = null, $filter = array() ) { + + try { + if ( ! current_user_can( 'read_private_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), 401 ); + } + + if ( ! empty( $status ) ) { + + if ( 'any' === $status ) { + + $order_statuses = array(); + + foreach ( wc_get_order_statuses() as $slug => $name ) { + $filter['status'] = str_replace( 'wc-', '', $slug ); + $query = $this->query_orders( $filter ); + $order_statuses[ str_replace( 'wc-', '', $slug ) ] = (int) $query->found_posts; + } + + return array( 'count' => $order_statuses ); + + } else { + $filter['status'] = $status; + } + } + + $query = $this->query_orders( $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 a list of valid order statuses + * + * Note this requires no specific permissions other than being an authenticated + * API user. Order statuses (particularly custom statuses) could be considered + * private information which is why it's not in the API index. + * + * @since 2.1 + * @return array + */ + public function get_order_statuses() { + + $order_statuses = array(); + + foreach ( wc_get_order_statuses() as $slug => $name ) { + $order_statuses[ str_replace( 'wc-', '', $slug ) ] = $name; + } + + return array( 'order_statuses' => apply_filters( 'woocommerce_api_order_statuses_response', $order_statuses, $this ) ); + } + + /** + * Create an order + * + * @since 2.2 + * + * @param array $data raw order data + * + * @return array|WP_Error + */ + public function create_order( $data ) { + global $wpdb; + + try { + if ( ! isset( $data['order'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order' ), 400 ); + } + + $data = $data['order']; + + // permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order', __( 'You do not have permission to create orders', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_order_data', $data, $this ); + + // default order args, note that status is checked for validity in wc_create_order() + $default_order_args = array( + 'status' => isset( $data['status'] ) ? $data['status'] : '', + 'customer_note' => isset( $data['note'] ) ? $data['note'] : null, + ); + + // if creating order for existing customer + if ( ! empty( $data['customer_id'] ) ) { + + // make sure customer exists + if ( false === get_user_by( 'id', $data['customer_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + $default_order_args['customer_id'] = $data['customer_id']; + } + + // create the pending order + $order = $this->create_base_order( $default_order_args, $data ); + + if ( is_wp_error( $order ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order', sprintf( __( 'Cannot create order: %s', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 ); + } + + // billing/shipping addresses + $this->set_order_addresses( $order, $data ); + + $lines = array( + 'line_item' => 'line_items', + 'shipping' => 'shipping_lines', + 'fee' => 'fee_lines', + 'coupon' => 'coupon_lines', + ); + + foreach ( $lines as $line_type => $line ) { + + if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { + + $set_item = "set_{$line_type}"; + + foreach ( $data[ $line ] as $item ) { + + $this->$set_item( $order, $item, 'create' ); + } + } + } + + // calculate totals and set them + $order->calculate_totals(); + + // payment method (and payment_complete() if `paid` == true) + if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { + + // method ID & title are required + if ( empty( $data['payment_details']['method_id'] ) || empty( $data['payment_details']['method_title'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_payment_details', __( 'Payment method ID and title are required', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); + update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); + + // mark as paid if set + if ( isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { + $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); + } + } + + // set order currency + if ( isset( $data['currency'] ) ) { + + if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); + } + + // set order meta + if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { + $this->set_order_meta( $order->get_id(), $data['order_meta'] ); + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + wc_delete_shop_order_transients( $order ); + + do_action( 'woocommerce_api_create_order', $order->get_id(), $data, $this ); + do_action( 'woocommerce_new_order', $order->get_id() ); + + return $this->get_order( $order->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Creates new WC_Order. + * + * Requires a separate function for classes that extend WC_API_Orders. + * + * @since 2.3 + * + * @param $args array + * @param $data + * + * @return WC_Order + */ + protected function create_base_order( $args, $data ) { + return wc_create_order( $args ); + } + + /** + * Edit an order + * + * @since 2.2 + * + * @param int $id the order ID + * @param array $data + * + * @return array|WP_Error + */ + public function edit_order( $id, $data ) { + try { + if ( ! isset( $data['order'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order' ), 400 ); + } + + $data = $data['order']; + + $update_totals = false; + + $id = $this->validate_request( $id, $this->post_type, 'edit' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $data = apply_filters( 'woocommerce_api_edit_order_data', $data, $id, $this ); + $order = wc_get_order( $id ); + + if ( empty( $order ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); + } + + $order_args = array( 'order_id' => $order->get_id() ); + + // Customer note. + if ( isset( $data['note'] ) ) { + $order_args['customer_note'] = $data['note']; + } + + // Customer ID. + if ( isset( $data['customer_id'] ) && $data['customer_id'] != $order->get_user_id() ) { + // Make sure customer exists. + if ( false === get_user_by( 'id', $data['customer_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_customer_user', $data['customer_id'] ); + } + + // Billing/shipping address. + $this->set_order_addresses( $order, $data ); + + $lines = array( + 'line_item' => 'line_items', + 'shipping' => 'shipping_lines', + 'fee' => 'fee_lines', + 'coupon' => 'coupon_lines', + ); + + foreach ( $lines as $line_type => $line ) { + + if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { + + $update_totals = true; + + foreach ( $data[ $line ] as $item ) { + + // Item ID is always required. + if ( ! array_key_exists( 'id', $item ) ) { + $item['id'] = null; + } + + // Create item. + if ( is_null( $item['id'] ) ) { + $this->set_item( $order, $line_type, $item, 'create' ); + } elseif ( $this->item_is_null( $item ) ) { + // Delete item. + wc_delete_order_item( $item['id'] ); + } else { + // Update item. + $this->set_item( $order, $line_type, $item, 'update' ); + } + } + } + } + + // Payment method (and payment_complete() if `paid` == true and order needs payment). + if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { + + // Method ID. + if ( isset( $data['payment_details']['method_id'] ) ) { + update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); + } + + // Method title. + if ( isset( $data['payment_details']['method_title'] ) ) { + update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); + } + + // Mark as paid if set. + if ( $order->needs_payment() && isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { + $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); + } + } + + // Set order currency. + if ( isset( $data['currency'] ) ) { + if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); + } + + // If items have changed, recalculate order totals. + if ( $update_totals ) { + $order->calculate_totals(); + } + + // Update order meta. + if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { + $this->set_order_meta( $order->get_id(), $data['order_meta'] ); + } + + // Update the order post to set customer note/modified date. + wc_update_order( $order_args ); + + // Order status. + if ( ! empty( $data['status'] ) ) { + // Refresh the order instance. + $order = wc_get_order( $order->get_id() ); + $order->update_status( $data['status'], isset( $data['status_note'] ) ? $data['status_note'] : '', true ); + } + + wc_delete_shop_order_transients( $order ); + + do_action( 'woocommerce_api_edit_order', $order->get_id(), $data, $this ); + do_action( 'woocommerce_update_order', $order->get_id() ); + + return $this->get_order( $id ); + + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete an order + * + * @param int $id the order ID + * @param bool $force true to permanently delete order, false to move to trash + * @return array|WP_Error + */ + public function delete_order( $id, $force = false ) { + + $id = $this->validate_request( $id, $this->post_type, 'delete' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + wc_delete_shop_order_transients( $id ); + + do_action( 'woocommerce_api_delete_order', $id, $this ); + + return $this->delete( $id, 'order', ( 'true' === $force ) ); + } + + /** + * Helper method to get order post objects + * + * @since 2.1 + * @param array $args request arguments for filtering query + * @return WP_Query + */ + protected function query_orders( $args ) { + + // set base query arguments + $query_args = array( + 'fields' => 'ids', + 'post_type' => $this->post_type, + '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 set/update the billing & shipping addresses for + * an order + * + * @since 2.1 + * @param \WC_Order $order + * @param array $data + */ + protected function set_order_addresses( $order, $data ) { + + $address_fields = array( + 'first_name', + 'last_name', + 'company', + 'email', + 'phone', + 'address_1', + 'address_2', + 'city', + 'state', + 'postcode', + 'country', + ); + + $billing_address = $shipping_address = array(); + + // billing address + if ( isset( $data['billing_address'] ) && is_array( $data['billing_address'] ) ) { + + foreach ( $address_fields as $field ) { + + if ( isset( $data['billing_address'][ $field ] ) ) { + $billing_address[ $field ] = wc_clean( $data['billing_address'][ $field ] ); + } + } + + unset( $address_fields['email'] ); + unset( $address_fields['phone'] ); + } + + // shipping address + if ( isset( $data['shipping_address'] ) && is_array( $data['shipping_address'] ) ) { + + foreach ( $address_fields as $field ) { + + if ( isset( $data['shipping_address'][ $field ] ) ) { + $shipping_address[ $field ] = wc_clean( $data['shipping_address'][ $field ] ); + } + } + } + + $this->update_address( $order, $billing_address, 'billing' ); + $this->update_address( $order, $shipping_address, 'shipping' ); + + // update user meta + if ( $order->get_user_id() ) { + foreach ( $billing_address as $key => $value ) { + update_user_meta( $order->get_user_id(), 'billing_' . $key, $value ); + } + foreach ( $shipping_address as $key => $value ) { + update_user_meta( $order->get_user_id(), 'shipping_' . $key, $value ); + } + } + } + + /** + * Update address. + * + * @param WC_Order $order + * @param array $posted + * @param string $type + */ + protected function update_address( $order, $posted, $type = 'billing' ) { + foreach ( $posted as $key => $value ) { + if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { + $order->{"set_{$type}_{$key}"}( $value ); + } + } + } + + /** + * Helper method to add/update order meta, with two restrictions: + * + * 1) Only non-protected meta (no leading underscore) can be set + * 2) Meta values must be scalar (int, string, bool) + * + * @since 2.2 + * @param int $order_id valid order ID + * @param array $order_meta order meta in array( 'meta_key' => 'meta_value' ) format + */ + protected function set_order_meta( $order_id, $order_meta ) { + + foreach ( $order_meta as $meta_key => $meta_value ) { + + if ( is_string( $meta_key ) && ! is_protected_meta( $meta_key ) && is_scalar( $meta_value ) ) { + update_post_meta( $order_id, $meta_key, $meta_value ); + } + } + } + + /** + * Helper method to check if the resource ID associated with the provided item is null + * + * Items can be deleted by setting the resource ID to null + * + * @since 2.2 + * @param array $item item provided in the request body + * @return bool true if the item resource ID is null, false otherwise + */ + protected function item_is_null( $item ) { + + $keys = array( 'product_id', 'method_id', 'title', 'code' ); + + foreach ( $keys as $key ) { + if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { + return true; + } + } + + return false; + } + + /** + * Wrapper method to create/update order items + * + * When updating, the item ID provided is checked to ensure it is associated + * with the order. + * + * @since 2.2 + * @param \WC_Order $order order + * @param string $item_type + * @param array $item item provided in the request body + * @param string $action either 'create' or 'update' + * @throws WC_API_Exception if item ID is not associated with order + */ + protected function set_item( $order, $item_type, $item, $action ) { + global $wpdb; + + $set_method = "set_{$item_type}"; + + // verify provided line item ID is associated with order + if ( 'update' === $action ) { + + $result = $wpdb->get_row( + $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", + absint( $item['id'] ), + absint( $order->get_id() ) + ) ); + + if ( is_null( $result ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); + } + } + + $this->$set_method( $order, $item, $action ); + } + + /** + * Create or update a line item + * + * @since 2.2 + * @param \WC_Order $order + * @param array $item line item data + * @param string $action 'create' to add line item or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_line_item( $order, $item, $action ) { + $creating = ( 'create' === $action ); + + // product is always required + if ( ! isset( $item['product_id'] ) && ! isset( $item['sku'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID or SKU is required', 'woocommerce' ), 400 ); + } + + // when updating, ensure product ID provided matches + if ( 'update' === $action ) { + + $item_product_id = wc_get_order_item_meta( $item['id'], '_product_id' ); + $item_variation_id = wc_get_order_item_meta( $item['id'], '_variation_id' ); + + if ( $item['product_id'] != $item_product_id && $item['product_id'] != $item_variation_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID provided does not match this line item', 'woocommerce' ), 400 ); + } + } + + if ( isset( $item['product_id'] ) ) { + $product_id = $item['product_id']; + } elseif ( isset( $item['sku'] ) ) { + $product_id = wc_get_product_id_by_sku( $item['sku'] ); + } + + // variations must each have a key & value + $variation_id = 0; + if ( isset( $item['variations'] ) && is_array( $item['variations'] ) ) { + foreach ( $item['variations'] as $key => $value ) { + if ( ! $key || ! $value ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_variation', __( 'The product variation is invalid', 'woocommerce' ), 400 ); + } + } + $variation_id = $this->get_variation_id( wc_get_product( $product_id ), $item['variations'] ); + } + + $product = wc_get_product( $variation_id ? $variation_id : $product_id ); + + // must be a valid WC_Product + if ( ! is_object( $product ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product', __( 'Product is invalid.', 'woocommerce' ), 400 ); + } + + // quantity must be positive float + if ( isset( $item['quantity'] ) && floatval( $item['quantity'] ) <= 0 ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity must be a positive float.', 'woocommerce' ), 400 ); + } + + // quantity is required when creating + if ( $creating && ! isset( $item['quantity'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity is required.', 'woocommerce' ), 400 ); + } + + if ( $creating ) { + $line_item = new WC_Order_Item_Product(); + } else { + $line_item = new WC_Order_Item_Product( $item['id'] ); + } + + $line_item->set_product( $product ); + $line_item->set_order_id( $order->get_id() ); + + if ( isset( $item['quantity'] ) ) { + $line_item->set_quantity( $item['quantity'] ); + } + if ( isset( $item['total'] ) ) { + $line_item->set_total( floatval( $item['total'] ) ); + } elseif ( $creating ) { + $total = wc_get_price_excluding_tax( $product, array( 'qty' => $line_item->get_quantity() ) ); + $line_item->set_total( $total ); + $line_item->set_subtotal( $total ); + } + if ( isset( $item['total_tax'] ) ) { + $line_item->set_total_tax( floatval( $item['total_tax'] ) ); + } + if ( isset( $item['subtotal'] ) ) { + $line_item->set_subtotal( floatval( $item['subtotal'] ) ); + } + if ( isset( $item['subtotal_tax'] ) ) { + $line_item->set_subtotal_tax( floatval( $item['subtotal_tax'] ) ); + } + if ( $variation_id ) { + $line_item->set_variation_id( $variation_id ); + $line_item->set_variation( $item['variations'] ); + } + + // Save or add to order. + if ( $creating ) { + $order->add_item( $line_item ); + } else { + $item_id = $line_item->save(); + + if ( ! $item_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_create_line_item', __( 'Cannot create line item, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Given a product ID & API provided variations, find the correct variation ID to use for calculation + * We can't just trust input from the API to pass a variation_id manually, otherwise you could pass + * the cheapest variation ID but provide other information so we have to look up the variation ID. + * + * @param WC_Product $product + * @param array $variations + * + * @return int returns an ID if a valid variation was found for this product + */ + function get_variation_id( $product, $variations = array() ) { + $variation_id = null; + $variations_normalized = array(); + + if ( $product->is_type( 'variable' ) && $product->has_child() ) { + if ( isset( $variations ) && is_array( $variations ) ) { + // start by normalizing the passed variations + foreach ( $variations as $key => $value ) { + $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); // from get_attributes in class-wc-api-products.php + $variations_normalized[ $key ] = strtolower( $value ); + } + // now search through each product child and see if our passed variations match anything + foreach ( $product->get_children() as $variation ) { + $meta = array(); + foreach ( get_post_meta( $variation ) as $key => $value ) { + $value = $value[0]; + $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); + $meta[ $key ] = strtolower( $value ); + } + // if the variation array is a part of the $meta array, we found our match + if ( $this->array_contains( $variations_normalized, $meta ) ) { + $variation_id = $variation; + break; + } + } + } + } + + return $variation_id; + } + + /** + * Utility function to see if the meta array contains data from variations + * + * @param array $needles + * @param array $haystack + * + * @return bool + */ + protected function array_contains( $needles, $haystack ) { + foreach ( $needles as $key => $value ) { + if ( $haystack[ $key ] !== $value ) { + return false; + } + } + return true; + } + + /** + * Create or update an order shipping method + * + * @since 2.2 + * @param \WC_Order $order + * @param array $shipping item data + * @param string $action 'create' to add shipping or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_shipping( $order, $shipping, $action ) { + + // total must be a positive float + if ( isset( $shipping['total'] ) && floatval( $shipping['total'] ) < 0 ) { + throw new WC_API_Exception( 'woocommerce_invalid_shipping_total', __( 'Shipping total must be a positive amount.', 'woocommerce' ), 400 ); + } + + if ( 'create' === $action ) { + + // method ID is required + if ( ! isset( $shipping['method_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); + } + + $rate = new WC_Shipping_Rate( $shipping['method_id'], isset( $shipping['method_title'] ) ? $shipping['method_title'] : '', isset( $shipping['total'] ) ? floatval( $shipping['total'] ) : 0, array(), $shipping['method_id'] ); + $item = new WC_Order_Item_Shipping(); + $item->set_order_id( $order->get_id() ); + $item->set_shipping_rate( $rate ); + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Shipping( $shipping['id'] ); + + if ( isset( $shipping['method_id'] ) ) { + $item->set_method_id( $shipping['method_id'] ); + } + + if ( isset( $shipping['method_title'] ) ) { + $item->set_method_title( $shipping['method_title'] ); + } + + if ( isset( $shipping['total'] ) ) { + $item->set_total( floatval( $shipping['total'] ) ); + } + + $shipping_id = $item->save(); + + if ( ! $shipping_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_shipping', __( 'Cannot update shipping method, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order fee + * + * @since 2.2 + * @param \WC_Order $order + * @param array $fee item data + * @param string $action 'create' to add fee or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_fee( $order, $fee, $action ) { + + if ( 'create' === $action ) { + + // fee title is required + if ( ! isset( $fee['title'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee title is required', 'woocommerce' ), 400 ); + } + + $item = new WC_Order_Item_Fee(); + $item->set_order_id( $order->get_id() ); + $item->set_name( wc_clean( $fee['title'] ) ); + $item->set_total( isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0 ); + + // if taxable, tax class and total are required + if ( ! empty( $fee['taxable'] ) ) { + if ( ! isset( $fee['tax_class'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee tax class is required when fee is taxable.', 'woocommerce' ), 400 ); + } + + $item->set_tax_status( 'taxable' ); + $item->set_tax_class( $fee['tax_class'] ); + + if ( isset( $fee['total_tax'] ) ) { + $item->set_total_tax( isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0 ); + } + + if ( isset( $fee['tax_data'] ) ) { + $item->set_total_tax( wc_format_refund_total( array_sum( $fee['tax_data'] ) ) ); + $item->set_taxes( array_map( 'wc_format_refund_total', $fee['tax_data'] ) ); + } + } + + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Fee( $fee['id'] ); + + if ( isset( $fee['title'] ) ) { + $item->set_name( wc_clean( $fee['title'] ) ); + } + + if ( isset( $fee['tax_class'] ) ) { + $item->set_tax_class( $fee['tax_class'] ); + } + + if ( isset( $fee['total'] ) ) { + $item->set_total( floatval( $fee['total'] ) ); + } + + if ( isset( $fee['total_tax'] ) ) { + $item->set_total_tax( floatval( $fee['total_tax'] ) ); + } + + $fee_id = $item->save(); + + if ( ! $fee_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_fee', __( 'Cannot update fee, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order coupon + * + * @since 2.2 + * @param \WC_Order $order + * @param array $coupon item data + * @param string $action 'create' to add coupon or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_coupon( $order, $coupon, $action ) { + + // coupon amount must be positive float + if ( isset( $coupon['amount'] ) && floatval( $coupon['amount'] ) < 0 ) { + throw new WC_API_Exception( 'woocommerce_invalid_coupon_total', __( 'Coupon discount total must be a positive amount.', 'woocommerce' ), 400 ); + } + + if ( 'create' === $action ) { + + // coupon code is required + if ( empty( $coupon['code'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); + } + + $item = new WC_Order_Item_Coupon(); + $item->set_props( array( + 'code' => $coupon['code'], + 'discount' => isset( $coupon['amount'] ) ? floatval( $coupon['amount'] ) : 0, + 'discount_tax' => 0, + 'order_id' => $order->get_id(), + ) ); + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Coupon( $coupon['id'] ); + + if ( isset( $coupon['code'] ) ) { + $item->set_code( $coupon['code'] ); + } + + if ( isset( $coupon['amount'] ) ) { + $item->set_discount( floatval( $coupon['amount'] ) ); + } + + $coupon_id = $item->save(); + + if ( ! $coupon_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_order_coupon', __( 'Cannot update coupon, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Get the admin order notes for an order + * + * @since 2.1 + * @param string $order_id order ID + * @param string|null $fields fields to include in response + * @return array|WP_Error + */ + public function get_order_notes( $order_id, $fields = null ) { + + // ensure ID is valid order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $args = array( + 'post_id' => $order_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[] = current( $this->get_order_note( $order_id, $note->comment_ID, $fields ) ); + } + + return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $order_id, $fields, $notes, $this->server ) ); + } + + /** + * Get an order note for the given order ID and ID + * + * @since 2.2 + * + * @param string $order_id order ID + * @param string $id order note ID + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_order_note( $order_id, $id, $fields = null ) { + try { + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $order_note = 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_note' => apply_filters( 'woocommerce_api_order_note_response', $order_note, $id, $fields, $note, $order_id, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new order note for the given order + * + * @since 2.2 + * @param string $order_id order ID + * @param array $data raw request data + * @return WP_Error|array error or created note response data + */ + public function create_order_note( $order_id, $data ) { + try { + if ( ! isset( $data['order_note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_note' ), 400 ); + } + + $data = $data['order_note']; + + // permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_note', __( 'You do not have permission to create order notes', 'woocommerce' ), 401 ); + } + + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $order = wc_get_order( $order_id ); + + $data = apply_filters( 'woocommerce_api_create_order_note_data', $data, $order_id, $this ); + + // note content is required + if ( ! isset( $data['note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note', __( 'Order note is required', 'woocommerce' ), 400 ); + } + + $is_customer_note = ( isset( $data['customer_note'] ) && true === $data['customer_note'] ); + + // create the note + $note_id = $order->add_order_note( $data['note'], $is_customer_note ); + + if ( ! $note_id ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), 500 ); + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + do_action( 'woocommerce_api_create_order_note', $note_id, $order_id, $this ); + + return $this->get_order_note( $order->get_id(), $note_id ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit the order note + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id note ID + * @param array $data parsed request data + * @return WP_Error|array error or edited note response data + */ + public function edit_order_note( $order_id, $id, $data ) { + try { + if ( ! isset( $data['order_note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_note' ), 400 ); + } + + $data = $data['order_note']; + + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $order = wc_get_order( $order_id ); + + // Validate note ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + // Ensure note ID is valid + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + // Ensure note ID is associated with given order + if ( $note->comment_post_ID != $order->get_id() ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_edit_order_note_data', $data, $note->comment_ID, $order->get_id(), $this ); + + // Note content + if ( isset( $data['note'] ) ) { + + wp_update_comment( + array( + 'comment_ID' => $note->comment_ID, + 'comment_content' => $data['note'], + ) + ); + } + + // Customer note + if ( isset( $data['customer_note'] ) ) { + + update_comment_meta( $note->comment_ID, 'is_customer_note', true === $data['customer_note'] ? 1 : 0 ); + } + + do_action( 'woocommerce_api_edit_order_note', $note->comment_ID, $order->get_id(), $this ); + + return $this->get_order_note( $order->get_id(), $note->comment_ID ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete order note + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id note ID + * @return WP_Error|array error or deleted message + */ + public function delete_order_note( $order_id, $id ) { + try { + $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate note ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + // Ensure note ID is valid + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + // Ensure note ID is associated with given order + if ( $note->comment_post_ID != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); + } + + // Force delete since trashed order notes could not be managed through comments list table + $result = wc_delete_order_note( $note->comment_ID ); + + if ( ! $result ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_order_note', __( 'This order note cannot be deleted', 'woocommerce' ), 500 ); + } + + do_action( 'woocommerce_api_delete_order_note', $note->comment_ID, $order_id, $this ); + + return array( 'message' => __( 'Permanently deleted order note', 'woocommerce' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the order refunds for an order + * + * @since 2.2 + * @param string $order_id order ID + * @param string|null $fields fields to include in response + * @return array|WP_Error + */ + public function get_order_refunds( $order_id, $fields = null ) { + + // Ensure ID is valid order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $refund_items = wc_get_orders( array( + 'type' => 'shop_order_refund', + 'parent' => $order_id, + 'limit' => -1, + 'return' => 'ids', + ) ); + $order_refunds = array(); + + foreach ( $refund_items as $refund_id ) { + $order_refunds[] = current( $this->get_order_refund( $order_id, $refund_id, $fields ) ); + } + + return array( 'order_refunds' => apply_filters( 'woocommerce_api_order_refunds_response', $order_refunds, $order_id, $fields, $refund_items, $this ) ); + } + + /** + * Get an order refund for the given order ID and ID + * + * @since 2.2 + * + * @param string $order_id order ID + * @param int $id + * @param string|null $fields fields to limit response to + * @param array $filter + * + * @return array|WP_Error + */ + public function get_order_refund( $order_id, $id, $fields = null, $filter = array() ) { + try { + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + $order = wc_get_order( $order_id ); + $refund = wc_get_order( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + $line_items = array(); + + // Add line items + foreach ( $refund->get_items( 'line_item' ) as $item_id => $item ) { + $product = $item->get_product(); + $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; + $item_meta = $item->get_formatted_meta_data( $hideprefix ); + + foreach ( $item_meta as $key => $values ) { + $item_meta[ $key ]->label = $values->display_key; + unset( $item_meta[ $key ]->display_key ); + unset( $item_meta[ $key ]->display_value ); + } + + $line_items[] = array( + 'id' => $item_id, + 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ), + 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), 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, + 'meta' => array_values( $item_meta ), + 'refunded_item_id' => (int) $item->get_meta( 'refunded_item_id' ), + ); + } + + $order_refund = array( + 'id' => $refund->get_id(), + 'created_at' => $this->server->format_datetime( $refund->get_date_created() ? $refund->get_date_created()->getTimestamp() : 0, false, false ), + 'amount' => wc_format_decimal( $refund->get_amount(), 2 ), + 'reason' => $refund->get_reason(), + 'line_items' => $line_items, + ); + + return array( 'order_refund' => apply_filters( 'woocommerce_api_order_refund_response', $order_refund, $id, $fields, $refund, $order_id, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new order refund for the given order + * + * @since 2.2 + * @param string $order_id order ID + * @param array $data raw request data + * @param bool $api_refund do refund using a payment gateway API + * @return WP_Error|array error or created refund response data + */ + public function create_order_refund( $order_id, $data, $api_refund = true ) { + try { + if ( ! isset( $data['order_refund'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_refund' ), 400 ); + } + + $data = $data['order_refund']; + + // Permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_refund', __( 'You do not have permission to create order refunds', 'woocommerce' ), 401 ); + } + + $order_id = absint( $order_id ); + + if ( empty( $order_id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_create_order_refund_data', $data, $order_id, $this ); + + // Refund amount is required + if ( ! isset( $data['amount'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount is required.', 'woocommerce' ), 400 ); + } elseif ( 0 > $data['amount'] ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount must be positive.', 'woocommerce' ), 400 ); + } + + $data['order_id'] = $order_id; + $data['refund_id'] = 0; + + // Create the refund + $refund = wc_create_refund( $data ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); + } + + // Refund via API + if ( $api_refund ) { + if ( WC()->payment_gateways() ) { + $payment_gateways = WC()->payment_gateways->payment_gateways(); + } + + $order = wc_get_order( $order_id ); + + if ( isset( $payment_gateways[ $order->get_payment_method() ] ) && $payment_gateways[ $order->get_payment_method() ]->supports( 'refunds' ) ) { + $result = $payment_gateways[ $order->get_payment_method() ]->process_refund( $order_id, $refund->get_amount(), $refund->get_reason() ); + + if ( is_wp_error( $result ) ) { + return $result; + } elseif ( ! $result ) { + throw new WC_API_Exception( 'woocommerce_api_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ), 500 ); + } + } + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + do_action( 'woocommerce_api_create_order_refund', $refund->get_id(), $order_id, $this ); + + return $this->get_order_refund( $order_id, $refund->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit an order refund + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id refund ID + * @param array $data parsed request data + * @return WP_Error|array error or edited refund response data + */ + public function edit_order_refund( $order_id, $id, $data ) { + try { + if ( ! isset( $data['order_refund'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_refund' ), 400 ); + } + + $data = $data['order_refund']; + + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate refund ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + // Ensure order ID is valid + $refund = get_post( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + // Ensure refund ID is associated with given order + if ( $refund->post_parent != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_edit_order_refund_data', $data, $refund->ID, $order_id, $this ); + + // Update reason + if ( isset( $data['reason'] ) ) { + $updated_refund = wp_update_post( array( 'ID' => $refund->ID, 'post_excerpt' => $data['reason'] ) ); + + if ( is_wp_error( $updated_refund ) ) { + return $updated_refund; + } + } + + // Update refund amount + if ( isset( $data['amount'] ) && 0 < $data['amount'] ) { + update_post_meta( $refund->ID, '_refund_amount', wc_format_decimal( $data['amount'] ) ); + } + + do_action( 'woocommerce_api_edit_order_refund', $refund->ID, $order_id, $this ); + + return $this->get_order_refund( $order_id, $refund->ID ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete order refund + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id refund ID + * @return WP_Error|array error or deleted message + */ + public function delete_order_refund( $order_id, $id ) { + try { + $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate refund ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + // Ensure refund ID is valid + $refund = get_post( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + // Ensure refund ID is associated with given order + if ( $refund->post_parent != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); + } + + wc_delete_shop_order_transients( $order_id ); + + do_action( 'woocommerce_api_delete_order_refund', $refund->ID, $order_id, $this ); + + return $this->delete( $refund->ID, 'refund', true ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Bulk update or insert orders + * Accepts an array with orders in the formats supported by + * WC_API_Orders->create_order() and WC_API_Orders->edit_order() + * + * @since 2.4.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function bulk( $data ) { + + try { + if ( ! isset( $data['orders'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_orders_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'orders' ), 400 ); + } + + $data = $data['orders']; + $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'orders' ); + + // Limit bulk operation + if ( count( $data ) > $limit ) { + throw new WC_API_Exception( 'woocommerce_api_orders_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); + } + + $orders = array(); + + foreach ( $data as $_order ) { + $order_id = 0; + + // Try to get the order ID + if ( isset( $_order['id'] ) ) { + $order_id = intval( $_order['id'] ); + } + + // Order exists / edit order + if ( $order_id ) { + $edit = $this->edit_order( $order_id, array( 'order' => $_order ) ); + + if ( is_wp_error( $edit ) ) { + $orders[] = array( + 'id' => $order_id, + 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), + ); + } else { + $orders[] = $edit['order']; + } + } else { + // Order don't exists / create order + $new = $this->create_order( array( 'order' => $_order ) ); + + if ( is_wp_error( $new ) ) { + $orders[] = array( + 'id' => $order_id, + 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), + ); + } else { + $orders[] = $new['order']; + } + } + } + + return array( 'orders' => apply_filters( 'woocommerce_api_orders_bulk_response', $orders, $this ) ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/legacy/api/v2/class-wc-api-products.php b/includes/legacy/api/v2/class-wc-api-products.php new file mode 100644 index 00000000000..35a0b4c0f98 --- /dev/null +++ b/includes/legacy/api/v2/class-wc-api-products.php @@ -0,0 +1,2312 @@ + + * GET /products//reviews + * + * @since 2.1 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET/POST /products + $routes[ $this->base ] = array( + array( array( $this, 'get_products' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /products/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_products_count' ), WC_API_Server::READABLE ), + ); + + # GET/PUT/DELETE /products/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_product' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product' ), WC_API_Server::DELETABLE ), + ); + + # GET /products//reviews + $routes[ $this->base . '/(?P\d+)/reviews' ] = array( + array( array( $this, 'get_product_reviews' ), WC_API_Server::READABLE ), + ); + + # GET /products//orders + $routes[ $this->base . '/(?P\d+)/orders' ] = array( + array( array( $this, 'get_product_orders' ), WC_API_Server::READABLE ), + ); + + # GET /products/categories + $routes[ $this->base . '/categories' ] = array( + array( array( $this, 'get_product_categories' ), WC_API_Server::READABLE ), + ); + + # GET /products/categories/ + $routes[ $this->base . '/categories/(?P\d+)' ] = array( + array( array( $this, 'get_product_category' ), WC_API_Server::READABLE ), + ); + + # GET/POST /products/attributes + $routes[ $this->base . '/attributes' ] = array( + array( array( $this, 'get_product_attributes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product_attribute' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET/PUT/DELETE /attributes/ + $routes[ $this->base . '/attributes/(?P\d+)' ] = array( + array( array( $this, 'get_product_attribute' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product_attribute' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product_attribute' ), WC_API_Server::DELETABLE ), + ); + + # GET /products/sku/ + $routes[ $this->base . '/sku/(?P\w[\w\s\-]*)' ] = array( + array( array( $this, 'get_product_by_sku' ), WC_API_Server::READABLE ), + ); + + # POST|PUT /products/bulk + $routes[ $this->base . '/bulk' ] = array( + array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + ); + + 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->get_parent_id() ) { + $_product = wc_get_product( $product->get_parent_id() ); + $product_data['parent'] = $this->get_product_data( $_product ); + } + + return array( 'product' => apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields, $this->server ) ); + } + + /** + * Get the total number of products + * + * @since 2.1 + * + * @param string $type + * @param array $filter + * + * @return array|WP_Error + */ + public function get_products_count( $type = null, $filter = array() ) { + try { + if ( ! current_user_can( 'read_private_products' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), 401 ); + } + + if ( ! empty( $type ) ) { + $filter['type'] = $type; + } + + $query = $this->query_products( $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() ) ); + } + } + + /** + * Create a new product + * + * @since 2.2 + * + * @param array $data posted data + * + * @return array|WP_Error + */ + public function create_product( $data ) { + $id = 0; + + try { + if ( ! isset( $data['product'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product' ), 400 ); + } + + $data = $data['product']; + + // Check permissions + if ( ! current_user_can( 'publish_products' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product', __( 'You do not have permission to create products', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_product_data', $data, $this ); + + // Check if product title is specified + if ( ! isset( $data['title'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_title', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'title' ), 400 ); + } + + // Check product type + if ( ! isset( $data['type'] ) ) { + $data['type'] = 'simple'; + } + + // Set visible visibility when not sent + if ( ! isset( $data['catalog_visibility'] ) ) { + $data['catalog_visibility'] = 'visible'; + } + + // Validate the product type + if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 ); + } + + // Enable description html tags. + $post_content = isset( $data['description'] ) ? wc_clean( $data['description'] ) : ''; + if ( $post_content && isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) { + + $post_content = wp_filter_post_kses( $data['description'] ); + } + + // Enable short description html tags. + $post_excerpt = isset( $data['short_description'] ) ? wc_clean( $data['short_description'] ) : ''; + if ( $post_excerpt && isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) { + $post_excerpt = wp_filter_post_kses( $data['short_description'] ); + } + + $classname = WC_Product_Factory::get_classname_from_product_type( $data['type'] ); + if ( ! class_exists( $classname ) ) { + $classname = 'WC_Product_Simple'; + } + $product = new $classname(); + + $product->set_name( wc_clean( $data['title'] ) ); + $product->set_status( isset( $data['status'] ) ? wc_clean( $data['status'] ) : 'publish' ); + $product->set_short_description( isset( $data['short_description'] ) ? $post_excerpt : '' ); + $product->set_description( isset( $data['description'] ) ? $post_content : '' ); + + // Attempts to create the new product. + $product->save(); + $id = $product->get_id(); + + // Checks for an error in the product creation + if ( 0 >= $id ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product', $id->get_error_message(), 400 ); + } + + // Check for featured/gallery images, upload it and set it + if ( isset( $data['images'] ) ) { + $product = $this->save_product_images( $product, $data['images'] ); + } + + // Save product meta fields + $product = $this->save_product_meta( $product, $data ); + $product->save(); + + // Save variations + if ( isset( $data['type'] ) && 'variable' == $data['type'] && isset( $data['variations'] ) && is_array( $data['variations'] ) ) { + $this->save_variations( $product, $data ); + } + + do_action( 'woocommerce_api_create_product', $id, $data ); + + // Clear cache/transients + wc_delete_product_transients( $id ); + + $this->server->send_status( 201 ); + + return $this->get_product( $id ); + } catch ( WC_Data_Exception $e ) { + $this->clear_product( $id ); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } catch ( WC_API_Exception $e ) { + $this->clear_product( $id ); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product + * + * @since 2.2 + * + * @param int $id the product ID + * @param array $data + * + * @return array|WP_Error + */ + public function edit_product( $id, $data ) { + try { + if ( ! isset( $data['product'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product' ), 400 ); + } + + $data = $data['product']; + + $id = $this->validate_request( $id, 'product', 'edit' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $product = wc_get_product( $id ); + + $data = apply_filters( 'woocommerce_api_edit_product_data', $data, $this ); + + // Product title. + if ( isset( $data['title'] ) ) { + $product->set_name( wc_clean( $data['title'] ) ); + } + + // Product name (slug). + if ( isset( $data['name'] ) ) { + $product->set_slug( wc_clean( $data['name'] ) ); + } + + // Product status. + if ( isset( $data['status'] ) ) { + $product->set_status( wc_clean( $data['status'] ) ); + } + + // Product short description. + if ( isset( $data['short_description'] ) ) { + // Enable short description html tags. + $post_excerpt = ( isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) ? wp_filter_post_kses( $data['short_description'] ) : wc_clean( $data['short_description'] ); + $product->set_short_description( $post_excerpt ); + } + + // Product description. + if ( isset( $data['description'] ) ) { + // Enable description html tags. + $post_content = ( isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) ? wp_filter_post_kses( $data['description'] ) : wc_clean( $data['description'] ); + $product->set_description( $post_content ); + } + + // Validate the product type. + if ( isset( $data['type'] ) && ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 ); + } + + // Check for featured/gallery images, upload it and set it. + if ( isset( $data['images'] ) ) { + $product = $this->save_product_images( $product, $data['images'] ); + } + + // Save product meta fields. + $product = $this->save_product_meta( $product, $data ); + + // Save variations. + if ( $product->is_type( 'variable' ) ) { + if ( isset( $data['variations'] ) && is_array( $data['variations'] ) ) { + $this->save_variations( $product, $data ); + } else { + // Just sync variations. + $product = WC_Product_Variable::sync( $product, false ); + } + } + + $product->save(); + + do_action( 'woocommerce_api_edit_product', $id, $data ); + + // Clear cache/transients. + wc_delete_product_transients( $id ); + + return $this->get_product( $id ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product. + * + * @since 2.2 + * + * @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; + } + + $product = wc_get_product( $id ); + + do_action( 'woocommerce_api_delete_product', $id, $this ); + + // If we're forcing, then delete permanently. + if ( $force ) { + if ( $product->is_type( 'variable' ) ) { + foreach ( $product->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! empty( $child ) ) { + $child->delete( true ); + } + } + } else { + // For other product types, if the product has children, remove the relationship. + foreach ( $product->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! empty( $child ) ) { + $child->set_parent_id( 0 ); + $child->save(); + } + } + } + + $product->delete( true ); + $result = ! ( $product->get_id() > 0 ); + } else { + $product->delete(); + $result = 'trash' === $product->get_status(); + } + + if ( ! $result ) { + return new WP_Error( 'woocommerce_api_cannot_delete_product', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product' ), array( 'status' => 500 ) ); + } + + // Delete parent product transients. + if ( $parent_id = wp_get_post_parent_id( $id ) ) { + wc_delete_product_transients( $parent_id ); + } + + if ( $force ) { + return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), 'product' ) ); + } else { + $this->server->send_status( '202' ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product' ) ); + } + } + + /** + * 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; + } + + $comments = get_approved_comments( $id ); + $reviews = array(); + + foreach ( $comments as $comment ) { + + $reviews[] = array( + 'id' => intval( $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 ) ); + } + + /** + * Get the orders for a product + * + * @since 2.4.0 + * @param int $id the product ID to get orders for + * @param string fields fields to retrieve + * @param array $filter filters to include in response + * @param string $status the order status to retrieve + * @param $page $page page to retrieve + * @return array|WP_Error + */ + public function get_product_orders( $id, $fields = null, $filter = array(), $status = null, $page = 1 ) { + global $wpdb; + + $id = $this->validate_request( $id, 'product', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $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' + ", $id ) ); + + if ( empty( $order_ids ) ) { + return array( 'orders' => array() ); + } + + $filter = array_merge( $filter, array( + 'in' => implode( ',', $order_ids ), + ) ); + + $orders = WC()->api->WC_API_Orders->get_orders( $fields, $filter, $status, $page ); + + return array( 'orders' => apply_filters( 'woocommerce_api_product_orders_response', $orders['orders'], $id, $filter, $fields, $this->server ) ); + } + + /** + * Get a listing of product categories + * + * @since 2.2 + * + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_categories( $fields = null ) { + try { + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); + } + + $product_categories = array(); + + $terms = get_terms( 'product_cat', array( 'hide_empty' => false, 'fields' => 'ids' ) ); + + foreach ( $terms as $term_id ) { + $product_categories[] = current( $this->get_product_category( $term_id, $fields ) ); + } + + return array( 'product_categories' => apply_filters( 'woocommerce_api_product_categories_response', $product_categories, $terms, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product category for the given ID + * + * @since 2.2 + * + * @param string $id product category term ID + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_category( $id, $fields = null ) { + try { + $id = absint( $id ); + + // Validate ID + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'Invalid product category ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); + } + + $term = get_term( $id, 'product_cat' ); + + if ( is_wp_error( $term ) || is_null( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'A product category with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $term_id = intval( $term->term_id ); + + // Get category display type + $display_type = get_term_meta( $term_id, 'display_type', true ); + + // Get category image + $image = ''; + if ( $image_id = get_term_meta( $term_id, 'thumbnail_id', true ) ) { + $image = wp_get_attachment_url( $image_id ); + } + + $product_category = array( + 'id' => $term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'parent' => $term->parent, + 'description' => $term->description, + 'display' => $display_type ? $display_type : 'default', + 'image' => $image ? esc_url( $image ) : '', + 'count' => intval( $term->count ), + ); + + return array( 'product_category' => apply_filters( 'woocommerce_api_product_category_response', $product_category, $id, $fields, $term, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * 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'] ); + } + + // Filter products by category + if ( ! empty( $args['category'] ) ) { + $query_args['product_cat'] = $args['category']; + } + + // Filter by specific sku + if ( ! empty( $args['sku'] ) ) { + if ( ! is_array( $query_args['meta_query'] ) ) { + $query_args['meta_query'] = array(); + } + + $query_args['meta_query'][] = array( + 'key' => '_sku', + 'value' => $args['sku'], + 'compare' => '=', + ); + + $query_args['post_type'] = array( 'product', 'product_variation' ); + } + + $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(); + } + + $prices_precision = wc_get_price_decimals(); + 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(), $prices_precision ), + 'regular_price' => wc_format_decimal( $product->get_regular_price(), $prices_precision ), + 'sale_price' => $product->get_sale_price() ? wc_format_decimal( $product->get_sale_price(), $prices_precision ) : 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(), + 'product_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '', + 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '', + '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' => wpautop( do_shortcode( $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() ), + 'parent_id' => $product->get_parent_id(), + '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' => wpautop( do_shortcode( wp_kses_post( $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 ) { + $prices_precision = wc_get_price_decimals(); + $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(), $prices_precision ), + 'regular_price' => wc_format_decimal( $variation->get_regular_price(), $prices_precision ), + 'sale_price' => $variation->get_sale_price() ? wc_format_decimal( $variation->get_sale_price(), $prices_precision ) : null, + 'taxable' => $variation->is_taxable(), + 'tax_status' => $variation->get_tax_status(), + 'tax_class' => $variation->get_tax_class(), + 'managing_stock' => $variation->managing_stock(), + '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; + } + + /** + * Save default attributes. + * + * @since 3.0.0 + * @param WC_Product $product + * @param array $request + * @return WC_Product + */ + protected function save_default_attributes( $product, $request ) { + // Update default attributes options setting. + if ( isset( $request['default_attribute'] ) ) { + $request['default_attributes'] = $request['default_attribute']; + } + + if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { + $attributes = $product->get_attributes(); + $default_attributes = array(); + + foreach ( $request['default_attributes'] as $default_attr_key => $default_attr ) { + if ( ! isset( $default_attr['name'] ) ) { + continue; + } + + $taxonomy = sanitize_title( $default_attr['name'] ); + + if ( isset( $default_attr['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $default_attr['slug'] ); + } + + if ( isset( $attributes[ $taxonomy ] ) ) { + $_attribute = $attributes[ $taxonomy ]; + + if ( $_attribute['is_variation'] ) { + $value = ''; + + if ( isset( $default_attr['option'] ) ) { + if ( $_attribute['is_taxonomy'] ) { + // Don't use wc_clean as it destroys sanitized characters. + $value = sanitize_title( trim( stripslashes( $default_attr['option'] ) ) ); + } else { + $value = wc_clean( trim( stripslashes( $default_attr['option'] ) ) ); + } + } + + if ( $value ) { + $default_attributes[ $taxonomy ] = $value; + } + } + } + } + + $product->set_default_attributes( $default_attributes ); + } + + return $product; + } + + /** + * Save product meta + * + * @since 2.2 + * @param WC_Product $product + * @param array $data + * @return WC_Product + * @throws WC_API_Exception + */ + protected function save_product_meta( $product, $data ) { + global $wpdb; + + // Virtual + if ( isset( $data['virtual'] ) ) { + $product->set_virtual( $data['virtual'] ); + } + + // Tax status + if ( isset( $data['tax_status'] ) ) { + $product->set_tax_status( wc_clean( $data['tax_status'] ) ); + } + + // Tax Class + if ( isset( $data['tax_class'] ) ) { + $product->set_tax_class( wc_clean( $data['tax_class'] ) ); + } + + // Catalog Visibility + if ( isset( $data['catalog_visibility'] ) ) { + $product->set_catalog_visibility( wc_clean( $data['catalog_visibility'] ) ); + } + + // Purchase Note + if ( isset( $data['purchase_note'] ) ) { + $product->set_purchase_note( wc_clean( $data['purchase_note'] ) ); + } + + // Featured Product + if ( isset( $data['featured'] ) ) { + $product->set_featured( $data['featured'] ); + } + + // Shipping data + $product = $this->save_product_shipping_data( $product, $data ); + + // SKU + if ( isset( $data['sku'] ) ) { + $sku = $product->get_sku(); + $new_sku = wc_clean( $data['sku'] ); + + if ( '' == $new_sku ) { + $product->set_sku( '' ); + } elseif ( $new_sku !== $sku ) { + if ( ! empty( $new_sku ) ) { + $unique_sku = wc_product_has_unique_sku( $product->get_id(), $new_sku ); + if ( ! $unique_sku ) { + throw new WC_API_Exception( 'woocommerce_api_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400 ); + } else { + $product->set_sku( $new_sku ); + } + } else { + $product->set_sku( '' ); + } + } + } + + // Attributes + if ( isset( $data['attributes'] ) ) { + $attributes = array(); + + foreach ( $data['attributes'] as $attribute ) { + $is_taxonomy = 0; + $taxonomy = 0; + + if ( ! isset( $attribute['name'] ) ) { + continue; + } + + $attribute_slug = sanitize_title( $attribute['name'] ); + + if ( isset( $attribute['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); + $attribute_slug = sanitize_title( $attribute['slug'] ); + } + + if ( $taxonomy ) { + $is_taxonomy = 1; + } + + if ( $is_taxonomy ) { + + $attribute_id = wc_attribute_taxonomy_id_by_name( $attribute['name'] ); + + 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(); + } + + // Update post terms + if ( taxonomy_exists( $taxonomy ) ) { + wp_set_object_terms( $product->get_id(), $values, $taxonomy ); + } + + 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( $taxonomy ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? 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'] ) ) { + // Array based + if ( is_array( $attribute['options'] ) ) { + $values = $attribute['options']; + + // Text based, separate by pipe + } else { + $values = array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) ); + } + + // Custom attribute - Add attribute to array and set the values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_name( $attribute['name'] ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? 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; + } + } + + uasort( $attributes, 'wc_product_attribute_uasort_comparison' ); + + $product->set_attributes( $attributes ); + } + + // Sales and prices. + if ( in_array( $product->get_type(), array( 'variable', 'grouped' ) ) ) { + + // Variable and grouped products have no prices. + $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( $data['regular_price'] ) ) { + $regular_price = ( '' === $data['regular_price'] ) ? '' : $data['regular_price']; + $product->set_regular_price( $regular_price ); + } + + // Sale Price. + if ( isset( $data['sale_price'] ) ) { + $sale_price = ( '' === $data['sale_price'] ) ? '' : $data['sale_price']; + $product->set_sale_price( $sale_price ); + } + + if ( isset( $data['sale_price_dates_from'] ) ) { + $date_from = $data['sale_price_dates_from']; + } else { + $date_from = $product->get_date_on_sale_from() ? date( 'Y-m-d', $product->get_date_on_sale_from()->getTimestamp() ) : ''; + } + + if ( isset( $data['sale_price_dates_to'] ) ) { + $date_to = $data['sale_price_dates_to']; + } else { + $date_to = $product->get_date_on_sale_to() ? date( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ) : ''; + } + + if ( $date_to && ! $date_from ) { + $date_from = strtotime( 'NOW', current_time( 'timestamp', true ) ); + } + + $product->set_date_on_sale_to( $date_to ); + $product->set_date_on_sale_from( $date_from ); + + if ( $product->is_on_sale( 'edit' ) ) { + $product->set_price( $product->get_sale_price( 'edit' ) ); + } else { + $product->set_price( $product->get_regular_price( 'edit' ) ); + } + } + + // Product parent ID for groups + if ( isset( $data['parent_id'] ) ) { + $product->set_parent_id( absint( $data['parent_id'] ) ); + } + + // Sold Individually + if ( isset( $data['sold_individually'] ) ) { + $product->set_sold_individually( true === $data['sold_individually'] ? 'yes' : '' ); + } + + // Stock status + if ( isset( $data['in_stock'] ) ) { + $stock_status = ( true === $data['in_stock'] ) ? 'instock' : 'outofstock'; + } else { + $stock_status = $product->get_stock_status(); + + if ( '' === $stock_status ) { + $stock_status = 'instock'; + } + } + + // Stock Data + if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock + if ( isset( $data['managing_stock'] ) ) { + $managing_stock = ( true === $data['managing_stock'] ) ? 'yes' : 'no'; + $product->set_manage_stock( $managing_stock ); + } else { + $managing_stock = $product->get_manage_stock() ? 'yes' : 'no'; + } + + // Backorders + if ( isset( $data['backorders'] ) ) { + if ( 'notify' == $data['backorders'] ) { + $backorders = 'notify'; + } else { + $backorders = ( true === $data['backorders'] ) ? 'yes' : 'no'; + } + + $product->set_backorders( $backorders ); + } else { + $backorders = $product->get_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 ( 'yes' == $managing_stock ) { + $product->set_backorders( $backorders ); + + // 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( $data['stock_quantity'] ) ) { + $product->set_stock_quantity( wc_stock_amount( $data['stock_quantity'] ) ); + } + } else { + // Don't manage stock. + $product->set_manage_stock( 'no' ); + $product->set_backorders( $backorders ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } + } elseif ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Upsells + if ( isset( $data['upsell_ids'] ) ) { + $upsells = array(); + $ids = $data['upsell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $upsells[] = $id; + } + } + + $product->set_upsell_ids( $upsells ); + } else { + $product->set_upsell_ids( array() ); + } + } + + // Cross sells + if ( isset( $data['cross_sell_ids'] ) ) { + $crosssells = array(); + $ids = $data['cross_sell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $crosssells[] = $id; + } + } + + $product->set_cross_sell_ids( $crosssells ); + } else { + $product->set_cross_sell_ids( array() ); + } + } + + // Product categories + if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) { + $product->set_category_ids( $data['categories'] ); + } + + // Product tags + if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) { + $product->set_tag_ids( $data['tags'] ); + } + + // Downloadable + if ( isset( $data['downloadable'] ) ) { + $is_downloadable = ( true === $data['downloadable'] ) ? 'yes' : 'no'; + $product->set_downloadable( $is_downloadable ); + } else { + $is_downloadable = $product->get_downloadable() ? 'yes' : 'no'; + } + + // Downloadable options + if ( 'yes' == $is_downloadable ) { + + // Downloadable files + if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { + $product = $this->save_downloadable_files( $product, $data['downloads'] ); + } + + // Download limit + if ( isset( $data['download_limit'] ) ) { + $product->set_download_limit( $data['download_limit'] ); + } + + // Download expiry + if ( isset( $data['download_expiry'] ) ) { + $product->set_download_expiry( $data['download_expiry'] ); + } + } + + // Product url + if ( $product->is_type( 'external' ) ) { + if ( isset( $data['product_url'] ) ) { + $product->set_product_url( $data['product_url'] ); + } + + if ( isset( $data['button_text'] ) ) { + $product->set_button_text( $data['button_text'] ); + } + } + + // Reviews allowed + if ( isset( $data['reviews_allowed'] ) ) { + $product->set_reviews_allowed( $data['reviews_allowed'] ); + } + + // Save default attributes for variable products. + if ( $product->is_type( 'variable' ) ) { + $product = $this->save_default_attributes( $product, $data ); + } + + // Do action for product type + do_action( 'woocommerce_api_process_product_meta_' . $product->get_type(), $product->get_id(), $data ); + + return $product; + } + + /** + * Save variations + * + * @since 2.2 + * @param WC_Product $product + * @param array $request + * + * @return true + * + * @throws WC_API_Exception + */ + protected function save_variations( $product, $request ) { + global $wpdb; + + $id = $product->get_id(); + $attributes = $product->get_attributes(); + + foreach ( $request['variations'] as $menu_order => $data ) { + $variation_id = isset( $data['id'] ) ? absint( $data['id'] ) : 0; + $variation = new WC_Product_Variation( $variation_id ); + + // 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 = current( $data['image'] ); + if ( is_array( $image ) ) { + $image['position'] = 0; + } + + $variation = $this->save_product_images( $variation, array( $image ) ); + } + + // Virtual variation. + if ( isset( $data['virtual'] ) ) { + $variation->set_virtual( $data['virtual'] ); + } + + // Downloadable variation. + if ( isset( $data['downloadable'] ) ) { + $is_downloadable = $data['downloadable']; + $variation->set_downloadable( $is_downloadable ); + } else { + $is_downloadable = $variation->get_downloadable(); + } + + // Downloads. + if ( $is_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. + $manage_stock = (bool) $variation->get_manage_stock(); + if ( isset( $data['managing_stock'] ) ) { + $manage_stock = $data['managing_stock']; + } + $variation->set_manage_stock( $manage_stock ); + + $stock_status = $variation->get_stock_status(); + if ( isset( $data['in_stock'] ) ) { + $stock_status = true === $data['in_stock'] ? 'instock' : 'outofstock'; + } + $variation->set_stock_status( $stock_status ); + + $backorders = $variation->get_backorders(); + if ( isset( $data['backorders'] ) ) { + $backorders = $data['backorders']; + } + $variation->set_backorders( $backorders ); + + if ( $manage_stock ) { + if ( isset( $data['stock_quantity'] ) ) { + $variation->set_stock_quantity( $data['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['sale_price_dates_from'] ) ) { + $variation->set_date_on_sale_from( $data['sale_price_dates_from'] ); + } + + if ( isset( $data['sale_price_dates_to'] ) ) { + $variation->set_date_on_sale_to( $data['sale_price_dates_to'] ); + } + + // Tax class. + if ( isset( $data['tax_class'] ) ) { + $variation->set_tax_class( $data['tax_class'] ); + } + + // Update taxonomies. + if ( isset( $data['attributes'] ) ) { + $_attributes = array(); + + foreach ( $data['attributes'] as $attribute_key => $attribute ) { + if ( ! isset( $attribute['name'] ) ) { + continue; + } + + $taxonomy = 0; + $_attribute = array(); + + if ( isset( $attribute['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); + } + + if ( ! $taxonomy ) { + $taxonomy = sanitize_title( $attribute['name'] ); + } + + if ( isset( $attributes[ $taxonomy ] ) ) { + $_attribute = $attributes[ $taxonomy ]; + } + + if ( isset( $_attribute['is_variation'] ) && $_attribute['is_variation'] ) { + $_attribute_key = sanitize_title( $_attribute['name'] ); + + if ( isset( $_attribute['is_taxonomy'] ) && $_attribute['is_taxonomy'] ) { + // Don't use wc_clean as it destroys sanitized characters + $_attribute_value = isset( $attribute['option'] ) ? sanitize_title( stripslashes( $attribute['option'] ) ) : ''; + } else { + $_attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + } + + $_attributes[ $_attribute_key ] = $_attribute_value; + } + } + + $variation->set_attributes( $_attributes ); + } + + $variation->save(); + + do_action( 'woocommerce_api_save_product_variation', $variation_id, $menu_order, $variation ); + } + + return true; + } + + /** + * Save product shipping data + * + * @since 2.2 + * @param WC_Product $product + * @param array $data + * @return WC_Product + */ + private function save_product_shipping_data( $product, $data ) { + if ( isset( $data['weight'] ) ) { + $product->set_weight( '' === $data['weight'] ? '' : wc_format_decimal( $data['weight'] ) ); + } + + // Product dimensions + if ( isset( $data['dimensions'] ) ) { + // Height + if ( isset( $data['dimensions']['height'] ) ) { + $product->set_height( '' === $data['dimensions']['height'] ? '' : wc_format_decimal( $data['dimensions']['height'] ) ); + } + + // Width + if ( isset( $data['dimensions']['width'] ) ) { + $product->set_width( '' === $data['dimensions']['width'] ? '' : wc_format_decimal( $data['dimensions']['width'] ) ); + } + + // Length + if ( isset( $data['dimensions']['length'] ) ) { + $product->set_length( '' === $data['dimensions']['length'] ? '' : wc_format_decimal( $data['dimensions']['length'] ) ); + } + } + + // Virtual + if ( isset( $data['virtual'] ) ) { + $virtual = ( true === $data['virtual'] ) ? 'yes' : 'no'; + + if ( 'yes' == $virtual ) { + $product->set_weight( '' ); + $product->set_height( '' ); + $product->set_length( '' ); + $product->set_width( '' ); + } + } + + // Shipping class + if ( isset( $data['shipping_class'] ) ) { + $data_store = $product->get_data_store(); + $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) ); + $product->set_shipping_class_id( $shipping_class_id ); + } + + return $product; + } + + /** + * Save downloadable files + * + * @since 2.2 + * @param WC_Product $product + * @param array $downloads + * @param int $deprecated Deprecated since 3.0. + * @return WC_Product + */ + private function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { + if ( $deprecated ) { + wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() does not require a variation_id anymore.' ); + } + + $files = array(); + foreach ( $downloads as $key => $file ) { + if ( isset( $file['url'] ) ) { + $file['file'] = $file['url']; + } + + if ( empty( $file['file'] ) ) { + continue; + } + + $download = new WC_Product_Download(); + $download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() ); + $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); + $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); + $files[] = $download; + } + $product->set_downloads( $files ); + + return $product; + } + + /** + * Get attribute taxonomy by slug. + * + * @since 2.2 + * @param string $slug + * @return string|null + */ + private function get_attribute_taxonomy_by_slug( $slug ) { + $taxonomy = null; + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + foreach ( $attribute_taxonomies as $key => $tax ) { + if ( $slug == $tax->attribute_name ) { + $taxonomy = 'pa_' . $tax->attribute_name; + + break; + } + } + + return $taxonomy; + } + + /** + * 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' => (int) $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; + } + + /** + * Save product images + * + * @since 2.2 + * + * @param WC_Product $product + * @param array $images + * + * @return WC_Product + * @throws WC_API_Exception + */ + protected function save_product_images( $product, $images ) { + if ( is_array( $images ) ) { + $gallery = array(); + + foreach ( $images as $image ) { + if ( isset( $image['position'] ) && 0 == $image['position'] ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 ); + } + + $attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() ); + } + + $product->set_image_id( $attachment_id ); + } else { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 ); + } + + $gallery[] = $this->set_product_image_as_attachment( $upload, $product->get_id() ); + } else { + $gallery[] = $attachment_id; + } + } + } + + if ( ! empty( $gallery ) ) { + $product->set_gallery_image_ids( $gallery ); + } + } else { + $product->set_image_id( '' ); + $product->set_gallery_image_ids( array() ); + } + + return $product; + } + + /** + * Upload image from URL + * + * @since 2.2 + * + * @param string $image_url + * + * @return array + * + * @throws WC_API_Exception + */ + public function upload_product_image( $image_url ) { + $upload = wc_rest_upload_image_from_url( $image_url ); + if ( is_wp_error( $upload ) ) { + throw new WC_API_Exception( 'woocommerce_api_product_image_upload_error', $upload->get_error_message(), 400 ); + } + + return $upload; + } + + /** + * Sets product image as attachment and returns the attachment ID. + * + * @since 2.2 + * @param array $upload + * @param int $id + * @return int + */ + protected function set_product_image_as_attachment( $upload, $id ) { + $info = wp_check_filetype( $upload['file'] ); + $title = ''; + $content = ''; + + if ( $image_meta = @wp_read_image_metadata( $upload['file'] ) ) { + if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { + $title = wc_clean( $image_meta['title'] ); + } + if ( trim( $image_meta['caption'] ) ) { + $content = wc_clean( $image_meta['caption'] ); + } + } + + $attachment = array( + 'post_mime_type' => $info['type'], + 'guid' => $upload['url'], + 'post_parent' => $id, + 'post_title' => $title, + 'post_content' => $content, + ); + + $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id ); + if ( ! is_wp_error( $attachment_id ) ) { + wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) ); + } + + return $attachment_id; + } + + /** + * 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' => wc_attribute_label( str_replace( 'attribute_', '', $attribute_name ) ), + 'slug' => str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $attribute_name ) ), + 'option' => $attribute, + ); + } + } else { + + foreach ( $product->get_attributes() as $attribute ) { + $attributes[] = array( + 'name' => wc_attribute_label( $attribute['name'] ), + 'slug' => wc_attribute_taxonomy_slug( $attribute['name'] ), + 'position' => (int) $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; + } + + /** + * Get a listing of product attributes + * + * @since 2.4.0 + * + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_attributes( $fields = null ) { + try { + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 ); + } + + $product_attributes = array(); + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + foreach ( $attribute_taxonomies as $attribute ) { + $product_attributes[] = array( + 'id' => intval( $attribute->attribute_id ), + 'name' => $attribute->attribute_label, + 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ), + 'type' => $attribute->attribute_type, + 'order_by' => $attribute->attribute_orderby, + 'has_archives' => (bool) $attribute->attribute_public, + ); + } + + return array( 'product_attributes' => apply_filters( 'woocommerce_api_product_attributes_response', $product_attributes, $attribute_taxonomies, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product attribute for the given ID + * + * @since 2.4.0 + * + * @param string $id product attribute term ID + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_attribute( $id, $fields = null ) { + global $wpdb; + + try { + $id = absint( $id ); + + // Validate ID + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 ); + } + + $attribute = $wpdb->get_row( $wpdb->prepare( " + SELECT * + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_id = %d + ", $id ) ); + + if ( is_wp_error( $attribute ) || is_null( $attribute ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $product_attribute = array( + 'id' => intval( $attribute->attribute_id ), + 'name' => $attribute->attribute_label, + 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ), + 'type' => $attribute->attribute_type, + 'order_by' => $attribute->attribute_orderby, + 'has_archives' => (bool) $attribute->attribute_public, + ); + + return array( 'product_attribute' => apply_filters( 'woocommerce_api_product_attribute_response', $product_attribute, $id, $fields, $attribute, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Validate attribute data. + * + * @since 2.4.0 + * @param string $name + * @param string $slug + * @param string $type + * @param string $order_by + * @param bool $new_data + * @return bool + * @throws WC_API_Exception + */ + protected function validate_attribute_data( $name, $slug, $type, $order_by, $new_data = true ) { + if ( empty( $name ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 ); + } + + if ( strlen( $slug ) >= 28 ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), 400 ); + } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), 400 ); + } elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), 400 ); + } + + // Validate the attribute type + if ( ! in_array( wc_clean( $type ), array_keys( wc_get_attribute_types() ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_type', sprintf( __( 'Invalid product attribute type - the product attribute type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_attribute_types() ) ) ), 400 ); + } + + // Validate the attribute order by + if ( ! in_array( wc_clean( $order_by ), array( 'menu_order', 'name', 'name_num', 'id' ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_order_by', sprintf( __( 'Invalid product attribute order_by type - the product attribute order_by type must be any of these: %s', 'woocommerce' ), implode( ', ', array( 'menu_order', 'name', 'name_num', 'id' ) ) ), 400 ); + } + + return true; + } + + /** + * Create a new product attribute + * + * @since 2.4.0 + * + * @param array $data posted data + * + * @return array|WP_Error + */ + public function create_product_attribute( $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_attribute'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute' ), 400 ); + } + + $data = $data['product_attribute']; + + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_product_attribute_data', $data, $this ); + + if ( ! isset( $data['name'] ) ) { + $data['name'] = ''; + } + + // Set the attribute slug + if ( ! isset( $data['slug'] ) ) { + $data['slug'] = wc_sanitize_taxonomy_name( stripslashes( $data['name'] ) ); + } else { + $data['slug'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ) ); + } + + // Set attribute type when not sent + if ( ! isset( $data['type'] ) ) { + $data['type'] = 'select'; + } + + // Set order by when not sent + if ( ! isset( $data['order_by'] ) ) { + $data['order_by'] = 'menu_order'; + } + + // Validate the attribute data + $this->validate_attribute_data( $data['name'], $data['slug'], $data['type'], $data['order_by'], true ); + + $insert = $wpdb->insert( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + array( + 'attribute_label' => $data['name'], + 'attribute_name' => $data['slug'], + 'attribute_type' => $data['type'], + 'attribute_orderby' => $data['order_by'], + 'attribute_public' => isset( $data['has_archives'] ) && true === $data['has_archives'] ? 1 : 0, + ), + array( '%s', '%s', '%s', '%s', '%d' ) + ); + + // Checks for an error in the product creation + if ( is_wp_error( $insert ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $insert->get_error_message(), 400 ); + } + + $id = $wpdb->insert_id; + + do_action( 'woocommerce_api_create_product_attribute', $id, $data ); + + // Clear transients + delete_transient( 'wc_attribute_taxonomies' ); + WC_Cache_Helper::incr_cache_prefix( 'woocommerce-attributes' ); + + $this->server->send_status( 201 ); + + return $this->get_product_attribute( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product attribute + * + * @since 2.4.0 + * + * @param int $id the attribute ID + * @param array $data + * + * @return array|WP_Error + */ + public function edit_product_attribute( $id, $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_attribute'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute' ), 400 ); + } + + $id = absint( $id ); + $data = $data['product_attribute']; + + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_edit_product_attribute_data', $data, $this ); + $attribute = $this->get_product_attribute( $id ); + + if ( is_wp_error( $attribute ) ) { + return $attribute; + } + + $attribute_name = isset( $data['name'] ) ? $data['name'] : $attribute['product_attribute']['name']; + $attribute_type = isset( $data['type'] ) ? $data['type'] : $attribute['product_attribute']['type']; + $attribute_order_by = isset( $data['order_by'] ) ? $data['order_by'] : $attribute['product_attribute']['order_by']; + + if ( isset( $data['slug'] ) ) { + $attribute_slug = wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ); + } else { + $attribute_slug = $attribute['product_attribute']['slug']; + } + $attribute_slug = preg_replace( '/^pa\_/', '', $attribute_slug ); + + if ( isset( $data['has_archives'] ) ) { + $attribute_public = true === $data['has_archives'] ? 1 : 0; + } else { + $attribute_public = $attribute['product_attribute']['has_archives']; + } + + // Validate the attribute data + $this->validate_attribute_data( $attribute_name, $attribute_slug, $attribute_type, $attribute_order_by, false ); + + $update = $wpdb->update( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + array( + 'attribute_label' => $attribute_name, + 'attribute_name' => $attribute_slug, + 'attribute_type' => $attribute_type, + 'attribute_orderby' => $attribute_order_by, + 'attribute_public' => $attribute_public, + ), + array( 'attribute_id' => $id ), + array( '%s', '%s', '%s', '%s', '%d' ), + array( '%d' ) + ); + + // Checks for an error in the product creation + if ( false === $update ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute', __( 'Could not edit the attribute', 'woocommerce' ), 400 ); + } + + do_action( 'woocommerce_api_edit_product_attribute', $id, $data ); + + // Clear transients + delete_transient( 'wc_attribute_taxonomies' ); + WC_Cache_Helper::incr_cache_prefix( 'woocommerce-attributes' ); + + return $this->get_product_attribute( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product attribute + * + * @since 2.4.0 + * + * @param int $id the product attribute ID + * + * @return array|WP_Error + */ + public function delete_product_attribute( $id ) { + global $wpdb; + + try { + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute', __( 'You do not have permission to delete product attributes', 'woocommerce' ), 401 ); + } + + $id = absint( $id ); + + $attribute_name = $wpdb->get_var( $wpdb->prepare( " + SELECT attribute_name + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_id = %d + ", $id ) ); + + if ( is_null( $attribute_name ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $deleted = $wpdb->delete( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + array( 'attribute_id' => $id ), + array( '%d' ) + ); + + if ( false === $deleted ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute', __( 'Could not delete the attribute', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name( $attribute_name ); + + if ( taxonomy_exists( $taxonomy ) ) { + $terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' ); + foreach ( $terms as $term ) { + wp_delete_term( $term->term_id, $taxonomy ); + } + } + + do_action( 'woocommerce_attribute_deleted', $id, $attribute_name, $taxonomy ); + do_action( 'woocommerce_api_delete_product_attribute', $id, $this ); + + // Clear transients + delete_transient( 'wc_attribute_taxonomies' ); + WC_Cache_Helper::incr_cache_prefix( 'woocommerce-attributes' ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get product by SKU + * + * @deprecated 2.4.0 + * + * @since 2.3.0 + * + * @param int $sku the product SKU + * @param string $fields + * + * @return array|WP_Error + */ + public function get_product_by_sku( $sku, $fields = null ) { + try { + $id = wc_get_product_id_by_sku( $sku ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_sku', __( 'Invalid product SKU', 'woocommerce' ), 404 ); + } + + return $this->get_product( $id, $fields ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Clear product + * + * @param int $product_id + */ + protected function clear_product( $product_id ) { + if ( ! is_numeric( $product_id ) || 0 >= $product_id ) { + return; + } + + // Delete product attachments + $attachments = get_children( array( + 'post_parent' => $product_id, + 'post_status' => 'any', + 'post_type' => 'attachment', + ) ); + + foreach ( (array) $attachments as $attachment ) { + wp_delete_attachment( $attachment->ID, true ); + } + + // Delete product + $product = wc_get_product( $product_id ); + $product->delete(); + } + + /** + * Bulk update or insert products + * Accepts an array with products in the formats supported by + * WC_API_Products->create_product() and WC_API_Products->edit_product() + * + * @since 2.4.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function bulk( $data ) { + + try { + if ( ! isset( $data['products'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_products_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'products' ), 400 ); + } + + $data = $data['products']; + $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'products' ); + + // Limit bulk operation + if ( count( $data ) > $limit ) { + throw new WC_API_Exception( 'woocommerce_api_products_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); + } + + $products = array(); + + foreach ( $data as $_product ) { + $product_id = 0; + $product_sku = ''; + + // Try to get the product ID + if ( isset( $_product['id'] ) ) { + $product_id = intval( $_product['id'] ); + } + + if ( ! $product_id && isset( $_product['sku'] ) ) { + $product_sku = wc_clean( $_product['sku'] ); + $product_id = wc_get_product_id_by_sku( $product_sku ); + } + + if ( $product_id ) { + + // Product exists / edit product + $edit = $this->edit_product( $product_id, array( 'product' => $_product ) ); + + if ( is_wp_error( $edit ) ) { + $products[] = array( + 'id' => $product_id, + 'sku' => $product_sku, + 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), + ); + } else { + $products[] = $edit['product']; + } + } else { + + // Product don't exists / create product + $new = $this->create_product( array( 'product' => $_product ) ); + + if ( is_wp_error( $new ) ) { + $products[] = array( + 'id' => $product_id, + 'sku' => $product_sku, + 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), + ); + } else { + $products[] = $new['product']; + } + } + } + + return array( 'products' => apply_filters( 'woocommerce_api_products_bulk_response', $products, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/legacy/api/v2/class-wc-api-reports.php b/includes/legacy/api/v2/class-wc-api-reports.php new file mode 100644 index 00000000000..8387a2e7b9b --- /dev/null +++ b/includes/legacy/api/v2/class-wc-api-reports.php @@ -0,0 +1,329 @@ +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; + } + } +} diff --git a/includes/legacy/api/v2/class-wc-api-resource.php b/includes/legacy/api/v2/class-wc-api-resource.php new file mode 100644 index 00000000000..cd2a9ecde6f --- /dev/null +++ b/includes/legacy/api/v2/class-wc-api-resource.php @@ -0,0 +1,466 @@ +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 + * `_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; + } + } +} diff --git a/includes/legacy/api/v2/class-wc-api-server.php b/includes/legacy/api/v2/class-wc-api-server.php new file mode 100644 index 00000000000..4df7e3bfcb3 --- /dev/null +++ b/includes/legacy/api/v2/class-wc-api-server.php @@ -0,0 +1,775 @@ + 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; + } +} diff --git a/includes/legacy/api/v2/class-wc-api-webhooks.php b/includes/legacy/api/v2/class-wc-api-webhooks.php new file mode 100644 index 00000000000..83121936eaf --- /dev/null +++ b/includes/legacy/api/v2/class-wc-api-webhooks.php @@ -0,0 +1,509 @@ +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/ + $routes[ $this->base . '/(?P\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//deliveries + $routes[ $this->base . '/(?P\d+)/deliveries' ] = array( + array( array( $this, 'get_webhook_deliveries' ), WC_API_Server::READABLE ), + ); + + # GET /webhooks//deliveries/ + $routes[ $this->base . '/(?P\d+)/deliveries/(?P\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; + } +} diff --git a/includes/legacy/api/v2/interface-wc-api-handler.php b/includes/legacy/api/v2/interface-wc-api-handler.php new file mode 100644 index 00000000000..484f9f57f02 --- /dev/null +++ b/includes/legacy/api/v2/interface-wc-api-handler.php @@ -0,0 +1,47 @@ +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' ) + ); + } +} diff --git a/includes/legacy/api/v3/class-wc-api-coupons.php b/includes/legacy/api/v3/class-wc-api-coupons.php new file mode 100644 index 00000000000..43c71cb817c --- /dev/null +++ b/includes/legacy/api/v3/class-wc-api-coupons.php @@ -0,0 +1,576 @@ + + * + * @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/ + $routes[ $this->base . '/(?P\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/, note that coupon codes can contain spaces, dashes and underscores + $routes[ $this->base . '/code/(?P\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() ) ); + } + } +} diff --git a/includes/legacy/api/v3/class-wc-api-customers.php b/includes/legacy/api/v3/class-wc-api-customers.php new file mode 100644 index 00000000000..8e6c5a13d0b --- /dev/null +++ b/includes/legacy/api/v3/class-wc-api-customers.php @@ -0,0 +1,829 @@ + + * GET /customers//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/ + $routes[ $this->base . '/(?P\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/ + $routes[ $this->base . '/email/(?P.+)' ] = array( + array( array( $this, 'get_customer_by_email' ), WC_API_SERVER::READABLE ), + ); + + # GET /customers//orders + $routes[ $this->base . '/(?P\d+)/orders' ] = array( + array( array( $this, 'get_customer_orders' ), WC_API_SERVER::READABLE ), + ); + + # GET /customers//downloads + $routes[ $this->base . '/(?P\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() ) ); + } + } +} diff --git a/includes/legacy/api/v3/class-wc-api-exception.php b/includes/legacy/api/v3/class-wc-api-exception.php new file mode 100644 index 00000000000..834ed04d6eb --- /dev/null +++ b/includes/legacy/api/v3/class-wc-api-exception.php @@ -0,0 +1,48 @@ +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; + } +} diff --git a/includes/legacy/api/v3/class-wc-api-json-handler.php b/includes/legacy/api/v3/class-wc-api-json-handler.php new file mode 100644 index 00000000000..672aa8850c2 --- /dev/null +++ b/includes/legacy/api/v3/class-wc-api-json-handler.php @@ -0,0 +1,73 @@ +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 ); + } +} diff --git a/includes/legacy/api/v3/class-wc-api-orders.php b/includes/legacy/api/v3/class-wc-api-orders.php new file mode 100644 index 00000000000..aa2f69219f6 --- /dev/null +++ b/includes/legacy/api/v3/class-wc-api-orders.php @@ -0,0 +1,1877 @@ + + * GET /orders//notes + * + * @since 2.1 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET|POST /orders + $routes[ $this->base ] = array( + array( array( $this, 'get_orders' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /orders/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ), + ); + + # GET /orders/statuses + $routes[ $this->base . '/statuses' ] = array( + array( array( $this, 'get_order_statuses' ), WC_API_Server::READABLE ), + ); + + # GET|PUT|DELETE /orders/ + $routes[ $this->base . '/(?P\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 ), + array( array( $this, 'delete_order' ), WC_API_Server::DELETABLE ), + ); + + # GET|POST /orders//notes + $routes[ $this->base . '/(?P\d+)/notes' ] = array( + array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order_note' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET|PUT|DELETE /orders//notes/ + $routes[ $this->base . '/(?P\d+)/notes/(?P\d+)' ] = array( + array( array( $this, 'get_order_note' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_order_note' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_order_note' ), WC_API_SERVER::DELETABLE ), + ); + + # GET|POST /orders//refunds + $routes[ $this->base . '/(?P\d+)/refunds' ] = array( + array( array( $this, 'get_order_refunds' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order_refund' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET|PUT|DELETE /orders//refunds/ + $routes[ $this->base . '/(?P\d+)/refunds/(?P\d+)' ] = array( + array( array( $this, 'get_order_refund' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_order_refund' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_order_refund' ), WC_API_SERVER::DELETABLE ), + ); + + # POST|PUT /orders/bulk + $routes[ $this->base . '/bulk' ] = array( + array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + ); + + 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, $filter ) ); + } + + $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 Request fields. + * @param array $filter Request filters. + * @return array|WP_Error + */ + public function get_order( $id, $fields = null, $filter = array() ) { + + // Ensure order ID is valid & user has permission to read. + $id = $this->validate_request( $id, $this->post_type, 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + // Get the decimal precession. + $dp = ( isset( $filter['dp'] ) ? intval( $filter['dp'] ) : 2 ); + $order = wc_get_order( $id ); + $expand = array(); + + if ( ! empty( $filter['expand'] ) ) { + $expand = explode( ',', $filter['expand'] ); + } + + $order_data = array( + 'id' => $order->get_id(), + 'order_number' => $order->get_order_number(), + 'order_key' => $order->get_order_key(), + '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(), $dp ), + 'subtotal' => wc_format_decimal( $order->get_subtotal(), $dp ), + 'total_line_items_quantity' => $order->get_item_count(), + 'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ), + 'total_shipping' => wc_format_decimal( $order->get_shipping_total(), $dp ), + 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ), + 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ), + 'total_discount' => wc_format_decimal( $order->get_total_discount(), $dp ), + '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(); + $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; + $item_meta = $item->get_formatted_meta_data( $hideprefix ); + + foreach ( $item_meta as $key => $values ) { + $item_meta[ $key ]->label = $values->display_key; + unset( $item_meta[ $key ]->display_key ); + unset( $item_meta[ $key ]->display_value ); + } + + $line_item = array( + 'id' => $item_id, + 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ), + 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), $dp ), + 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), + 'total_tax' => wc_format_decimal( $item->get_total_tax(), $dp ), + 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), + '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, + 'meta' => array_values( $item_meta ), + ); + + if ( in_array( 'products', $expand ) && is_object( $product ) ) { + $_product_data = WC()->api->WC_API_Products->get_product( $product->get_id() ); + + if ( isset( $_product_data['product'] ) ) { + $line_item['product_data'] = $_product_data['product']; + } + } + + $order_data['line_items'][] = $line_item; + } + + // 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(), $dp ), + ); + } + + // Add taxes. + foreach ( $order->get_tax_totals() as $tax_code => $tax ) { + $tax_line = array( + 'id' => $tax->id, + 'rate_id' => $tax->rate_id, + 'code' => $tax_code, + 'title' => $tax->label, + 'total' => wc_format_decimal( $tax->amount, $dp ), + 'compound' => (bool) $tax->is_compound, + ); + + if ( in_array( 'taxes', $expand ) ) { + $_rate_data = WC()->api->WC_API_Taxes->get_tax( $tax->rate_id ); + + if ( isset( $_rate_data['tax'] ) ) { + $tax_line['rate_data'] = $_rate_data['tax']; + } + } + + $order_data['tax_lines'][] = $tax_line; + } + + // 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 ), $dp ), + 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ), + ); + } + + // Add coupons. + foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { + $coupon_line = array( + 'id' => $coupon_item_id, + 'code' => $coupon_item->get_code(), + 'amount' => wc_format_decimal( $coupon_item->get_discount(), $dp ), + ); + + if ( in_array( 'coupons', $expand ) ) { + $_coupon_data = WC()->api->WC_API_Coupons->get_coupon_by_code( $coupon_item->get_code() ); + + if ( ! is_wp_error( $_coupon_data ) && isset( $_coupon_data['coupon'] ) ) { + $coupon_line['coupon_data'] = $_coupon_data['coupon']; + } + } + + $order_data['coupon_lines'][] = $coupon_line; + } + + return array( 'order' => apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server ) ); + } + + /** + * Get the total number of orders + * + * @since 2.4 + * + * @param string $status + * @param array $filter + * + * @return array|WP_Error + */ + public function get_orders_count( $status = null, $filter = array() ) { + + try { + if ( ! current_user_can( 'read_private_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), 401 ); + } + + if ( ! empty( $status ) ) { + + if ( 'any' === $status ) { + + $order_statuses = array(); + + foreach ( wc_get_order_statuses() as $slug => $name ) { + $filter['status'] = str_replace( 'wc-', '', $slug ); + $query = $this->query_orders( $filter ); + $order_statuses[ str_replace( 'wc-', '', $slug ) ] = (int) $query->found_posts; + } + + return array( 'count' => $order_statuses ); + + } else { + $filter['status'] = $status; + } + } + + $query = $this->query_orders( $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 a list of valid order statuses + * + * Note this requires no specific permissions other than being an authenticated + * API user. Order statuses (particularly custom statuses) could be considered + * private information which is why it's not in the API index. + * + * @since 2.1 + * @return array + */ + public function get_order_statuses() { + + $order_statuses = array(); + + foreach ( wc_get_order_statuses() as $slug => $name ) { + $order_statuses[ str_replace( 'wc-', '', $slug ) ] = $name; + } + + return array( 'order_statuses' => apply_filters( 'woocommerce_api_order_statuses_response', $order_statuses, $this ) ); + } + + /** + * Create an order + * + * @since 2.2 + * @param array $data raw order data + * @return array|WP_Error + */ + public function create_order( $data ) { + global $wpdb; + + try { + if ( ! isset( $data['order'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order' ), 400 ); + } + + $data = $data['order']; + + // permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order', __( 'You do not have permission to create orders', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_order_data', $data, $this ); + + // default order args, note that status is checked for validity in wc_create_order() + $default_order_args = array( + 'status' => isset( $data['status'] ) ? $data['status'] : '', + 'customer_note' => isset( $data['note'] ) ? $data['note'] : null, + ); + + // if creating order for existing customer + if ( ! empty( $data['customer_id'] ) ) { + + // make sure customer exists + if ( false === get_user_by( 'id', $data['customer_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + $default_order_args['customer_id'] = $data['customer_id']; + } + + // create the pending order + $order = $this->create_base_order( $default_order_args, $data ); + + if ( is_wp_error( $order ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order', sprintf( __( 'Cannot create order: %s', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 ); + } + + // billing/shipping addresses + $this->set_order_addresses( $order, $data ); + + $lines = array( + 'line_item' => 'line_items', + 'shipping' => 'shipping_lines', + 'fee' => 'fee_lines', + 'coupon' => 'coupon_lines', + ); + + foreach ( $lines as $line_type => $line ) { + + if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { + + $set_item = "set_{$line_type}"; + + foreach ( $data[ $line ] as $item ) { + + $this->$set_item( $order, $item, 'create' ); + } + } + } + + // set is vat exempt + if ( isset( $data['is_vat_exempt'] ) ) { + update_post_meta( $order->get_id(), '_is_vat_exempt', $data['is_vat_exempt'] ? 'yes' : 'no' ); + } + + // calculate totals and set them + $order->calculate_totals(); + + // payment method (and payment_complete() if `paid` == true) + if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { + + // method ID & title are required + if ( empty( $data['payment_details']['method_id'] ) || empty( $data['payment_details']['method_title'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_payment_details', __( 'Payment method ID and title are required', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); + update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); + + // mark as paid if set + if ( isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { + $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); + } + } + + // set order currency + if ( isset( $data['currency'] ) ) { + + if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); + } + + // set order meta + if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { + $this->set_order_meta( $order->get_id(), $data['order_meta'] ); + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + wc_delete_shop_order_transients( $order ); + + do_action( 'woocommerce_api_create_order', $order->get_id(), $data, $this ); + do_action( 'woocommerce_new_order', $order->get_id() ); + + return $this->get_order( $order->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Creates new WC_Order. + * + * Requires a separate function for classes that extend WC_API_Orders. + * + * @since 2.3 + * + * @param $args array + * @param $data + * + * @return WC_Order + */ + protected function create_base_order( $args, $data ) { + return wc_create_order( $args ); + } + + /** + * Edit an order + * + * @since 2.2 + * @param int $id the order ID + * @param array $data + * @return array|WP_Error + */ + public function edit_order( $id, $data ) { + try { + if ( ! isset( $data['order'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order' ), 400 ); + } + + $data = $data['order']; + + $update_totals = false; + + $id = $this->validate_request( $id, $this->post_type, 'edit' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $data = apply_filters( 'woocommerce_api_edit_order_data', $data, $id, $this ); + $order = wc_get_order( $id ); + + if ( empty( $order ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); + } + + $order_args = array( 'order_id' => $order->get_id() ); + + // Customer note. + if ( isset( $data['note'] ) ) { + $order_args['customer_note'] = $data['note']; + } + + // Customer ID. + if ( isset( $data['customer_id'] ) && $data['customer_id'] != $order->get_user_id() ) { + // Make sure customer exists. + if ( false === get_user_by( 'id', $data['customer_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_customer_user', $data['customer_id'] ); + } + + // Billing/shipping address. + $this->set_order_addresses( $order, $data ); + + $lines = array( + 'line_item' => 'line_items', + 'shipping' => 'shipping_lines', + 'fee' => 'fee_lines', + 'coupon' => 'coupon_lines', + ); + + foreach ( $lines as $line_type => $line ) { + + if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { + + $update_totals = true; + + foreach ( $data[ $line ] as $item ) { + // Item ID is always required. + if ( ! array_key_exists( 'id', $item ) ) { + $item['id'] = null; + } + + // Create item. + if ( is_null( $item['id'] ) ) { + $this->set_item( $order, $line_type, $item, 'create' ); + } elseif ( $this->item_is_null( $item ) ) { + // Delete item. + wc_delete_order_item( $item['id'] ); + } else { + // Update item. + $this->set_item( $order, $line_type, $item, 'update' ); + } + } + } + } + + // Payment method (and payment_complete() if `paid` == true and order needs payment). + if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { + + // Method ID. + if ( isset( $data['payment_details']['method_id'] ) ) { + update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); + } + + // Method title. + if ( isset( $data['payment_details']['method_title'] ) ) { + update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); + } + + // Mark as paid if set. + if ( $order->needs_payment() && isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { + $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); + } + } + + // Set order currency. + if ( isset( $data['currency'] ) ) { + if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); + } + + // If items have changed, recalculate order totals. + if ( $update_totals ) { + $order->calculate_totals(); + } + + // Update order meta. + if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { + $this->set_order_meta( $order->get_id(), $data['order_meta'] ); + } + + // Update the order post to set customer note/modified date. + wc_update_order( $order_args ); + + // Order status. + if ( ! empty( $data['status'] ) ) { + // Refresh the order instance. + $order = wc_get_order( $order->get_id() ); + $order->update_status( $data['status'], isset( $data['status_note'] ) ? $data['status_note'] : '', true ); + } + + wc_delete_shop_order_transients( $order ); + + do_action( 'woocommerce_api_edit_order', $order->get_id(), $data, $this ); + do_action( 'woocommerce_update_order', $order->get_id() ); + + return $this->get_order( $id ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete an order + * + * @param int $id the order ID + * @param bool $force true to permanently delete order, false to move to trash + * @return array|WP_Error + */ + public function delete_order( $id, $force = false ) { + + $id = $this->validate_request( $id, $this->post_type, 'delete' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + wc_delete_shop_order_transients( $id ); + + do_action( 'woocommerce_api_delete_order', $id, $this ); + + return $this->delete( $id, 'order', ( 'true' === $force ) ); + } + + /** + * Helper method to get order post objects + * + * @since 2.1 + * @param array $args request arguments for filtering query + * @return WP_Query + */ + protected function query_orders( $args ) { + + // set base query arguments + $query_args = array( + 'fields' => 'ids', + 'post_type' => $this->post_type, + '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'] ); + } + + if ( ! empty( $args['customer_id'] ) ) { + $query_args['meta_query'] = array( + array( + 'key' => '_customer_user', + 'value' => absint( $args['customer_id'] ), + 'compare' => '=', + ), + ); + } + + $query_args = $this->merge_query_args( $query_args, $args ); + + return new WP_Query( $query_args ); + } + + /** + * Helper method to set/update the billing & shipping addresses for + * an order + * + * @since 2.1 + * @param \WC_Order $order + * @param array $data + */ + protected function set_order_addresses( $order, $data ) { + + $address_fields = array( + 'first_name', + 'last_name', + 'company', + 'email', + 'phone', + 'address_1', + 'address_2', + 'city', + 'state', + 'postcode', + 'country', + ); + + $billing_address = $shipping_address = array(); + + // billing address + if ( isset( $data['billing_address'] ) && is_array( $data['billing_address'] ) ) { + + foreach ( $address_fields as $field ) { + + if ( isset( $data['billing_address'][ $field ] ) ) { + $billing_address[ $field ] = wc_clean( $data['billing_address'][ $field ] ); + } + } + + unset( $address_fields['email'] ); + unset( $address_fields['phone'] ); + } + + // shipping address + if ( isset( $data['shipping_address'] ) && is_array( $data['shipping_address'] ) ) { + + foreach ( $address_fields as $field ) { + + if ( isset( $data['shipping_address'][ $field ] ) ) { + $shipping_address[ $field ] = wc_clean( $data['shipping_address'][ $field ] ); + } + } + } + + $this->update_address( $order, $billing_address, 'billing' ); + $this->update_address( $order, $shipping_address, 'shipping' ); + + // update user meta + if ( $order->get_user_id() ) { + foreach ( $billing_address as $key => $value ) { + update_user_meta( $order->get_user_id(), 'billing_' . $key, $value ); + } + foreach ( $shipping_address as $key => $value ) { + update_user_meta( $order->get_user_id(), 'shipping_' . $key, $value ); + } + } + } + + /** + * Update address. + * + * @param WC_Order $order + * @param array $posted + * @param string $type + */ + protected function update_address( $order, $posted, $type = 'billing' ) { + foreach ( $posted as $key => $value ) { + if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { + $order->{"set_{$type}_{$key}"}( $value ); + } + } + } + + /** + * Helper method to add/update order meta, with two restrictions: + * + * 1) Only non-protected meta (no leading underscore) can be set + * 2) Meta values must be scalar (int, string, bool) + * + * @since 2.2 + * @param int $order_id valid order ID + * @param array $order_meta order meta in array( 'meta_key' => 'meta_value' ) format + */ + protected function set_order_meta( $order_id, $order_meta ) { + + foreach ( $order_meta as $meta_key => $meta_value ) { + + if ( is_string( $meta_key ) && ! is_protected_meta( $meta_key ) && is_scalar( $meta_value ) ) { + update_post_meta( $order_id, $meta_key, $meta_value ); + } + } + } + + /** + * Helper method to check if the resource ID associated with the provided item is null + * + * Items can be deleted by setting the resource ID to null + * + * @since 2.2 + * @param array $item item provided in the request body + * @return bool true if the item resource ID is null, false otherwise + */ + protected function item_is_null( $item ) { + + $keys = array( 'product_id', 'method_id', 'title', 'code' ); + + foreach ( $keys as $key ) { + if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { + return true; + } + } + + return false; + } + + /** + * Wrapper method to create/update order items + * + * When updating, the item ID provided is checked to ensure it is associated + * with the order. + * + * @since 2.2 + * @param \WC_Order $order order + * @param string $item_type + * @param array $item item provided in the request body + * @param string $action either 'create' or 'update' + * @throws WC_API_Exception if item ID is not associated with order + */ + protected function set_item( $order, $item_type, $item, $action ) { + global $wpdb; + + $set_method = "set_{$item_type}"; + + // verify provided line item ID is associated with order + if ( 'update' === $action ) { + + $result = $wpdb->get_row( + $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", + absint( $item['id'] ), + absint( $order->get_id() ) + ) ); + + if ( is_null( $result ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); + } + } + + $this->$set_method( $order, $item, $action ); + } + + /** + * Create or update a line item + * + * @since 2.2 + * @param \WC_Order $order + * @param array $item line item data + * @param string $action 'create' to add line item or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_line_item( $order, $item, $action ) { + $creating = ( 'create' === $action ); + + // product is always required + if ( ! isset( $item['product_id'] ) && ! isset( $item['sku'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID or SKU is required', 'woocommerce' ), 400 ); + } + + // when updating, ensure product ID provided matches + if ( 'update' === $action ) { + + $item_product_id = wc_get_order_item_meta( $item['id'], '_product_id' ); + $item_variation_id = wc_get_order_item_meta( $item['id'], '_variation_id' ); + + if ( $item['product_id'] != $item_product_id && $item['product_id'] != $item_variation_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID provided does not match this line item', 'woocommerce' ), 400 ); + } + } + + if ( isset( $item['product_id'] ) ) { + $product_id = $item['product_id']; + } elseif ( isset( $item['sku'] ) ) { + $product_id = wc_get_product_id_by_sku( $item['sku'] ); + } + + // variations must each have a key & value + $variation_id = 0; + if ( isset( $item['variations'] ) && is_array( $item['variations'] ) ) { + foreach ( $item['variations'] as $key => $value ) { + if ( ! $key || ! $value ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_variation', __( 'The product variation is invalid', 'woocommerce' ), 400 ); + } + } + $variation_id = $this->get_variation_id( wc_get_product( $product_id ), $item['variations'] ); + } + + $product = wc_get_product( $variation_id ? $variation_id : $product_id ); + + // must be a valid WC_Product + if ( ! is_object( $product ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product', __( 'Product is invalid.', 'woocommerce' ), 400 ); + } + + // quantity must be positive float + if ( isset( $item['quantity'] ) && floatval( $item['quantity'] ) <= 0 ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity must be a positive float.', 'woocommerce' ), 400 ); + } + + // quantity is required when creating + if ( $creating && ! isset( $item['quantity'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity is required.', 'woocommerce' ), 400 ); + } + + // quantity + if ( $creating ) { + $line_item = new WC_Order_Item_Product(); + } else { + $line_item = new WC_Order_Item_Product( $item['id'] ); + } + + $line_item->set_product( $product ); + $line_item->set_order_id( $order->get_id() ); + + if ( isset( $item['quantity'] ) ) { + $line_item->set_quantity( $item['quantity'] ); + } + if ( isset( $item['total'] ) ) { + $line_item->set_total( floatval( $item['total'] ) ); + } elseif ( $creating ) { + $total = wc_get_price_excluding_tax( $product, array( 'qty' => $line_item->get_quantity() ) ); + $line_item->set_total( $total ); + $line_item->set_subtotal( $total ); + } + if ( isset( $item['total_tax'] ) ) { + $line_item->set_total_tax( floatval( $item['total_tax'] ) ); + } + if ( isset( $item['subtotal'] ) ) { + $line_item->set_subtotal( floatval( $item['subtotal'] ) ); + } + if ( isset( $item['subtotal_tax'] ) ) { + $line_item->set_subtotal_tax( floatval( $item['subtotal_tax'] ) ); + } + if ( $variation_id ) { + $line_item->set_variation_id( $variation_id ); + $line_item->set_variation( $item['variations'] ); + } + + // Save or add to order. + if ( $creating ) { + $order->add_item( $line_item ); + } else { + $item_id = $line_item->save(); + + if ( ! $item_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_create_line_item', __( 'Cannot create line item, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Given a product ID & API provided variations, find the correct variation ID to use for calculation + * We can't just trust input from the API to pass a variation_id manually, otherwise you could pass + * the cheapest variation ID but provide other information so we have to look up the variation ID. + * + * @param WC_Product $product Product instance + * @param array $variations + * + * @return int Returns an ID if a valid variation was found for this product + */ + public function get_variation_id( $product, $variations = array() ) { + $variation_id = null; + $variations_normalized = array(); + + if ( $product->is_type( 'variable' ) && $product->has_child() ) { + if ( isset( $variations ) && is_array( $variations ) ) { + // start by normalizing the passed variations + foreach ( $variations as $key => $value ) { + $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); // from get_attributes in class-wc-api-products.php + $variations_normalized[ $key ] = strtolower( $value ); + } + // now search through each product child and see if our passed variations match anything + foreach ( $product->get_children() as $variation ) { + $meta = array(); + foreach ( get_post_meta( $variation ) as $key => $value ) { + $value = $value[0]; + $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); + $meta[ $key ] = strtolower( $value ); + } + // if the variation array is a part of the $meta array, we found our match + if ( $this->array_contains( $variations_normalized, $meta ) ) { + $variation_id = $variation; + break; + } + } + } + } + + return $variation_id; + } + + /** + * Utility function to see if the meta array contains data from variations + * + * @param array $needles + * @param array $haystack + * + * @return bool + */ + protected function array_contains( $needles, $haystack ) { + foreach ( $needles as $key => $value ) { + if ( $haystack[ $key ] !== $value ) { + return false; + } + } + return true; + } + + /** + * Create or update an order shipping method + * + * @since 2.2 + * @param \WC_Order $order + * @param array $shipping item data + * @param string $action 'create' to add shipping or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_shipping( $order, $shipping, $action ) { + + // total must be a positive float + if ( isset( $shipping['total'] ) && floatval( $shipping['total'] ) < 0 ) { + throw new WC_API_Exception( 'woocommerce_invalid_shipping_total', __( 'Shipping total must be a positive amount.', 'woocommerce' ), 400 ); + } + + if ( 'create' === $action ) { + + // method ID is required + if ( ! isset( $shipping['method_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); + } + + $rate = new WC_Shipping_Rate( $shipping['method_id'], isset( $shipping['method_title'] ) ? $shipping['method_title'] : '', isset( $shipping['total'] ) ? floatval( $shipping['total'] ) : 0, array(), $shipping['method_id'] ); + $item = new WC_Order_Item_Shipping(); + $item->set_order_id( $order->get_id() ); + $item->set_shipping_rate( $rate ); + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Shipping( $shipping['id'] ); + + if ( isset( $shipping['method_id'] ) ) { + $item->set_method_id( $shipping['method_id'] ); + } + + if ( isset( $shipping['method_title'] ) ) { + $item->set_method_title( $shipping['method_title'] ); + } + + if ( isset( $shipping['total'] ) ) { + $item->set_total( floatval( $shipping['total'] ) ); + } + + $shipping_id = $item->save(); + + if ( ! $shipping_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_shipping', __( 'Cannot update shipping method, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order fee + * + * @since 2.2 + * @param \WC_Order $order + * @param array $fee item data + * @param string $action 'create' to add fee or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_fee( $order, $fee, $action ) { + + if ( 'create' === $action ) { + + // fee title is required + if ( ! isset( $fee['title'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee title is required', 'woocommerce' ), 400 ); + } + + $item = new WC_Order_Item_Fee(); + $item->set_order_id( $order->get_id() ); + $item->set_name( wc_clean( $fee['title'] ) ); + $item->set_total( isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0 ); + + // if taxable, tax class and total are required + if ( ! empty( $fee['taxable'] ) ) { + if ( ! isset( $fee['tax_class'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee tax class is required when fee is taxable.', 'woocommerce' ), 400 ); + } + + $item->set_tax_status( 'taxable' ); + $item->set_tax_class( $fee['tax_class'] ); + + if ( isset( $fee['total_tax'] ) ) { + $item->set_total_tax( isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0 ); + } + + if ( isset( $fee['tax_data'] ) ) { + $item->set_total_tax( wc_format_refund_total( array_sum( $fee['tax_data'] ) ) ); + $item->set_taxes( array_map( 'wc_format_refund_total', $fee['tax_data'] ) ); + } + } + + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Fee( $fee['id'] ); + + if ( isset( $fee['title'] ) ) { + $item->set_name( wc_clean( $fee['title'] ) ); + } + + if ( isset( $fee['tax_class'] ) ) { + $item->set_tax_class( $fee['tax_class'] ); + } + + if ( isset( $fee['total'] ) ) { + $item->set_total( floatval( $fee['total'] ) ); + } + + if ( isset( $fee['total_tax'] ) ) { + $item->set_total_tax( floatval( $fee['total_tax'] ) ); + } + + $fee_id = $item->save(); + + if ( ! $fee_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_fee', __( 'Cannot update fee, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order coupon + * + * @since 2.2 + * @param \WC_Order $order + * @param array $coupon item data + * @param string $action 'create' to add coupon or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_coupon( $order, $coupon, $action ) { + + // coupon amount must be positive float + if ( isset( $coupon['amount'] ) && floatval( $coupon['amount'] ) < 0 ) { + throw new WC_API_Exception( 'woocommerce_invalid_coupon_total', __( 'Coupon discount total must be a positive amount.', 'woocommerce' ), 400 ); + } + + if ( 'create' === $action ) { + + // coupon code is required + if ( empty( $coupon['code'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); + } + + $item = new WC_Order_Item_Coupon(); + $item->set_props( array( + 'code' => $coupon['code'], + 'discount' => isset( $coupon['amount'] ) ? floatval( $coupon['amount'] ) : 0, + 'discount_tax' => 0, + 'order_id' => $order->get_id(), + ) ); + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Coupon( $coupon['id'] ); + + if ( isset( $coupon['code'] ) ) { + $item->set_code( $coupon['code'] ); + } + + if ( isset( $coupon['amount'] ) ) { + $item->set_discount( floatval( $coupon['amount'] ) ); + } + + $coupon_id = $item->save(); + + if ( ! $coupon_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_order_coupon', __( 'Cannot update coupon, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Get the admin order notes for an order + * + * @since 2.1 + * @param string $order_id order ID + * @param string|null $fields fields to include in response + * @return array|WP_Error + */ + public function get_order_notes( $order_id, $fields = null ) { + + // ensure ID is valid order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $args = array( + 'post_id' => $order_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[] = current( $this->get_order_note( $order_id, $note->comment_ID, $fields ) ); + } + + return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $order_id, $fields, $notes, $this->server ) ); + } + + /** + * Get an order note for the given order ID and ID + * + * @since 2.2 + * + * @param string $order_id order ID + * @param string $id order note ID + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_order_note( $order_id, $id, $fields = null ) { + try { + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $order_note = 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_note' => apply_filters( 'woocommerce_api_order_note_response', $order_note, $id, $fields, $note, $order_id, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new order note for the given order + * + * @since 2.2 + * @param string $order_id order ID + * @param array $data raw request data + * @return WP_Error|array error or created note response data + */ + public function create_order_note( $order_id, $data ) { + try { + if ( ! isset( $data['order_note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_note' ), 400 ); + } + + $data = $data['order_note']; + + // permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_note', __( 'You do not have permission to create order notes', 'woocommerce' ), 401 ); + } + + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $order = wc_get_order( $order_id ); + + $data = apply_filters( 'woocommerce_api_create_order_note_data', $data, $order_id, $this ); + + // note content is required + if ( ! isset( $data['note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note', __( 'Order note is required', 'woocommerce' ), 400 ); + } + + $is_customer_note = ( isset( $data['customer_note'] ) && true === $data['customer_note'] ); + + // create the note + $note_id = $order->add_order_note( $data['note'], $is_customer_note ); + + if ( ! $note_id ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), 500 ); + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + do_action( 'woocommerce_api_create_order_note', $note_id, $order_id, $this ); + + return $this->get_order_note( $order->get_id(), $note_id ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit the order note + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id note ID + * @param array $data parsed request data + * @return WP_Error|array error or edited note response data + */ + public function edit_order_note( $order_id, $id, $data ) { + try { + if ( ! isset( $data['order_note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_note' ), 400 ); + } + + $data = $data['order_note']; + + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $order = wc_get_order( $order_id ); + + // Validate note ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + // Ensure note ID is valid + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + // Ensure note ID is associated with given order + if ( $note->comment_post_ID != $order->get_id() ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_edit_order_note_data', $data, $note->comment_ID, $order->get_id(), $this ); + + // Note content + if ( isset( $data['note'] ) ) { + + wp_update_comment( + array( + 'comment_ID' => $note->comment_ID, + 'comment_content' => $data['note'], + ) + ); + } + + // Customer note + if ( isset( $data['customer_note'] ) ) { + + update_comment_meta( $note->comment_ID, 'is_customer_note', true === $data['customer_note'] ? 1 : 0 ); + } + + do_action( 'woocommerce_api_edit_order_note', $note->comment_ID, $order->get_id(), $this ); + + return $this->get_order_note( $order->get_id(), $note->comment_ID ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete order note + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id note ID + * @return WP_Error|array error or deleted message + */ + public function delete_order_note( $order_id, $id ) { + try { + $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate note ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + // Ensure note ID is valid + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + // Ensure note ID is associated with given order + if ( $note->comment_post_ID != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); + } + + // Force delete since trashed order notes could not be managed through comments list table + $result = wc_delete_order_note( $note->comment_ID ); + + if ( ! $result ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_order_note', __( 'This order note cannot be deleted', 'woocommerce' ), 500 ); + } + + do_action( 'woocommerce_api_delete_order_note', $note->comment_ID, $order_id, $this ); + + return array( 'message' => __( 'Permanently deleted order note', 'woocommerce' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the order refunds for an order + * + * @since 2.2 + * @param string $order_id order ID + * @param string|null $fields fields to include in response + * @return array|WP_Error + */ + public function get_order_refunds( $order_id, $fields = null ) { + + // Ensure ID is valid order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $refund_items = wc_get_orders( array( + 'type' => 'shop_order_refund', + 'parent' => $order_id, + 'limit' => -1, + 'return' => 'ids', + ) ); + $order_refunds = array(); + + foreach ( $refund_items as $refund_id ) { + $order_refunds[] = current( $this->get_order_refund( $order_id, $refund_id, $fields ) ); + } + + return array( 'order_refunds' => apply_filters( 'woocommerce_api_order_refunds_response', $order_refunds, $order_id, $fields, $refund_items, $this ) ); + } + + /** + * Get an order refund for the given order ID and ID + * + * @since 2.2 + * + * @param string $order_id order ID + * @param int $id + * @param string|null $fields fields to limit response to + * @param array $filter + * + * @return array|WP_Error + */ + public function get_order_refund( $order_id, $id, $fields = null, $filter = array() ) { + try { + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + $order = wc_get_order( $order_id ); + $refund = wc_get_order( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + $line_items = array(); + + // Add line items + foreach ( $refund->get_items( 'line_item' ) as $item_id => $item ) { + $product = $item->get_product(); + $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; + $item_meta = $item->get_formatted_meta_data( $hideprefix ); + + foreach ( $item_meta as $key => $values ) { + $item_meta[ $key ]->label = $values->display_key; + unset( $item_meta[ $key ]->display_key ); + unset( $item_meta[ $key ]->display_value ); + } + + $line_items[] = array( + 'id' => $item_id, + 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ), + 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), 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, + 'meta' => array_values( $item_meta ), + 'refunded_item_id' => (int) $item->get_meta( 'refunded_item_id' ), + ); + } + + $order_refund = array( + 'id' => $refund->get_id(), + 'created_at' => $this->server->format_datetime( $refund->get_date_created() ? $refund->get_date_created()->getTimestamp() : 0, false, false ), + 'amount' => wc_format_decimal( $refund->get_amount(), 2 ), + 'reason' => $refund->get_reason(), + 'line_items' => $line_items, + ); + + return array( 'order_refund' => apply_filters( 'woocommerce_api_order_refund_response', $order_refund, $id, $fields, $refund, $order_id, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new order refund for the given order + * + * @since 2.2 + * @param string $order_id order ID + * @param array $data raw request data + * @param bool $api_refund do refund using a payment gateway API + * @return WP_Error|array error or created refund response data + */ + public function create_order_refund( $order_id, $data, $api_refund = true ) { + try { + if ( ! isset( $data['order_refund'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_refund' ), 400 ); + } + + $data = $data['order_refund']; + + // Permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_refund', __( 'You do not have permission to create order refunds', 'woocommerce' ), 401 ); + } + + $order_id = absint( $order_id ); + + if ( empty( $order_id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_create_order_refund_data', $data, $order_id, $this ); + + // Refund amount is required + if ( ! isset( $data['amount'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount is required.', 'woocommerce' ), 400 ); + } elseif ( 0 > $data['amount'] ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount must be positive.', 'woocommerce' ), 400 ); + } + + $data['order_id'] = $order_id; + $data['refund_id'] = 0; + + // Create the refund + $refund = wc_create_refund( $data ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); + } + + // Refund via API + if ( $api_refund ) { + if ( WC()->payment_gateways() ) { + $payment_gateways = WC()->payment_gateways->payment_gateways(); + } + + $order = wc_get_order( $order_id ); + + if ( isset( $payment_gateways[ $order->get_payment_method() ] ) && $payment_gateways[ $order->get_payment_method() ]->supports( 'refunds' ) ) { + $result = $payment_gateways[ $order->get_payment_method() ]->process_refund( $order_id, $refund->get_amount(), $refund->get_reason() ); + + if ( is_wp_error( $result ) ) { + return $result; + } elseif ( ! $result ) { + throw new WC_API_Exception( 'woocommerce_api_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ), 500 ); + } + } + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + do_action( 'woocommerce_api_create_order_refund', $refund->get_id(), $order_id, $this ); + + return $this->get_order_refund( $order_id, $refund->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit an order refund + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id refund ID + * @param array $data parsed request data + * @return WP_Error|array error or edited refund response data + */ + public function edit_order_refund( $order_id, $id, $data ) { + try { + if ( ! isset( $data['order_refund'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_refund' ), 400 ); + } + + $data = $data['order_refund']; + + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate refund ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + // Ensure order ID is valid + $refund = get_post( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + // Ensure refund ID is associated with given order + if ( $refund->post_parent != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_edit_order_refund_data', $data, $refund->ID, $order_id, $this ); + + // Update reason + if ( isset( $data['reason'] ) ) { + $updated_refund = wp_update_post( array( 'ID' => $refund->ID, 'post_excerpt' => $data['reason'] ) ); + + if ( is_wp_error( $updated_refund ) ) { + return $updated_refund; + } + } + + // Update refund amount + if ( isset( $data['amount'] ) && 0 < $data['amount'] ) { + update_post_meta( $refund->ID, '_refund_amount', wc_format_decimal( $data['amount'] ) ); + } + + do_action( 'woocommerce_api_edit_order_refund', $refund->ID, $order_id, $this ); + + return $this->get_order_refund( $order_id, $refund->ID ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete order refund + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id refund ID + * @return WP_Error|array error or deleted message + */ + public function delete_order_refund( $order_id, $id ) { + try { + $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate refund ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + // Ensure refund ID is valid + $refund = get_post( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + // Ensure refund ID is associated with given order + if ( $refund->post_parent != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); + } + + wc_delete_shop_order_transients( $order_id ); + + do_action( 'woocommerce_api_delete_order_refund', $refund->ID, $order_id, $this ); + + return $this->delete( $refund->ID, 'refund', true ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Bulk update or insert orders + * Accepts an array with orders in the formats supported by + * WC_API_Orders->create_order() and WC_API_Orders->edit_order() + * + * @since 2.4.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function bulk( $data ) { + + try { + if ( ! isset( $data['orders'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_orders_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'orders' ), 400 ); + } + + $data = $data['orders']; + $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'orders' ); + + // Limit bulk operation + if ( count( $data ) > $limit ) { + throw new WC_API_Exception( 'woocommerce_api_orders_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); + } + + $orders = array(); + + foreach ( $data as $_order ) { + $order_id = 0; + + // Try to get the order ID + if ( isset( $_order['id'] ) ) { + $order_id = intval( $_order['id'] ); + } + + if ( $order_id ) { + + // Order exists / edit order + $edit = $this->edit_order( $order_id, array( 'order' => $_order ) ); + + if ( is_wp_error( $edit ) ) { + $orders[] = array( + 'id' => $order_id, + 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), + ); + } else { + $orders[] = $edit['order']; + } + } else { + + // Order don't exists / create order + $new = $this->create_order( array( 'order' => $_order ) ); + + if ( is_wp_error( $new ) ) { + $orders[] = array( + 'id' => $order_id, + 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), + ); + } else { + $orders[] = $new['order']; + } + } + } + + return array( 'orders' => apply_filters( 'woocommerce_api_orders_bulk_response', $orders, $this ) ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/legacy/api/v3/class-wc-api-products.php b/includes/legacy/api/v3/class-wc-api-products.php new file mode 100644 index 00000000000..8f084074078 --- /dev/null +++ b/includes/legacy/api/v3/class-wc-api-products.php @@ -0,0 +1,3308 @@ + + * GET /products//reviews + * + * @since 2.1 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET/POST /products + $routes[ $this->base ] = array( + array( array( $this, 'get_products' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /products/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_products_count' ), WC_API_Server::READABLE ), + ); + + # GET/PUT/DELETE /products/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_product' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product' ), WC_API_Server::DELETABLE ), + ); + + # GET /products//reviews + $routes[ $this->base . '/(?P\d+)/reviews' ] = array( + array( array( $this, 'get_product_reviews' ), WC_API_Server::READABLE ), + ); + + # GET /products//orders + $routes[ $this->base . '/(?P\d+)/orders' ] = array( + array( array( $this, 'get_product_orders' ), WC_API_Server::READABLE ), + ); + + # GET/POST /products/categories + $routes[ $this->base . '/categories' ] = array( + array( array( $this, 'get_product_categories' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product_category' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET/PUT/DELETE /products/categories/ + $routes[ $this->base . '/categories/(?P\d+)' ] = array( + array( array( $this, 'get_product_category' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product_category' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product_category' ), WC_API_Server::DELETABLE ), + ); + + # GET/POST /products/tags + $routes[ $this->base . '/tags' ] = array( + array( array( $this, 'get_product_tags' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product_tag' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET/PUT/DELETE /products/tags/ + $routes[ $this->base . '/tags/(?P\d+)' ] = array( + array( array( $this, 'get_product_tag' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product_tag' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product_tag' ), WC_API_Server::DELETABLE ), + ); + + # GET/POST /products/shipping_classes + $routes[ $this->base . '/shipping_classes' ] = array( + array( array( $this, 'get_product_shipping_classes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product_shipping_class' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET/PUT/DELETE /products/shipping_classes/ + $routes[ $this->base . '/shipping_classes/(?P\d+)' ] = array( + array( array( $this, 'get_product_shipping_class' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product_shipping_class' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product_shipping_class' ), WC_API_Server::DELETABLE ), + ); + + # GET/POST /products/attributes + $routes[ $this->base . '/attributes' ] = array( + array( array( $this, 'get_product_attributes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product_attribute' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET/PUT/DELETE /products/attributes/ + $routes[ $this->base . '/attributes/(?P\d+)' ] = array( + array( array( $this, 'get_product_attribute' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product_attribute' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product_attribute' ), WC_API_Server::DELETABLE ), + ); + + # GET/POST /products/attributes//terms + $routes[ $this->base . '/attributes/(?P\d+)/terms' ] = array( + array( array( $this, 'get_product_attribute_terms' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product_attribute_term' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET/PUT/DELETE /products/attributes//terms/ + $routes[ $this->base . '/attributes/(?P\d+)/terms/(?P\d+)' ] = array( + array( array( $this, 'get_product_attribute_term' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product_attribute_term' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product_attribute_term' ), WC_API_Server::DELETABLE ), + ); + + # POST|PUT /products/bulk + $routes[ $this->base . '/bulk' ] = array( + array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + ); + + 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->get_parent_id() ) { + $product_data['parent'] = $this->get_product_data( $product->get_parent_id() ); + } + + // Add grouped products data + if ( $product->is_type( 'grouped' ) && $product->has_child() ) { + $product_data['grouped_products'] = $this->get_grouped_products_data( $product ); + } + + if ( $product->is_type( 'simple' ) ) { + $parent_id = $product->get_parent_id(); + if ( ! empty( $parent_id ) ) { + $_product = wc_get_product( $parent_id ); + $product_data['parent'] = $this->get_product_data( $_product ); + } + } + + return array( 'product' => apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields, $this->server ) ); + } + + /** + * Get the total number of products + * + * @since 2.1 + * + * @param string $type + * @param array $filter + * + * @return array|WP_Error + */ + public function get_products_count( $type = null, $filter = array() ) { + try { + if ( ! current_user_can( 'read_private_products' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), 401 ); + } + + if ( ! empty( $type ) ) { + $filter['type'] = $type; + } + + $query = $this->query_products( $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() ) ); + } + } + + /** + * Create a new product. + * + * @since 2.2 + * + * @param array $data posted data + * + * @return array|WP_Error + */ + public function create_product( $data ) { + $id = 0; + + try { + if ( ! isset( $data['product'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product' ), 400 ); + } + + $data = $data['product']; + + // Check permissions. + if ( ! current_user_can( 'publish_products' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product', __( 'You do not have permission to create products', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_product_data', $data, $this ); + + // Check if product title is specified. + if ( ! isset( $data['title'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_title', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'title' ), 400 ); + } + + // Check product type. + if ( ! isset( $data['type'] ) ) { + $data['type'] = 'simple'; + } + + // Set visible visibility when not sent. + if ( ! isset( $data['catalog_visibility'] ) ) { + $data['catalog_visibility'] = 'visible'; + } + + // Validate the product type. + if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 ); + } + + // Enable description html tags. + $post_content = isset( $data['description'] ) ? wc_clean( $data['description'] ) : ''; + if ( $post_content && isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) { + + $post_content = wp_filter_post_kses( $data['description'] ); + } + + // Enable short description html tags. + $post_excerpt = isset( $data['short_description'] ) ? wc_clean( $data['short_description'] ) : ''; + if ( $post_excerpt && isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) { + $post_excerpt = wp_filter_post_kses( $data['short_description'] ); + } + + $classname = WC_Product_Factory::get_classname_from_product_type( $data['type'] ); + if ( ! class_exists( $classname ) ) { + $classname = 'WC_Product_Simple'; + } + $product = new $classname(); + + $product->set_name( wc_clean( $data['title'] ) ); + $product->set_status( isset( $data['status'] ) ? wc_clean( $data['status'] ) : 'publish' ); + $product->set_short_description( isset( $data['short_description'] ) ? $post_excerpt : '' ); + $product->set_description( isset( $data['description'] ) ? $post_content : '' ); + $product->set_menu_order( isset( $data['menu_order'] ) ? intval( $data['menu_order'] ) : 0 ); + + if ( ! empty( $data['name'] ) ) { + $product->set_slug( sanitize_title( $data['name'] ) ); + } + + // Attempts to create the new product. + $product->save(); + $id = $product->get_id(); + + // Checks for an error in the product creation. + if ( 0 >= $id ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product', $id->get_error_message(), 400 ); + } + + // Check for featured/gallery images, upload it and set it. + if ( isset( $data['images'] ) ) { + $product = $this->save_product_images( $product, $data['images'] ); + } + + // Save product meta fields. + $product = $this->save_product_meta( $product, $data ); + $product->save(); + + // Save variations. + if ( isset( $data['type'] ) && 'variable' == $data['type'] && isset( $data['variations'] ) && is_array( $data['variations'] ) ) { + $this->save_variations( $product, $data ); + } + + do_action( 'woocommerce_api_create_product', $id, $data ); + + // Clear cache/transients. + wc_delete_product_transients( $id ); + + $this->server->send_status( 201 ); + + return $this->get_product( $id ); + } catch ( WC_Data_Exception $e ) { + $this->clear_product( $id ); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } catch ( WC_API_Exception $e ) { + $this->clear_product( $id ); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product + * + * @since 2.2 + * + * @param int $id the product ID + * @param array $data + * + * @return array|WP_Error + */ + public function edit_product( $id, $data ) { + try { + if ( ! isset( $data['product'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product' ), 400 ); + } + + $data = $data['product']; + + $id = $this->validate_request( $id, 'product', 'edit' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $product = wc_get_product( $id ); + + $data = apply_filters( 'woocommerce_api_edit_product_data', $data, $this ); + + // Product title. + if ( isset( $data['title'] ) ) { + $product->set_name( wc_clean( $data['title'] ) ); + } + + // Product name (slug). + if ( isset( $data['name'] ) ) { + $product->set_slug( wc_clean( $data['name'] ) ); + } + + // Product status. + if ( isset( $data['status'] ) ) { + $product->set_status( wc_clean( $data['status'] ) ); + } + + // Product short description. + if ( isset( $data['short_description'] ) ) { + // Enable short description html tags. + $post_excerpt = ( isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) ? wp_filter_post_kses( $data['short_description'] ) : wc_clean( $data['short_description'] ); + $product->set_short_description( $post_excerpt ); + } + + // Product description. + if ( isset( $data['description'] ) ) { + // Enable description html tags. + $post_content = ( isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) ? wp_filter_post_kses( $data['description'] ) : wc_clean( $data['description'] ); + $product->set_description( $post_content ); + } + + // Validate the product type. + if ( isset( $data['type'] ) && ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 ); + } + + // Menu order. + if ( isset( $data['menu_order'] ) ) { + $product->set_menu_order( intval( $data['menu_order'] ) ); + } + + // Check for featured/gallery images, upload it and set it. + if ( isset( $data['images'] ) ) { + $product = $this->save_product_images( $product, $data['images'] ); + } + + // Save product meta fields. + $product = $this->save_product_meta( $product, $data ); + + // Save variations. + if ( $product->is_type( 'variable' ) ) { + if ( isset( $data['variations'] ) && is_array( $data['variations'] ) ) { + $this->save_variations( $product, $data ); + } else { + // Just sync variations. + $product = WC_Product_Variable::sync( $product, false ); + } + } + + $product->save(); + + do_action( 'woocommerce_api_edit_product', $id, $data ); + + // Clear cache/transients. + wc_delete_product_transients( $id ); + + return $this->get_product( $id ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product. + * + * @since 2.2 + * + * @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; + } + + $product = wc_get_product( $id ); + + do_action( 'woocommerce_api_delete_product', $id, $this ); + + // If we're forcing, then delete permanently. + if ( $force ) { + if ( $product->is_type( 'variable' ) ) { + foreach ( $product->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! empty( $child ) ) { + $child->delete( true ); + } + } + } else { + // For other product types, if the product has children, remove the relationship. + foreach ( $product->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! empty( $child ) ) { + $child->set_parent_id( 0 ); + $child->save(); + } + } + } + + $product->delete( true ); + $result = ! ( $product->get_id() > 0 ); + } else { + $product->delete(); + $result = 'trash' === $product->get_status(); + } + + if ( ! $result ) { + return new WP_Error( 'woocommerce_api_cannot_delete_product', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product' ), array( 'status' => 500 ) ); + } + + // Delete parent product transients. + if ( $parent_id = wp_get_post_parent_id( $id ) ) { + wc_delete_product_transients( $parent_id ); + } + + if ( $force ) { + return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), 'product' ) ); + } else { + $this->server->send_status( '202' ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product' ) ); + } + } + + /** + * 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; + } + + $comments = get_approved_comments( $id ); + $reviews = array(); + + foreach ( $comments as $comment ) { + + $reviews[] = array( + 'id' => intval( $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 ) ); + } + + /** + * Get the orders for a product + * + * @since 2.4.0 + * @param int $id the product ID to get orders for + * @param string fields fields to retrieve + * @param array $filter filters to include in response + * @param string $status the order status to retrieve + * @param $page $page page to retrieve + * @return array|WP_Error + */ + public function get_product_orders( $id, $fields = null, $filter = array(), $status = null, $page = 1 ) { + global $wpdb; + + $id = $this->validate_request( $id, 'product', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $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' + ", $id ) ); + + if ( empty( $order_ids ) ) { + return array( 'orders' => array() ); + } + + $filter = array_merge( $filter, array( + 'in' => implode( ',', $order_ids ), + ) ); + + $orders = WC()->api->WC_API_Orders->get_orders( $fields, $filter, $status, $page ); + + return array( 'orders' => apply_filters( 'woocommerce_api_product_orders_response', $orders['orders'], $id, $filter, $fields, $this->server ) ); + } + + /** + * Get a listing of product categories + * + * @since 2.2 + * + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_categories( $fields = null ) { + try { + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); + } + + $product_categories = array(); + + $terms = get_terms( 'product_cat', array( 'hide_empty' => false, 'fields' => 'ids' ) ); + + foreach ( $terms as $term_id ) { + $product_categories[] = current( $this->get_product_category( $term_id, $fields ) ); + } + + return array( 'product_categories' => apply_filters( 'woocommerce_api_product_categories_response', $product_categories, $terms, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product category for the given ID + * + * @since 2.2 + * + * @param string $id product category term ID + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_category( $id, $fields = null ) { + try { + $id = absint( $id ); + + // Validate ID + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'Invalid product category ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); + } + + $term = get_term( $id, 'product_cat' ); + + if ( is_wp_error( $term ) || is_null( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'A product category with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $term_id = intval( $term->term_id ); + + // Get category display type + $display_type = get_term_meta( $term_id, 'display_type', true ); + + // Get category image + $image = ''; + if ( $image_id = get_term_meta( $term_id, 'thumbnail_id', true ) ) { + $image = wp_get_attachment_url( $image_id ); + } + + $product_category = array( + 'id' => $term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'parent' => $term->parent, + 'description' => $term->description, + 'display' => $display_type ? $display_type : 'default', + 'image' => $image ? esc_url( $image ) : '', + 'count' => intval( $term->count ), + ); + + return array( 'product_category' => apply_filters( 'woocommerce_api_product_category_response', $product_category, $id, $fields, $term, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new product category. + * + * @since 2.5.0 + * @param array $data Posted data + * @return array|WP_Error Product category if succeed, otherwise WP_Error + * will be returned + */ + public function create_product_category( $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_category'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_category_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_category' ), 400 ); + } + + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_category', __( 'You do not have permission to create product categories', 'woocommerce' ), 401 ); + } + + $defaults = array( + 'name' => '', + 'slug' => '', + 'description' => '', + 'parent' => 0, + 'display' => 'default', + 'image' => '', + ); + + $data = wp_parse_args( $data['product_category'], $defaults ); + $data = apply_filters( 'woocommerce_api_create_product_category_data', $data, $this ); + + // Check parent. + $data['parent'] = absint( $data['parent'] ); + if ( $data['parent'] ) { + $parent = get_term_by( 'id', $data['parent'], 'product_cat' ); + if ( ! $parent ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_parent', __( 'Product category parent is invalid', 'woocommerce' ), 400 ); + } + } + + // If value of image is numeric, assume value as image_id. + $image = $data['image']; + $image_id = 0; + if ( is_numeric( $image ) ) { + $image_id = absint( $image ); + } elseif ( ! empty( $image ) ) { + $upload = $this->upload_product_category_image( esc_url_raw( $image ) ); + $image_id = $this->set_product_category_image_as_attachment( $upload ); + } + + $insert = wp_insert_term( $data['name'], 'product_cat', $data ); + if ( is_wp_error( $insert ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_category', $insert->get_error_message(), 400 ); + } + + $id = $insert['term_id']; + + update_term_meta( $id, 'display_type', 'default' === $data['display'] ? '' : sanitize_text_field( $data['display'] ) ); + + // Check if image_id is a valid image attachment before updating the term meta. + if ( $image_id && wp_attachment_is_image( $image_id ) ) { + update_term_meta( $id, 'thumbnail_id', $image_id ); + } + + do_action( 'woocommerce_api_create_product_category', $id, $data ); + + $this->server->send_status( 201 ); + + return $this->get_product_category( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product category. + * + * @since 2.5.0 + * @param int $id Product category term ID + * @param array $data Posted data + * @return array|WP_Error Product category if succeed, otherwise WP_Error + * will be returned + */ + public function edit_product_category( $id, $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_category'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_category', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_category' ), 400 ); + } + + $id = absint( $id ); + $data = $data['product_category']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_category', __( 'You do not have permission to edit product categories', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_edit_product_category_data', $data, $this ); + $category = $this->get_product_category( $id ); + + if ( is_wp_error( $category ) ) { + return $category; + } + + if ( isset( $data['image'] ) ) { + $image_id = 0; + + // If value of image is numeric, assume value as image_id. + $image = $data['image']; + if ( is_numeric( $image ) ) { + $image_id = absint( $image ); + } elseif ( ! empty( $image ) ) { + $upload = $this->upload_product_category_image( esc_url_raw( $image ) ); + $image_id = $this->set_product_category_image_as_attachment( $upload ); + } + + // In case client supplies invalid image or wants to unset category image. + if ( ! wp_attachment_is_image( $image_id ) ) { + $image_id = ''; + } + } + + $update = wp_update_term( $id, 'product_cat', $data ); + if ( is_wp_error( $update ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_catgory', __( 'Could not edit the category', 'woocommerce' ), 400 ); + } + + if ( ! empty( $data['display'] ) ) { + update_term_meta( $id, 'display_type', 'default' === $data['display'] ? '' : sanitize_text_field( $data['display'] ) ); + } + + if ( isset( $image_id ) ) { + update_term_meta( $id, 'thumbnail_id', $image_id ); + } + + do_action( 'woocommerce_api_edit_product_category', $id, $data ); + + return $this->get_product_category( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product category. + * + * @since 2.5.0 + * @param int $id Product category term ID + * @return array|WP_Error Success message if succeed, otherwise WP_Error + * will be returned + */ + public function delete_product_category( $id ) { + global $wpdb; + + try { + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_category', __( 'You do not have permission to delete product category', 'woocommerce' ), 401 ); + } + + $id = absint( $id ); + $deleted = wp_delete_term( $id, 'product_cat' ); + if ( ! $deleted || is_wp_error( $deleted ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_category', __( 'Could not delete the category', 'woocommerce' ), 401 ); + } + + do_action( 'woocommerce_api_delete_product_category', $id, $this ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_category' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get a listing of product tags. + * + * @since 2.5.0 + * + * @param string|null $fields Fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_tags( $fields = null ) { + try { + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_tags', __( 'You do not have permission to read product tags', 'woocommerce' ), 401 ); + } + + $product_tags = array(); + + $terms = get_terms( 'product_tag', array( 'hide_empty' => false, 'fields' => 'ids' ) ); + + foreach ( $terms as $term_id ) { + $product_tags[] = current( $this->get_product_tag( $term_id, $fields ) ); + } + + return array( 'product_tags' => apply_filters( 'woocommerce_api_product_tags_response', $product_tags, $terms, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product tag for the given ID. + * + * @since 2.5.0 + * + * @param string $id Product tag term ID + * @param string|null $fields Fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_tag( $id, $fields = null ) { + try { + $id = absint( $id ); + + // Validate ID + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_tag_id', __( 'Invalid product tag ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_tags', __( 'You do not have permission to read product tags', 'woocommerce' ), 401 ); + } + + $term = get_term( $id, 'product_tag' ); + + if ( is_wp_error( $term ) || is_null( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_tag_id', __( 'A product tag with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $term_id = intval( $term->term_id ); + + $tag = array( + 'id' => $term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'description' => $term->description, + 'count' => intval( $term->count ), + ); + + return array( 'product_tag' => apply_filters( 'woocommerce_api_product_tag_response', $tag, $id, $fields, $term, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new product tag. + * + * @since 2.5.0 + * @param array $data Posted data + * @return array|WP_Error Product tag if succeed, otherwise WP_Error + * will be returned + */ + public function create_product_tag( $data ) { + try { + if ( ! isset( $data['product_tag'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_tag_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_tag' ), 400 ); + } + + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_tag', __( 'You do not have permission to create product tags', 'woocommerce' ), 401 ); + } + + $defaults = array( + 'name' => '', + 'slug' => '', + 'description' => '', + ); + + $data = wp_parse_args( $data['product_tag'], $defaults ); + $data = apply_filters( 'woocommerce_api_create_product_tag_data', $data, $this ); + + $insert = wp_insert_term( $data['name'], 'product_tag', $data ); + if ( is_wp_error( $insert ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_tag', $insert->get_error_message(), 400 ); + } + $id = $insert['term_id']; + + do_action( 'woocommerce_api_create_product_tag', $id, $data ); + + $this->server->send_status( 201 ); + + return $this->get_product_tag( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product tag. + * + * @since 2.5.0 + * @param int $id Product tag term ID + * @param array $data Posted data + * @return array|WP_Error Product tag if succeed, otherwise WP_Error + * will be returned + */ + public function edit_product_tag( $id, $data ) { + try { + if ( ! isset( $data['product_tag'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_tag', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_tag' ), 400 ); + } + + $id = absint( $id ); + $data = $data['product_tag']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_tag', __( 'You do not have permission to edit product tags', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_edit_product_tag_data', $data, $this ); + $tag = $this->get_product_tag( $id ); + + if ( is_wp_error( $tag ) ) { + return $tag; + } + + $update = wp_update_term( $id, 'product_tag', $data ); + if ( is_wp_error( $update ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_tag', __( 'Could not edit the tag', 'woocommerce' ), 400 ); + } + + do_action( 'woocommerce_api_edit_product_tag', $id, $data ); + + return $this->get_product_tag( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product tag. + * + * @since 2.5.0 + * @param int $id Product tag term ID + * @return array|WP_Error Success message if succeed, otherwise WP_Error + * will be returned + */ + public function delete_product_tag( $id ) { + try { + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_tag', __( 'You do not have permission to delete product tag', 'woocommerce' ), 401 ); + } + + $id = absint( $id ); + $deleted = wp_delete_term( $id, 'product_tag' ); + if ( ! $deleted || is_wp_error( $deleted ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_tag', __( 'Could not delete the tag', 'woocommerce' ), 401 ); + } + + do_action( 'woocommerce_api_delete_product_tag', $id, $this ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_tag' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * 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(), + ); + + // 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_arg_map = array( + 'product_type' => 'type', + 'product_cat' => 'category', + 'product_tag' => 'tag', + 'product_shipping_class' => 'shipping_class', + ); + + // Add attribute taxonomy names into the map. + foreach ( wc_get_attribute_taxonomy_names() as $attribute_name ) { + $taxonomies_arg_map[ $attribute_name ] = $attribute_name; + } + + // Set tax_query for each passed arg. + foreach ( $taxonomies_arg_map as $tax_name => $arg ) { + if ( ! empty( $args[ $arg ] ) ) { + $terms = explode( ',', $args[ $arg ] ); + + $tax_query[] = array( + 'taxonomy' => $tax_name, + 'field' => 'slug', + 'terms' => $terms, + ); + + unset( $args[ $arg ] ); + } + } + + if ( ! empty( $tax_query ) ) { + $query_args['tax_query'] = $tax_query; + } + + // Filter by specific sku + if ( ! empty( $args['sku'] ) ) { + if ( ! is_array( $query_args['meta_query'] ) ) { + $query_args['meta_query'] = array(); + } + + $query_args['meta_query'][] = array( + 'key' => '_sku', + 'value' => $args['sku'], + 'compare' => '=', + ); + + $query_args['post_type'] = array( 'product', 'product_variation' ); + } + + $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' => $product->get_price(), + 'regular_price' => $product->get_regular_price(), + 'sale_price' => $product->get_sale_price() ? $product->get_sale_price() : 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(), + 'product_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '', + 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '', + 'weight' => $product->get_weight() ? $product->get_weight() : 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' => wpautop( do_shortcode( $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() ), + 'parent_id' => $product->get_parent_id(), + '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' => wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ), + 'total_sales' => $product->get_total_sales(), + 'variations' => array(), + 'parent' => array(), + 'grouped_products' => array(), + 'menu_order' => $this->get_product_menu_order( $product ), + ); + } + + /** + * Get product menu order. + * + * @since 2.5.3 + * @param WC_Product $product + * @return int + */ + private function get_product_menu_order( $product ) { + $menu_order = $product->get_menu_order(); + + return apply_filters( 'woocommerce_api_product_menu_order', $menu_order, $product ); + } + + /** + * 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' => $variation->get_price(), + 'regular_price' => $variation->get_regular_price(), + 'sale_price' => $variation->get_sale_price() ? $variation->get_sale_price() : null, + 'taxable' => $variation->is_taxable(), + 'tax_status' => $variation->get_tax_status(), + 'tax_class' => $variation->get_tax_class(), + 'managing_stock' => $variation->managing_stock(), + 'stock_quantity' => $variation->get_stock_quantity(), + 'in_stock' => $variation->is_in_stock(), + 'backorders_allowed' => $variation->backorders_allowed(), + 'backordered' => $variation->is_on_backorder(), + 'purchaseable' => $variation->is_purchasable(), + 'visible' => $variation->variation_is_visible(), + 'on_sale' => $variation->is_on_sale(), + 'weight' => $variation->get_weight() ? $variation->get_weight() : 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 grouped products data + * + * @since 2.5.0 + * @param WC_Product $product + * + * @return array + */ + private function get_grouped_products_data( $product ) { + $products = array(); + + foreach ( $product->get_children() as $child_id ) { + $_product = wc_get_product( $child_id ); + + if ( ! $_product || ! $_product->exists() ) { + continue; + } + + $products[] = $this->get_product_data( $_product ); + + } + + return $products; + } + + /** + * Save default attributes. + * + * @since 3.0.0 + * + * @param WC_Product $product + * @param WP_REST_Request $request + * @return WC_Product + */ + protected function save_default_attributes( $product, $request ) { + // Update default attributes options setting. + if ( isset( $request['default_attribute'] ) ) { + $request['default_attributes'] = $request['default_attribute']; + } + + if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { + $attributes = $product->get_attributes(); + $default_attributes = array(); + + foreach ( $request['default_attributes'] as $default_attr_key => $default_attr ) { + if ( ! isset( $default_attr['name'] ) ) { + continue; + } + + $taxonomy = sanitize_title( $default_attr['name'] ); + + if ( isset( $default_attr['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $default_attr['slug'] ); + } + + if ( isset( $attributes[ $taxonomy ] ) ) { + $_attribute = $attributes[ $taxonomy ]; + + if ( $_attribute['is_variation'] ) { + $value = ''; + + if ( isset( $default_attr['option'] ) ) { + if ( $_attribute['is_taxonomy'] ) { + // Don't use wc_clean as it destroys sanitized characters + $value = sanitize_title( trim( stripslashes( $default_attr['option'] ) ) ); + } else { + $value = wc_clean( trim( stripslashes( $default_attr['option'] ) ) ); + } + } + + if ( $value ) { + $default_attributes[ $taxonomy ] = $value; + } + } + } + } + + $product->set_default_attributes( $default_attributes ); + } + + return $product; + } + + /** + * Save product meta. + * + * @since 2.2 + * @param WC_Product $product + * @param array $data + * @return WC_Product + * @throws WC_API_Exception + */ + protected function save_product_meta( $product, $data ) { + global $wpdb; + + // Virtual. + if ( isset( $data['virtual'] ) ) { + $product->set_virtual( $data['virtual'] ); + } + + // Tax status. + if ( isset( $data['tax_status'] ) ) { + $product->set_tax_status( wc_clean( $data['tax_status'] ) ); + } + + // Tax Class. + if ( isset( $data['tax_class'] ) ) { + $product->set_tax_class( wc_clean( $data['tax_class'] ) ); + } + + // Catalog Visibility. + if ( isset( $data['catalog_visibility'] ) ) { + $product->set_catalog_visibility( wc_clean( $data['catalog_visibility'] ) ); + } + + // Purchase Note. + if ( isset( $data['purchase_note'] ) ) { + $product->set_purchase_note( wc_clean( $data['purchase_note'] ) ); + } + + // Featured Product. + if ( isset( $data['featured'] ) ) { + $product->set_featured( $data['featured'] ); + } + + // Shipping data. + $product = $this->save_product_shipping_data( $product, $data ); + + // SKU. + if ( isset( $data['sku'] ) ) { + $sku = $product->get_sku(); + $new_sku = wc_clean( $data['sku'] ); + + if ( '' == $new_sku ) { + $product->set_sku( '' ); + } elseif ( $new_sku !== $sku ) { + if ( ! empty( $new_sku ) ) { + $unique_sku = wc_product_has_unique_sku( $product->get_id(), $new_sku ); + if ( ! $unique_sku ) { + throw new WC_API_Exception( 'woocommerce_api_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400 ); + } else { + $product->set_sku( $new_sku ); + } + } else { + $product->set_sku( '' ); + } + } + } + + // Attributes. + if ( isset( $data['attributes'] ) ) { + $attributes = array(); + + foreach ( $data['attributes'] as $attribute ) { + $is_taxonomy = 0; + $taxonomy = 0; + + if ( ! isset( $attribute['name'] ) ) { + continue; + } + + $attribute_slug = sanitize_title( $attribute['name'] ); + + if ( isset( $attribute['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); + $attribute_slug = sanitize_title( $attribute['slug'] ); + } + + if ( $taxonomy ) { + $is_taxonomy = 1; + } + + if ( $is_taxonomy ) { + + $attribute_id = wc_attribute_taxonomy_id_by_name( $attribute['name'] ); + + 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(); + } + + // Update post terms + if ( taxonomy_exists( $taxonomy ) ) { + wp_set_object_terms( $product->get_id(), $values, $taxonomy ); + } + + 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( $taxonomy ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? 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'] ) ) { + // Array based. + if ( is_array( $attribute['options'] ) ) { + $values = $attribute['options']; + + // Text based, separate by pipe. + } else { + $values = array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) ); + } + + // Custom attribute - Add attribute to array and set the values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_name( $attribute['name'] ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? 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; + } + } + + uasort( $attributes, 'wc_product_attribute_uasort_comparison' ); + + $product->set_attributes( $attributes ); + } + + // Sales and prices. + if ( in_array( $product->get_type(), array( 'variable', 'grouped' ) ) ) { + + // Variable and grouped products have no prices. + $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( $data['regular_price'] ) ) { + $regular_price = ( '' === $data['regular_price'] ) ? '' : $data['regular_price']; + $product->set_regular_price( $regular_price ); + } + + // Sale Price. + if ( isset( $data['sale_price'] ) ) { + $sale_price = ( '' === $data['sale_price'] ) ? '' : $data['sale_price']; + $product->set_sale_price( $sale_price ); + } + + if ( isset( $data['sale_price_dates_from'] ) ) { + $date_from = $data['sale_price_dates_from']; + } else { + $date_from = $product->get_date_on_sale_from() ? date( 'Y-m-d', $product->get_date_on_sale_from()->getTimestamp() ) : ''; + } + + if ( isset( $data['sale_price_dates_to'] ) ) { + $date_to = $data['sale_price_dates_to']; + } else { + $date_to = $product->get_date_on_sale_to() ? date( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ) : ''; + } + + if ( $date_to && ! $date_from ) { + $date_from = strtotime( 'NOW', current_time( 'timestamp', true ) ); + } + + $product->set_date_on_sale_to( $date_to ); + $product->set_date_on_sale_from( $date_from ); + + if ( $product->is_on_sale( 'edit' ) ) { + $product->set_price( $product->get_sale_price( 'edit' ) ); + } else { + $product->set_price( $product->get_regular_price( 'edit' ) ); + } + } + + // Product parent ID for groups. + if ( isset( $data['parent_id'] ) ) { + $product->set_parent_id( absint( $data['parent_id'] ) ); + } + + // Sold Individually. + if ( isset( $data['sold_individually'] ) ) { + $product->set_sold_individually( true === $data['sold_individually'] ? 'yes' : '' ); + } + + // Stock status. + if ( isset( $data['in_stock'] ) ) { + $stock_status = ( true === $data['in_stock'] ) ? 'instock' : 'outofstock'; + } else { + $stock_status = $product->get_stock_status(); + + if ( '' === $stock_status ) { + $stock_status = 'instock'; + } + } + + // Stock Data. + if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock. + if ( isset( $data['managing_stock'] ) ) { + $managing_stock = ( true === $data['managing_stock'] ) ? 'yes' : 'no'; + $product->set_manage_stock( $managing_stock ); + } else { + $managing_stock = $product->get_manage_stock() ? 'yes' : 'no'; + } + + // Backorders. + if ( isset( $data['backorders'] ) ) { + if ( 'notify' === $data['backorders'] ) { + $backorders = 'notify'; + } else { + $backorders = ( true === $data['backorders'] ) ? 'yes' : 'no'; + } + + $product->set_backorders( $backorders ); + } else { + $backorders = $product->get_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 ( 'yes' == $managing_stock ) { + $product->set_backorders( $backorders ); + + // 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( $data['stock_quantity'] ) ) { + $product->set_stock_quantity( wc_stock_amount( $data['stock_quantity'] ) ); + } elseif ( isset( $data['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $data['inventory_delta'] ); + $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); + } + } else { + // Don't manage stock. + $product->set_manage_stock( 'no' ); + $product->set_backorders( $backorders ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } + } elseif ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Upsells. + if ( isset( $data['upsell_ids'] ) ) { + $upsells = array(); + $ids = $data['upsell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $upsells[] = $id; + } + } + + $product->set_upsell_ids( $upsells ); + } else { + $product->set_upsell_ids( array() ); + } + } + + // Cross sells. + if ( isset( $data['cross_sell_ids'] ) ) { + $crosssells = array(); + $ids = $data['cross_sell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $crosssells[] = $id; + } + } + + $product->set_cross_sell_ids( $crosssells ); + } else { + $product->set_cross_sell_ids( array() ); + } + } + + // Product categories. + if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) { + $product->set_category_ids( $data['categories'] ); + } + + // Product tags. + if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) { + $product->set_tag_ids( $data['tags'] ); + } + + // Downloadable. + if ( isset( $data['downloadable'] ) ) { + $is_downloadable = ( true === $data['downloadable'] ) ? 'yes' : 'no'; + $product->set_downloadable( $is_downloadable ); + } else { + $is_downloadable = $product->get_downloadable() ? 'yes' : 'no'; + } + + // Downloadable options. + if ( 'yes' == $is_downloadable ) { + + // Downloadable files. + if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { + $product = $this->save_downloadable_files( $product, $data['downloads'] ); + } + + // Download limit. + if ( isset( $data['download_limit'] ) ) { + $product->set_download_limit( $data['download_limit'] ); + } + + // Download expiry. + if ( isset( $data['download_expiry'] ) ) { + $product->set_download_expiry( $data['download_expiry'] ); + } + } + + // Product url. + if ( $product->is_type( 'external' ) ) { + if ( isset( $data['product_url'] ) ) { + $product->set_product_url( $data['product_url'] ); + } + + if ( isset( $data['button_text'] ) ) { + $product->set_button_text( $data['button_text'] ); + } + } + + // Reviews allowed. + if ( isset( $data['reviews_allowed'] ) ) { + $product->set_reviews_allowed( $data['reviews_allowed'] ); + } + + // Save default attributes for variable products. + if ( $product->is_type( 'variable' ) ) { + $product = $this->save_default_attributes( $product, $data ); + } + + // Do action for product type + do_action( 'woocommerce_api_process_product_meta_' . $product->get_type(), $product->get_id(), $data ); + + return $product; + } + + /** + * Save variations. + * + * @since 2.2 + * + * @param WC_Product $product + * @param array $request + * + * @return bool + * @throws WC_API_Exception + */ + protected function save_variations( $product, $request ) { + global $wpdb; + + $id = $product->get_id(); + $variations = $request['variations']; + $attributes = $product->get_attributes(); + + foreach ( $variations as $menu_order => $data ) { + $variation_id = isset( $data['id'] ) ? absint( $data['id'] ) : 0; + $variation = new WC_Product_Variation( $variation_id ); + + // 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 = current( $data['image'] ); + if ( is_array( $image ) ) { + $image['position'] = 0; + } + + $variation = $this->save_product_images( $variation, array( $image ) ); + } + + // Virtual variation. + if ( isset( $data['virtual'] ) ) { + $variation->set_virtual( $data['virtual'] ); + } + + // Downloadable variation. + if ( isset( $data['downloadable'] ) ) { + $is_downloadable = $data['downloadable']; + $variation->set_downloadable( $is_downloadable ); + } else { + $is_downloadable = $variation->get_downloadable(); + } + + // Downloads. + if ( $is_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. + $manage_stock = (bool) $variation->get_manage_stock(); + if ( isset( $data['managing_stock'] ) ) { + $manage_stock = $data['managing_stock']; + } + $variation->set_manage_stock( $manage_stock ); + + $stock_status = $variation->get_stock_status(); + if ( isset( $data['in_stock'] ) ) { + $stock_status = true === $data['in_stock'] ? 'instock' : 'outofstock'; + } + $variation->set_stock_status( $stock_status ); + + $backorders = $variation->get_backorders(); + if ( isset( $data['backorders'] ) ) { + $backorders = $data['backorders']; + } + $variation->set_backorders( $backorders ); + + if ( $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['sale_price_dates_from'] ) ) { + $variation->set_date_on_sale_from( $data['sale_price_dates_from'] ); + } + + if ( isset( $data['sale_price_dates_to'] ) ) { + $variation->set_date_on_sale_to( $data['sale_price_dates_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(); + + foreach ( $data['attributes'] as $attribute_key => $attribute ) { + if ( ! isset( $attribute['name'] ) ) { + continue; + } + + $taxonomy = 0; + $_attribute = array(); + + if ( isset( $attribute['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); + } + + if ( ! $taxonomy ) { + $taxonomy = sanitize_title( $attribute['name'] ); + } + + if ( isset( $attributes[ $taxonomy ] ) ) { + $_attribute = $attributes[ $taxonomy ]; + } + + if ( isset( $_attribute['is_variation'] ) && $_attribute['is_variation'] ) { + $_attribute_key = sanitize_title( $_attribute['name'] ); + + if ( isset( $_attribute['is_taxonomy'] ) && $_attribute['is_taxonomy'] ) { + // Don't use wc_clean as it destroys sanitized characters. + $_attribute_value = isset( $attribute['option'] ) ? sanitize_title( stripslashes( $attribute['option'] ) ) : ''; + } else { + $_attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + } + + $_attributes[ $_attribute_key ] = $_attribute_value; + } + } + + $variation->set_attributes( $_attributes ); + } + + $variation->save(); + + do_action( 'woocommerce_api_save_product_variation', $variation_id, $menu_order, $variation ); + } + + return true; + } + + /** + * Save product shipping data + * + * @since 2.2 + * @param WC_Product $product + * @param array $data + * @return WC_Product + */ + private function save_product_shipping_data( $product, $data ) { + if ( isset( $data['weight'] ) ) { + $product->set_weight( '' === $data['weight'] ? '' : wc_format_decimal( $data['weight'] ) ); + } + + // Product dimensions + if ( isset( $data['dimensions'] ) ) { + // Height + if ( isset( $data['dimensions']['height'] ) ) { + $product->set_height( '' === $data['dimensions']['height'] ? '' : wc_format_decimal( $data['dimensions']['height'] ) ); + } + + // Width + if ( isset( $data['dimensions']['width'] ) ) { + $product->set_width( '' === $data['dimensions']['width'] ? '' : wc_format_decimal( $data['dimensions']['width'] ) ); + } + + // Length + if ( isset( $data['dimensions']['length'] ) ) { + $product->set_length( '' === $data['dimensions']['length'] ? '' : wc_format_decimal( $data['dimensions']['length'] ) ); + } + } + + // Virtual + if ( isset( $data['virtual'] ) ) { + $virtual = ( true === $data['virtual'] ) ? 'yes' : 'no'; + + if ( 'yes' == $virtual ) { + $product->set_weight( '' ); + $product->set_height( '' ); + $product->set_length( '' ); + $product->set_width( '' ); + } + } + + // Shipping class + if ( isset( $data['shipping_class'] ) ) { + $data_store = $product->get_data_store(); + $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) ); + $product->set_shipping_class_id( $shipping_class_id ); + } + + return $product; + } + + /** + * Save downloadable files + * + * @since 2.2 + * @param WC_Product $product + * @param array $downloads + * @param int $deprecated Deprecated since 3.0. + * @return WC_Product + */ + private function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { + if ( $deprecated ) { + wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() does not require a variation_id anymore.' ); + } + + $files = array(); + foreach ( $downloads as $key => $file ) { + if ( isset( $file['url'] ) ) { + $file['file'] = $file['url']; + } + + if ( empty( $file['file'] ) ) { + continue; + } + + $download = new WC_Product_Download(); + $download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() ); + $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); + $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); + $files[] = $download; + } + $product->set_downloads( $files ); + + return $product; + } + + /** + * Get attribute taxonomy by slug. + * + * @since 2.2 + * @param string $slug + * @return string|null + */ + private function get_attribute_taxonomy_by_slug( $slug ) { + $taxonomy = null; + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + foreach ( $attribute_taxonomies as $key => $tax ) { + if ( $slug == $tax->attribute_name ) { + $taxonomy = 'pa_' . $tax->attribute_name; + + break; + } + } + + return $taxonomy; + } + + /** + * 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' => (int) $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; + } + + /** + * Save product images. + * + * @since 2.2 + * @param WC_Product $product + * @param array $images + * @throws WC_API_Exception + * @return WC_Product + */ + protected function save_product_images( $product, $images ) { + if ( is_array( $images ) ) { + $gallery = array(); + + foreach ( $images as $image ) { + if ( isset( $image['position'] ) && 0 == $image['position'] ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 ); + } + + $attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() ); + } + + $product->set_image_id( $attachment_id ); + } else { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 ); + } + + $attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() ); + } + + $gallery[] = $attachment_id; + } + + // Set the image alt if present. + if ( ! empty( $image['alt'] ) && $attachment_id ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image title if present. + if ( ! empty( $image['title'] ) && $attachment_id ) { + wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['title'] ) ); + } + } + + if ( ! empty( $gallery ) ) { + $product->set_gallery_image_ids( $gallery ); + } + } else { + $product->set_image_id( '' ); + $product->set_gallery_image_ids( array() ); + } + + return $product; + } + + /** + * Upload image from URL + * + * @since 2.2 + * @param string $image_url + * @return int|WP_Error attachment id + */ + public function upload_product_image( $image_url ) { + return $this->upload_image_from_url( $image_url, 'product_image' ); + } + + /** + * Upload product category image from URL. + * + * @since 2.5.0 + * @param string $image_url + * @return int|WP_Error attachment id + */ + public function upload_product_category_image( $image_url ) { + return $this->upload_image_from_url( $image_url, 'product_category_image' ); + } + + /** + * Upload image from URL. + * + * @throws WC_API_Exception + * + * @since 2.5.0 + * @param string $image_url + * @param string $upload_for + * @return array + */ + protected function upload_image_from_url( $image_url, $upload_for = 'product_image' ) { + $upload = wc_rest_upload_image_from_url( $image_url ); + if ( is_wp_error( $upload ) ) { + throw new WC_API_Exception( 'woocommerce_api_' . $upload_for . '_upload_error', $upload->get_error_message(), 400 ); + } + + do_action( 'woocommerce_api_uploaded_image_from_url', $upload, $image_url, $upload_for ); + + return $upload; + } + + /** + * Sets product image as attachment and returns the attachment ID. + * + * @since 2.2 + * @param array $upload + * @param int $id + * @return int + */ + protected function set_product_image_as_attachment( $upload, $id ) { + return $this->set_uploaded_image_as_attachment( $upload, $id ); + } + + /** + * Sets uploaded category image as attachment and returns the attachment ID. + * + * @since 2.5.0 + * @param integer $upload Upload information from wp_upload_bits + * @return int Attachment ID + */ + protected function set_product_category_image_as_attachment( $upload ) { + return $this->set_uploaded_image_as_attachment( $upload ); + } + + /** + * Set uploaded image as attachment. + * + * @since 2.5.0 + * @param array $upload Upload information from wp_upload_bits + * @param int $id Post ID. Default to 0. + * @return int Attachment ID + */ + protected function set_uploaded_image_as_attachment( $upload, $id = 0 ) { + $info = wp_check_filetype( $upload['file'] ); + $title = ''; + $content = ''; + + if ( $image_meta = @wp_read_image_metadata( $upload['file'] ) ) { + if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { + $title = wc_clean( $image_meta['title'] ); + } + if ( trim( $image_meta['caption'] ) ) { + $content = wc_clean( $image_meta['caption'] ); + } + } + + $attachment = array( + 'post_mime_type' => $info['type'], + 'guid' => $upload['url'], + 'post_parent' => $id, + 'post_title' => $title, + 'post_content' => $content, + ); + + $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id ); + if ( ! is_wp_error( $attachment_id ) ) { + wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) ); + } + + return $attachment_id; + } + + /** + * 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' => wc_attribute_label( str_replace( 'attribute_', '', $attribute_name ), $product ), + 'slug' => str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $attribute_name ) ), + 'option' => $attribute, + ); + } + } else { + + foreach ( $product->get_attributes() as $attribute ) { + $attributes[] = array( + 'name' => wc_attribute_label( $attribute['name'], $product ), + 'slug' => wc_attribute_taxonomy_slug( $attribute['name'] ), + 'position' => (int) $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; + } + + /** + * Get a listing of product attributes + * + * @since 2.5.0 + * + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_attributes( $fields = null ) { + try { + // Permissions check. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 ); + } + + $product_attributes = array(); + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + foreach ( $attribute_taxonomies as $attribute ) { + $product_attributes[] = array( + 'id' => intval( $attribute->attribute_id ), + 'name' => $attribute->attribute_label, + 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ), + 'type' => $attribute->attribute_type, + 'order_by' => $attribute->attribute_orderby, + 'has_archives' => (bool) $attribute->attribute_public, + ); + } + + return array( 'product_attributes' => apply_filters( 'woocommerce_api_product_attributes_response', $product_attributes, $attribute_taxonomies, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product attribute for the given ID + * + * @since 2.5.0 + * + * @param string $id product attribute term ID + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_attribute( $id, $fields = null ) { + global $wpdb; + + try { + $id = absint( $id ); + + // Validate ID + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 ); + } + + $attribute = $wpdb->get_row( $wpdb->prepare( " + SELECT * + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_id = %d + ", $id ) ); + + if ( is_wp_error( $attribute ) || is_null( $attribute ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $product_attribute = array( + 'id' => intval( $attribute->attribute_id ), + 'name' => $attribute->attribute_label, + 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ), + 'type' => $attribute->attribute_type, + 'order_by' => $attribute->attribute_orderby, + 'has_archives' => (bool) $attribute->attribute_public, + ); + + return array( 'product_attribute' => apply_filters( 'woocommerce_api_product_attribute_response', $product_attribute, $id, $fields, $attribute, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Validate attribute data. + * + * @since 2.5.0 + * @param string $name + * @param string $slug + * @param string $type + * @param string $order_by + * @param bool $new_data + * @return bool + * @throws WC_API_Exception + */ + protected function validate_attribute_data( $name, $slug, $type, $order_by, $new_data = true ) { + if ( empty( $name ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 ); + } + + if ( strlen( $slug ) >= 28 ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), 400 ); + } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), 400 ); + } elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), 400 ); + } + + // Validate the attribute type + if ( ! in_array( wc_clean( $type ), array_keys( wc_get_attribute_types() ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_type', sprintf( __( 'Invalid product attribute type - the product attribute type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_attribute_types() ) ) ), 400 ); + } + + // Validate the attribute order by + if ( ! in_array( wc_clean( $order_by ), array( 'menu_order', 'name', 'name_num', 'id' ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_order_by', sprintf( __( 'Invalid product attribute order_by type - the product attribute order_by type must be any of these: %s', 'woocommerce' ), implode( ', ', array( 'menu_order', 'name', 'name_num', 'id' ) ) ), 400 ); + } + + return true; + } + + /** + * Create a new product attribute. + * + * @since 2.5.0 + * + * @param array $data Posted data. + * + * @return array|WP_Error + */ + public function create_product_attribute( $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_attribute'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute' ), 400 ); + } + + $data = $data['product_attribute']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_product_attribute_data', $data, $this ); + + if ( ! isset( $data['name'] ) ) { + $data['name'] = ''; + } + + // Set the attribute slug. + if ( ! isset( $data['slug'] ) ) { + $data['slug'] = wc_sanitize_taxonomy_name( stripslashes( $data['name'] ) ); + } else { + $data['slug'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ) ); + } + + // Set attribute type when not sent. + if ( ! isset( $data['type'] ) ) { + $data['type'] = 'select'; + } + + // Set order by when not sent. + if ( ! isset( $data['order_by'] ) ) { + $data['order_by'] = 'menu_order'; + } + + // Validate the attribute data. + $this->validate_attribute_data( $data['name'], $data['slug'], $data['type'], $data['order_by'], true ); + + $insert = $wpdb->insert( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + array( + 'attribute_label' => $data['name'], + 'attribute_name' => $data['slug'], + 'attribute_type' => $data['type'], + 'attribute_orderby' => $data['order_by'], + 'attribute_public' => isset( $data['has_archives'] ) && true === $data['has_archives'] ? 1 : 0, + ), + array( '%s', '%s', '%s', '%s', '%d' ) + ); + + // Checks for an error in the product creation. + if ( is_wp_error( $insert ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $insert->get_error_message(), 400 ); + } + + $id = $wpdb->insert_id; + + do_action( 'woocommerce_api_create_product_attribute', $id, $data ); + + // Clear transients. + wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); + delete_transient( 'wc_attribute_taxonomies' ); + WC_Cache_Helper::incr_cache_prefix( 'woocommerce-attributes' ); + + $this->server->send_status( 201 ); + + return $this->get_product_attribute( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product attribute. + * + * @since 2.5.0 + * + * @param int $id the attribute ID. + * @param array $data + * + * @return array|WP_Error + */ + public function edit_product_attribute( $id, $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_attribute'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute' ), 400 ); + } + + $id = absint( $id ); + $data = $data['product_attribute']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_edit_product_attribute_data', $data, $this ); + $attribute = $this->get_product_attribute( $id ); + + if ( is_wp_error( $attribute ) ) { + return $attribute; + } + + $attribute_name = isset( $data['name'] ) ? $data['name'] : $attribute['product_attribute']['name']; + $attribute_type = isset( $data['type'] ) ? $data['type'] : $attribute['product_attribute']['type']; + $attribute_order_by = isset( $data['order_by'] ) ? $data['order_by'] : $attribute['product_attribute']['order_by']; + + if ( isset( $data['slug'] ) ) { + $attribute_slug = wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ); + } else { + $attribute_slug = $attribute['product_attribute']['slug']; + } + $attribute_slug = preg_replace( '/^pa\_/', '', $attribute_slug ); + + if ( isset( $data['has_archives'] ) ) { + $attribute_public = true === $data['has_archives'] ? 1 : 0; + } else { + $attribute_public = $attribute['product_attribute']['has_archives']; + } + + // Validate the attribute data. + $this->validate_attribute_data( $attribute_name, $attribute_slug, $attribute_type, $attribute_order_by, false ); + + $update = $wpdb->update( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + array( + 'attribute_label' => $attribute_name, + 'attribute_name' => $attribute_slug, + 'attribute_type' => $attribute_type, + 'attribute_orderby' => $attribute_order_by, + 'attribute_public' => $attribute_public, + ), + array( 'attribute_id' => $id ), + array( '%s', '%s', '%s', '%s', '%d' ), + array( '%d' ) + ); + + // Checks for an error in the product creation. + if ( false === $update ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute', __( 'Could not edit the attribute', 'woocommerce' ), 400 ); + } + + do_action( 'woocommerce_api_edit_product_attribute', $id, $data ); + + // Clear transients. + wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); + delete_transient( 'wc_attribute_taxonomies' ); + WC_Cache_Helper::incr_cache_prefix( 'woocommerce-attributes' ); + + return $this->get_product_attribute( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product attribute. + * + * @since 2.5.0 + * + * @param int $id the product attribute ID. + * + * @return array|WP_Error + */ + public function delete_product_attribute( $id ) { + global $wpdb; + + try { + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute', __( 'You do not have permission to delete product attributes', 'woocommerce' ), 401 ); + } + + $id = absint( $id ); + + $attribute_name = $wpdb->get_var( $wpdb->prepare( " + SELECT attribute_name + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_id = %d + ", $id ) ); + + if ( is_null( $attribute_name ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $deleted = $wpdb->delete( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + array( 'attribute_id' => $id ), + array( '%d' ) + ); + + if ( false === $deleted ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute', __( 'Could not delete the attribute', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name( $attribute_name ); + + if ( taxonomy_exists( $taxonomy ) ) { + $terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' ); + foreach ( $terms as $term ) { + wp_delete_term( $term->term_id, $taxonomy ); + } + } + + do_action( 'woocommerce_attribute_deleted', $id, $attribute_name, $taxonomy ); + do_action( 'woocommerce_api_delete_product_attribute', $id, $this ); + + // Clear transients. + wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); + delete_transient( 'wc_attribute_taxonomies' ); + WC_Cache_Helper::incr_cache_prefix( 'woocommerce-attributes' ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get a listing of product attribute terms. + * + * @since 2.5.0 + * + * @param int $attribute_id Attribute ID. + * @param string|null $fields Fields to limit response to. + * + * @return array|WP_Error + */ + public function get_product_attribute_terms( $attribute_id, $fields = null ) { + try { + // Permissions check. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attribute_terms', __( 'You do not have permission to read product attribute terms', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); + + if ( ! $taxonomy ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $terms = get_terms( $taxonomy, array( 'hide_empty' => false ) ); + $attribute_terms = array(); + + foreach ( $terms as $term ) { + $attribute_terms[] = array( + 'id' => $term->term_id, + 'slug' => $term->slug, + 'name' => $term->name, + 'count' => $term->count, + ); + } + + return array( 'product_attribute_terms' => apply_filters( 'woocommerce_api_product_attribute_terms_response', $attribute_terms, $terms, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product attribute term for the given ID. + * + * @since 2.5.0 + * + * @param int $attribute_id Attribute ID. + * @param string $id Product attribute term ID. + * @param string|null $fields Fields to limit response to. + * + * @return array|WP_Error + */ + public function get_product_attribute_term( $attribute_id, $id, $fields = null ) { + global $wpdb; + + try { + $id = absint( $id ); + + // Validate ID + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_term_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attribute_terms', __( 'You do not have permission to read product attribute terms', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); + + if ( ! $taxonomy ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $term = get_term( $id, $taxonomy ); + + if ( is_wp_error( $term ) || is_null( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_term_id', __( 'A product attribute term with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $attribute_term = array( + 'id' => $term->term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'count' => $term->count, + ); + + return array( 'product_attribute_term' => apply_filters( 'woocommerce_api_product_attribute_response', $attribute_term, $id, $fields, $term, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new product attribute term. + * + * @since 2.5.0 + * + * @param int $attribute_id Attribute ID. + * @param array $data Posted data. + * + * @return array|WP_Error + */ + public function create_product_attribute_term( $attribute_id, $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_attribute_term'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute_term' ), 400 ); + } + + $data = $data['product_attribute_term']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); + + if ( ! $taxonomy ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $data = apply_filters( 'woocommerce_api_create_product_attribute_term_data', $data, $this ); + + // Check if attribute term name is specified. + if ( ! isset( $data['name'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 ); + } + + $args = array(); + + // Set the attribute term slug. + if ( isset( $data['slug'] ) ) { + $args['slug'] = sanitize_title( wp_unslash( $data['slug'] ) ); + } + + $term = wp_insert_term( $data['name'], $taxonomy, $args ); + + // Checks for an error in the term creation. + if ( is_wp_error( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $term->get_error_message(), 400 ); + } + + $id = $term['term_id']; + + do_action( 'woocommerce_api_create_product_attribute_term', $id, $data ); + + $this->server->send_status( 201 ); + + return $this->get_product_attribute_term( $attribute_id, $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product attribute term. + * + * @since 2.5.0 + * + * @param int $attribute_id Attribute ID. + * @param int $id the attribute ID. + * @param array $data + * + * @return array|WP_Error + */ + public function edit_product_attribute_term( $attribute_id, $id, $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_attribute_term'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute_term' ), 400 ); + } + + $id = absint( $id ); + $data = $data['product_attribute_term']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); + + if ( ! $taxonomy ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $data = apply_filters( 'woocommerce_api_edit_product_attribute_term_data', $data, $this ); + + $args = array(); + + // Update name. + if ( isset( $data['name'] ) ) { + $args['name'] = wc_clean( wp_unslash( $data['name'] ) ); + } + + // Update slug. + if ( isset( $data['slug'] ) ) { + $args['slug'] = sanitize_title( wp_unslash( $data['slug'] ) ); + } + + $term = wp_update_term( $id, $taxonomy, $args ); + + if ( is_wp_error( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute_term', $term->get_error_message(), 400 ); + } + + do_action( 'woocommerce_api_edit_product_attribute_term', $id, $data ); + + return $this->get_product_attribute_term( $attribute_id, $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product attribute term. + * + * @since 2.5.0 + * + * @param int $attribute_id Attribute ID. + * @param int $id the product attribute ID. + * + * @return array|WP_Error + */ + public function delete_product_attribute_term( $attribute_id, $id ) { + global $wpdb; + + try { + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute_term', __( 'You do not have permission to delete product attribute terms', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); + + if ( ! $taxonomy ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $id = absint( $id ); + $term = wp_delete_term( $id, $taxonomy ); + + if ( ! $term ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute_term', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product_attribute_term' ), 500 ); + } elseif ( is_wp_error( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute_term', $term->get_error_message(), 400 ); + } + + do_action( 'woocommerce_api_delete_product_attribute_term', $id, $this ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Clear product + * + * @param int $product_id + */ + protected function clear_product( $product_id ) { + if ( ! is_numeric( $product_id ) || 0 >= $product_id ) { + return; + } + + // Delete product attachments + $attachments = get_children( array( + 'post_parent' => $product_id, + 'post_status' => 'any', + 'post_type' => 'attachment', + ) ); + + foreach ( (array) $attachments as $attachment ) { + wp_delete_attachment( $attachment->ID, true ); + } + + // Delete product + $product = wc_get_product( $product_id ); + $product->delete( true ); + } + + /** + * Bulk update or insert products + * Accepts an array with products in the formats supported by + * WC_API_Products->create_product() and WC_API_Products->edit_product() + * + * @since 2.4.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function bulk( $data ) { + + try { + if ( ! isset( $data['products'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_products_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'products' ), 400 ); + } + + $data = $data['products']; + $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'products' ); + + // Limit bulk operation + if ( count( $data ) > $limit ) { + throw new WC_API_Exception( 'woocommerce_api_products_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); + } + + $products = array(); + + foreach ( $data as $_product ) { + $product_id = 0; + $product_sku = ''; + + // Try to get the product ID + if ( isset( $_product['id'] ) ) { + $product_id = intval( $_product['id'] ); + } + + if ( ! $product_id && isset( $_product['sku'] ) ) { + $product_sku = wc_clean( $_product['sku'] ); + $product_id = wc_get_product_id_by_sku( $product_sku ); + } + + if ( $product_id ) { + + // Product exists / edit product + $edit = $this->edit_product( $product_id, array( 'product' => $_product ) ); + + if ( is_wp_error( $edit ) ) { + $products[] = array( + 'id' => $product_id, + 'sku' => $product_sku, + 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), + ); + } else { + $products[] = $edit['product']; + } + } else { + + // Product don't exists / create product + $new = $this->create_product( array( 'product' => $_product ) ); + + if ( is_wp_error( $new ) ) { + $products[] = array( + 'id' => $product_id, + 'sku' => $product_sku, + 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), + ); + } else { + $products[] = $new['product']; + } + } + } + + return array( 'products' => apply_filters( 'woocommerce_api_products_bulk_response', $products, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get a listing of product shipping classes. + * + * @since 2.5.0 + * @param string|null $fields Fields to limit response to + * @return array|WP_Error List of product shipping classes if succeed, + * otherwise WP_Error will be returned + */ + public function get_product_shipping_classes( $fields = null ) { + try { + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_shipping_classes', __( 'You do not have permission to read product shipping classes', 'woocommerce' ), 401 ); + } + + $product_shipping_classes = array(); + + $terms = get_terms( 'product_shipping_class', array( 'hide_empty' => false, 'fields' => 'ids' ) ); + + foreach ( $terms as $term_id ) { + $product_shipping_classes[] = current( $this->get_product_shipping_class( $term_id, $fields ) ); + } + + return array( 'product_shipping_classes' => apply_filters( 'woocommerce_api_product_shipping_classes_response', $product_shipping_classes, $terms, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product shipping class for the given ID. + * + * @since 2.5.0 + * @param string $id Product shipping class term ID + * @param string|null $fields Fields to limit response to + * @return array|WP_Error Product shipping class if succeed, otherwise + * WP_Error will be returned + */ + public function get_product_shipping_class( $id, $fields = null ) { + try { + $id = absint( $id ); + if ( ! $id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_id', __( 'Invalid product shipping class ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_shipping_classes', __( 'You do not have permission to read product shipping classes', 'woocommerce' ), 401 ); + } + + $term = get_term( $id, 'product_shipping_class' ); + + if ( is_wp_error( $term ) || is_null( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_id', __( 'A product shipping class with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $term_id = intval( $term->term_id ); + + $product_shipping_class = array( + 'id' => $term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'parent' => $term->parent, + 'description' => $term->description, + 'count' => intval( $term->count ), + ); + + return array( 'product_shipping_class' => apply_filters( 'woocommerce_api_product_shipping_class_response', $product_shipping_class, $id, $fields, $term, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new product shipping class. + * + * @since 2.5.0 + * @param array $data Posted data + * @return array|WP_Error Product shipping class if succeed, otherwise + * WP_Error will be returned + */ + public function create_product_shipping_class( $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_shipping_class'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_shipping_class_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_shipping_class' ), 400 ); + } + + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_shipping_class', __( 'You do not have permission to create product shipping classes', 'woocommerce' ), 401 ); + } + + $defaults = array( + 'name' => '', + 'slug' => '', + 'description' => '', + 'parent' => 0, + ); + + $data = wp_parse_args( $data['product_shipping_class'], $defaults ); + $data = apply_filters( 'woocommerce_api_create_product_shipping_class_data', $data, $this ); + + // Check parent. + $data['parent'] = absint( $data['parent'] ); + if ( $data['parent'] ) { + $parent = get_term_by( 'id', $data['parent'], 'product_shipping_class' ); + if ( ! $parent ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_parent', __( 'Product shipping class parent is invalid', 'woocommerce' ), 400 ); + } + } + + $insert = wp_insert_term( $data['name'], 'product_shipping_class', $data ); + if ( is_wp_error( $insert ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_shipping_class', $insert->get_error_message(), 400 ); + } + + $id = $insert['term_id']; + + do_action( 'woocommerce_api_create_product_shipping_class', $id, $data ); + + $this->server->send_status( 201 ); + + return $this->get_product_shipping_class( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product shipping class. + * + * @since 2.5.0 + * @param int $id Product shipping class term ID + * @param array $data Posted data + * @return array|WP_Error Product shipping class if succeed, otherwise + * WP_Error will be returned + */ + public function edit_product_shipping_class( $id, $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_shipping_class'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_shipping_class', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_shipping_class' ), 400 ); + } + + $id = absint( $id ); + $data = $data['product_shipping_class']; + + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_shipping_class', __( 'You do not have permission to edit product shipping classes', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_edit_product_shipping_class_data', $data, $this ); + $shipping_class = $this->get_product_shipping_class( $id ); + + if ( is_wp_error( $shipping_class ) ) { + return $shipping_class; + } + + $update = wp_update_term( $id, 'product_shipping_class', $data ); + if ( is_wp_error( $update ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_shipping_class', __( 'Could not edit the shipping class', 'woocommerce' ), 400 ); + } + + do_action( 'woocommerce_api_edit_product_shipping_class', $id, $data ); + + return $this->get_product_shipping_class( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product shipping class. + * + * @since 2.5.0 + * @param int $id Product shipping class term ID + * @return array|WP_Error Success message if succeed, otherwise WP_Error + * will be returned + */ + public function delete_product_shipping_class( $id ) { + global $wpdb; + + try { + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_shipping_class', __( 'You do not have permission to delete product shipping classes', 'woocommerce' ), 401 ); + } + + $id = absint( $id ); + $deleted = wp_delete_term( $id, 'product_shipping_class' ); + if ( ! $deleted || is_wp_error( $deleted ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_shipping_class', __( 'Could not delete the shipping class', 'woocommerce' ), 401 ); + } + + do_action( 'woocommerce_api_delete_product_shipping_class', $id, $this ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_shipping_class' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/legacy/api/v3/class-wc-api-reports.php b/includes/legacy/api/v3/class-wc-api-reports.php new file mode 100644 index 00000000000..552fd253fda --- /dev/null +++ b/includes/legacy/api/v3/class-wc-api-reports.php @@ -0,0 +1,330 @@ +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 ) + ); + } +} diff --git a/includes/legacy/api/v3/class-wc-api-resource.php b/includes/legacy/api/v3/class-wc-api-resource.php new file mode 100644 index 00000000000..4aface69c0c --- /dev/null +++ b/includes/legacy/api/v3/class-wc-api-resource.php @@ -0,0 +1,471 @@ +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 + * `_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 ); + } +} diff --git a/includes/legacy/api/v3/class-wc-api-server.php b/includes/legacy/api/v3/class-wc-api-server.php new file mode 100644 index 00000000000..9c22f6ef6fa --- /dev/null +++ b/includes/legacy/api/v3/class-wc-api-server.php @@ -0,0 +1,777 @@ + 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; + } +} diff --git a/includes/legacy/api/v3/class-wc-api-taxes.php b/includes/legacy/api/v3/class-wc-api-taxes.php new file mode 100644 index 00000000000..5fafe782e44 --- /dev/null +++ b/includes/legacy/api/v3/class-wc-api-taxes.php @@ -0,0 +1,691 @@ + + * + * @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/ + $routes[ $this->base . '/(?P\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/ + $routes[ $this->base . '/classes/(?P\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() ) ); + } + } +} diff --git a/includes/legacy/api/v3/class-wc-api-webhooks.php b/includes/legacy/api/v3/class-wc-api-webhooks.php new file mode 100644 index 00000000000..83121936eaf --- /dev/null +++ b/includes/legacy/api/v3/class-wc-api-webhooks.php @@ -0,0 +1,509 @@ +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/ + $routes[ $this->base . '/(?P\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//deliveries + $routes[ $this->base . '/(?P\d+)/deliveries' ] = array( + array( array( $this, 'get_webhook_deliveries' ), WC_API_Server::READABLE ), + ); + + # GET /webhooks//deliveries/ + $routes[ $this->base . '/(?P\d+)/deliveries/(?P\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; + } +} diff --git a/includes/legacy/api/v3/interface-wc-api-handler.php b/includes/legacy/api/v3/interface-wc-api-handler.php new file mode 100644 index 00000000000..484f9f57f02 --- /dev/null +++ b/includes/legacy/api/v3/interface-wc-api-handler.php @@ -0,0 +1,47 @@ + Date: Wed, 19 Jun 2019 11:29:49 +0100 Subject: [PATCH 2/5] Update paths to legacy API and implement init method. --- includes/class-woocommerce.php | 1 + includes/legacy/class-wc-legacy-api.php | 78 ++++++++++++------------- 2 files changed, 39 insertions(+), 40 deletions(-) diff --git a/includes/class-woocommerce.php b/includes/class-woocommerce.php index 38451d0501a..a04c841670a 100644 --- a/includes/class-woocommerce.php +++ b/includes/class-woocommerce.php @@ -459,6 +459,7 @@ final class WooCommerce { $this->theme_support_includes(); $this->query = new WC_Query(); $this->api = new WC_API(); + $this->api->init(); } /** diff --git a/includes/legacy/class-wc-legacy-api.php b/includes/legacy/class-wc-legacy-api.php index 2c1000dd3c7..e79781bab68 100644 --- a/includes/legacy/class-wc-legacy-api.php +++ b/includes/legacy/class-wc-legacy-api.php @@ -43,11 +43,9 @@ class WC_Legacy_API { public $authentication; /** - * Setup class. - * - * @since 2.0 + * Init the legacy API. */ - public function __construct() { + public function init() { add_action( 'parse_request', array( $this, 'handle_rest_api_requests' ), 0 ); } @@ -128,23 +126,23 @@ class WC_Legacy_API { public function includes() { // API server / response handlers. - include_once( dirname( __FILE__ ) . '/../api/legacy/v3/class-wc-api-exception.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v3/class-wc-api-server.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v3/interface-wc-api-handler.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v3/class-wc-api-json-handler.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-exception.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-server.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/interface-wc-api-handler.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-json-handler.php' ); // Authentication. - include_once( dirname( __FILE__ ) . '/../api/legacy/v3/class-wc-api-authentication.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-authentication.php' ); $this->authentication = new WC_API_Authentication(); - include_once( dirname( __FILE__ ) . '/../api/legacy/v3/class-wc-api-resource.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v3/class-wc-api-coupons.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v3/class-wc-api-customers.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v3/class-wc-api-orders.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v3/class-wc-api-products.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v3/class-wc-api-reports.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v3/class-wc-api-taxes.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v3/class-wc-api-webhooks.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-resource.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-coupons.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-customers.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-orders.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-products.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-reports.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-taxes.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-webhooks.php' ); // Allow plugins to load other response handlers or resource classes. do_action( 'woocommerce_api_loaded' ); @@ -186,20 +184,20 @@ class WC_Legacy_API { private function handle_v1_rest_api_request() { // Include legacy required files for v1 REST API request. - include_once( dirname( __FILE__ ) . '/../api/legacy/v1/class-wc-api-server.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v1/interface-wc-api-handler.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v1/class-wc-api-json-handler.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v1/class-wc-api-xml-handler.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-server.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/interface-wc-api-handler.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-json-handler.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-xml-handler.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v1/class-wc-api-authentication.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-authentication.php' ); $this->authentication = new WC_API_Authentication(); - include_once( dirname( __FILE__ ) . '/../api/legacy/v1/class-wc-api-resource.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v1/class-wc-api-coupons.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v1/class-wc-api-customers.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v1/class-wc-api-orders.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v1/class-wc-api-products.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v1/class-wc-api-reports.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-resource.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-coupons.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-customers.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-orders.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-products.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-reports.php' ); // Allow plugins to load other response handlers or resource classes. do_action( 'woocommerce_api_loaded' ); @@ -232,21 +230,21 @@ class WC_Legacy_API { * @deprecated 2.6.0 */ private function handle_v2_rest_api_request() { - include_once( dirname( __FILE__ ) . '/../api/legacy/v2/class-wc-api-exception.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v2/class-wc-api-server.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v2/interface-wc-api-handler.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v2/class-wc-api-json-handler.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-exception.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-server.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/interface-wc-api-handler.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-json-handler.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v2/class-wc-api-authentication.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-authentication.php' ); $this->authentication = new WC_API_Authentication(); - include_once( dirname( __FILE__ ) . '/../api/legacy/v2/class-wc-api-resource.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v2/class-wc-api-coupons.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v2/class-wc-api-customers.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v2/class-wc-api-orders.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v2/class-wc-api-products.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v2/class-wc-api-reports.php' ); - include_once( dirname( __FILE__ ) . '/../api/legacy/v2/class-wc-api-webhooks.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-resource.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-coupons.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-customers.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-orders.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-products.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-reports.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-webhooks.php' ); // allow plugins to load other response handlers or resource classes. do_action( 'woocommerce_api_loaded' ); From 9a07e92ebaf7ddf2792ca8dd57922a74e7cd92d1 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Wed, 19 Jun 2019 11:31:04 +0100 Subject: [PATCH 3/5] Remove parent construct call since init() is used. --- includes/class-wc-api.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/includes/class-wc-api.php b/includes/class-wc-api.php index f9c7855e6ae..a56ea1bb5e3 100644 --- a/includes/class-wc-api.php +++ b/includes/class-wc-api.php @@ -24,8 +24,6 @@ class WC_API extends WC_Legacy_API { * @since 2.0 */ public function __construct() { - parent::__construct(); - $this->wc_api_init(); $this->rest_api_init(); } From 10b0aae09e31242edcf5f92da0947b36dfd89602 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Wed, 26 Jun 2019 11:42:36 +0100 Subject: [PATCH 4/5] Revert "Move legacy API files from /api/ to /legacy/api" This reverts commit 00a8ef5d4d91bf61bd837d1719021bf17765b856. --- ...lass-wc-rest-legacy-coupons-controller.php | 164 - ...class-wc-rest-legacy-orders-controller.php | 306 -- ...ass-wc-rest-legacy-products-controller.php | 804 ---- .../api/v1/class-wc-api-authentication.php | 410 -- .../legacy/api/v1/class-wc-api-coupons.php | 247 -- .../legacy/api/v1/class-wc-api-customers.php | 481 --- .../api/v1/class-wc-api-json-handler.php | 74 - .../legacy/api/v1/class-wc-api-orders.php | 396 -- .../legacy/api/v1/class-wc-api-products.php | 548 --- .../legacy/api/v1/class-wc-api-reports.php | 482 --- .../legacy/api/v1/class-wc-api-resource.php | 409 -- .../legacy/api/v1/class-wc-api-server.php | 782 ---- .../api/v1/class-wc-api-xml-handler.php | 308 -- .../api/v1/interface-wc-api-handler.php | 48 - .../api/v2/class-wc-api-authentication.php | 408 -- .../legacy/api/v2/class-wc-api-coupons.php | 575 --- .../legacy/api/v2/class-wc-api-customers.php | 837 ----- .../legacy/api/v2/class-wc-api-exception.php | 48 - .../api/v2/class-wc-api-json-handler.php | 73 - .../legacy/api/v2/class-wc-api-orders.php | 1830 --------- .../legacy/api/v2/class-wc-api-products.php | 2312 ------------ .../legacy/api/v2/class-wc-api-reports.php | 329 -- .../legacy/api/v2/class-wc-api-resource.php | 466 --- .../legacy/api/v2/class-wc-api-server.php | 775 ---- .../legacy/api/v2/class-wc-api-webhooks.php | 509 --- .../api/v2/interface-wc-api-handler.php | 47 - .../api/v3/class-wc-api-authentication.php | 414 --- .../legacy/api/v3/class-wc-api-coupons.php | 576 --- .../legacy/api/v3/class-wc-api-customers.php | 829 ----- .../legacy/api/v3/class-wc-api-exception.php | 48 - .../api/v3/class-wc-api-json-handler.php | 73 - .../legacy/api/v3/class-wc-api-orders.php | 1877 ---------- .../legacy/api/v3/class-wc-api-products.php | 3308 ----------------- .../legacy/api/v3/class-wc-api-reports.php | 330 -- .../legacy/api/v3/class-wc-api-resource.php | 471 --- .../legacy/api/v3/class-wc-api-server.php | 777 ---- includes/legacy/api/v3/class-wc-api-taxes.php | 691 ---- .../legacy/api/v3/class-wc-api-webhooks.php | 509 --- .../api/v3/interface-wc-api-handler.php | 47 - 39 files changed, 23618 deletions(-) delete mode 100644 includes/legacy/api/class-wc-rest-legacy-coupons-controller.php delete mode 100644 includes/legacy/api/class-wc-rest-legacy-orders-controller.php delete mode 100644 includes/legacy/api/class-wc-rest-legacy-products-controller.php delete mode 100644 includes/legacy/api/v1/class-wc-api-authentication.php delete mode 100644 includes/legacy/api/v1/class-wc-api-coupons.php delete mode 100644 includes/legacy/api/v1/class-wc-api-customers.php delete mode 100644 includes/legacy/api/v1/class-wc-api-json-handler.php delete mode 100644 includes/legacy/api/v1/class-wc-api-orders.php delete mode 100644 includes/legacy/api/v1/class-wc-api-products.php delete mode 100644 includes/legacy/api/v1/class-wc-api-reports.php delete mode 100644 includes/legacy/api/v1/class-wc-api-resource.php delete mode 100644 includes/legacy/api/v1/class-wc-api-server.php delete mode 100644 includes/legacy/api/v1/class-wc-api-xml-handler.php delete mode 100644 includes/legacy/api/v1/interface-wc-api-handler.php delete mode 100644 includes/legacy/api/v2/class-wc-api-authentication.php delete mode 100644 includes/legacy/api/v2/class-wc-api-coupons.php delete mode 100644 includes/legacy/api/v2/class-wc-api-customers.php delete mode 100644 includes/legacy/api/v2/class-wc-api-exception.php delete mode 100644 includes/legacy/api/v2/class-wc-api-json-handler.php delete mode 100644 includes/legacy/api/v2/class-wc-api-orders.php delete mode 100644 includes/legacy/api/v2/class-wc-api-products.php delete mode 100644 includes/legacy/api/v2/class-wc-api-reports.php delete mode 100644 includes/legacy/api/v2/class-wc-api-resource.php delete mode 100644 includes/legacy/api/v2/class-wc-api-server.php delete mode 100644 includes/legacy/api/v2/class-wc-api-webhooks.php delete mode 100644 includes/legacy/api/v2/interface-wc-api-handler.php delete mode 100644 includes/legacy/api/v3/class-wc-api-authentication.php delete mode 100644 includes/legacy/api/v3/class-wc-api-coupons.php delete mode 100644 includes/legacy/api/v3/class-wc-api-customers.php delete mode 100644 includes/legacy/api/v3/class-wc-api-exception.php delete mode 100644 includes/legacy/api/v3/class-wc-api-json-handler.php delete mode 100644 includes/legacy/api/v3/class-wc-api-orders.php delete mode 100644 includes/legacy/api/v3/class-wc-api-products.php delete mode 100644 includes/legacy/api/v3/class-wc-api-reports.php delete mode 100644 includes/legacy/api/v3/class-wc-api-resource.php delete mode 100644 includes/legacy/api/v3/class-wc-api-server.php delete mode 100644 includes/legacy/api/v3/class-wc-api-taxes.php delete mode 100644 includes/legacy/api/v3/class-wc-api-webhooks.php delete mode 100644 includes/legacy/api/v3/interface-wc-api-handler.php diff --git a/includes/legacy/api/class-wc-rest-legacy-coupons-controller.php b/includes/legacy/api/class-wc-rest-legacy-coupons-controller.php deleted file mode 100644 index 3378de41a40..00000000000 --- a/includes/legacy/api/class-wc-rest-legacy-coupons-controller.php +++ /dev/null @@ -1,164 +0,0 @@ -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 ); - } -} diff --git a/includes/legacy/api/class-wc-rest-legacy-orders-controller.php b/includes/legacy/api/class-wc-rest-legacy-orders-controller.php deleted file mode 100644 index ccc9db267f3..00000000000 --- a/includes/legacy/api/class-wc-rest-legacy-orders-controller.php +++ /dev/null @@ -1,306 +0,0 @@ - '_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() ) ); - } - } -} diff --git a/includes/legacy/api/class-wc-rest-legacy-products-controller.php b/includes/legacy/api/class-wc-rest-legacy-products-controller.php deleted file mode 100644 index 9096cbd0f98..00000000000 --- a/includes/legacy/api/class-wc-rest-legacy-products-controller.php +++ /dev/null @@ -1,804 +0,0 @@ - '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 ); - } -} diff --git a/includes/legacy/api/v1/class-wc-api-authentication.php b/includes/legacy/api/v1/class-wc-api-authentication.php deleted file mode 100644 index 1d26ae80ff6..00000000000 --- a/includes/legacy/api/v1/class-wc-api-authentication.php +++ /dev/null @@ -1,410 +0,0 @@ -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' ) - ); - } -} diff --git a/includes/legacy/api/v1/class-wc-api-coupons.php b/includes/legacy/api/v1/class-wc-api-coupons.php deleted file mode 100644 index 244b5efd3b0..00000000000 --- a/includes/legacy/api/v1/class-wc-api-coupons.php +++ /dev/null @@ -1,247 +0,0 @@ - - * - * @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/ - $routes[ $this->base . '/(?P\d+)' ] = array( - array( array( $this, 'get_coupon' ), WC_API_Server::READABLE ), - ); - - # GET /coupons/code/, note that coupon codes can contain spaces, dashes and underscores - $routes[ $this->base . '/code/(?P\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 ); - } -} diff --git a/includes/legacy/api/v1/class-wc-api-customers.php b/includes/legacy/api/v1/class-wc-api-customers.php deleted file mode 100644 index d5edb1503a2..00000000000 --- a/includes/legacy/api/v1/class-wc-api-customers.php +++ /dev/null @@ -1,481 +0,0 @@ - - * GET /customers//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/ - $routes[ $this->base . '/(?P\d+)' ] = array( - array( array( $this, 'get_customer' ), WC_API_SERVER::READABLE ), - ); - - # GET /customers//orders - $routes[ $this->base . '/(?P\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' ); - } -} diff --git a/includes/legacy/api/v1/class-wc-api-json-handler.php b/includes/legacy/api/v1/class-wc-api-json-handler.php deleted file mode 100644 index 691bbb9663c..00000000000 --- a/includes/legacy/api/v1/class-wc-api-json-handler.php +++ /dev/null @@ -1,74 +0,0 @@ -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 ); - } -} diff --git a/includes/legacy/api/v1/class-wc-api-orders.php b/includes/legacy/api/v1/class-wc-api-orders.php deleted file mode 100644 index c4a391d5bdb..00000000000 --- a/includes/legacy/api/v1/class-wc-api-orders.php +++ /dev/null @@ -1,396 +0,0 @@ - - * GET /orders//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/ - $routes[ $this->base . '/(?P\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//notes - $routes[ $this->base . '/(?P\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; - } -} diff --git a/includes/legacy/api/v1/class-wc-api-products.php b/includes/legacy/api/v1/class-wc-api-products.php deleted file mode 100644 index b608a3f326c..00000000000 --- a/includes/legacy/api/v1/class-wc-api-products.php +++ /dev/null @@ -1,548 +0,0 @@ - - * GET /products//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/ - $routes[ $this->base . '/(?P\d+)' ] = array( - array( array( $this, 'get_product' ), WC_API_Server::READABLE ), - ); - - # GET /products//reviews - $routes[ $this->base . '/(?P\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; - } -} diff --git a/includes/legacy/api/v1/class-wc-api-reports.php b/includes/legacy/api/v1/class-wc-api-reports.php deleted file mode 100644 index 527ea64bf94..00000000000 --- a/includes/legacy/api/v1/class-wc-api-reports.php +++ /dev/null @@ -1,482 +0,0 @@ -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; - } - } -} diff --git a/includes/legacy/api/v1/class-wc-api-resource.php b/includes/legacy/api/v1/class-wc-api-resource.php deleted file mode 100644 index d419f8346e9..00000000000 --- a/includes/legacy/api/v1/class-wc-api-resource.php +++ /dev/null @@ -1,409 +0,0 @@ -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 - * `_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; - } - } -} diff --git a/includes/legacy/api/v1/class-wc-api-server.php b/includes/legacy/api/v1/class-wc-api-server.php deleted file mode 100644 index 182d482b51f..00000000000 --- a/includes/legacy/api/v1/class-wc-api-server.php +++ /dev/null @@ -1,782 +0,0 @@ - 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; - } -} diff --git a/includes/legacy/api/v1/class-wc-api-xml-handler.php b/includes/legacy/api/v1/class-wc-api-xml-handler.php deleted file mode 100644 index 04f47e669e4..00000000000 --- a/includes/legacy/api/v1/class-wc-api-xml-handler.php +++ /dev/null @@ -1,308 +0,0 @@ -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. - * @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 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; - } -} diff --git a/includes/legacy/api/v1/interface-wc-api-handler.php b/includes/legacy/api/v1/interface-wc-api-handler.php deleted file mode 100644 index 464d9cb73cb..00000000000 --- a/includes/legacy/api/v1/interface-wc-api-handler.php +++ /dev/null @@ -1,48 +0,0 @@ -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' ) - ); - } -} diff --git a/includes/legacy/api/v2/class-wc-api-coupons.php b/includes/legacy/api/v2/class-wc-api-coupons.php deleted file mode 100644 index ca57fb4670e..00000000000 --- a/includes/legacy/api/v2/class-wc-api-coupons.php +++ /dev/null @@ -1,575 +0,0 @@ - - * - * @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/ - $routes[ $this->base . '/(?P\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/, note that coupon codes can contain spaces, dashes and underscores - $routes[ $this->base . '/code/(?P\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() ) ); - } - } -} diff --git a/includes/legacy/api/v2/class-wc-api-customers.php b/includes/legacy/api/v2/class-wc-api-customers.php deleted file mode 100644 index 4912be3b34e..00000000000 --- a/includes/legacy/api/v2/class-wc-api-customers.php +++ /dev/null @@ -1,837 +0,0 @@ - - * GET /customers//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/ - $routes[ $this->base . '/(?P\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/ - $routes[ $this->base . '/email/(?P.+)' ] = array( - array( array( $this, 'get_customer_by_email' ), WC_API_SERVER::READABLE ), - ); - - # GET /customers//orders - $routes[ $this->base . '/(?P\d+)/orders' ] = array( - array( array( $this, 'get_customer_orders' ), WC_API_SERVER::READABLE ), - ); - - # GET /customers//downloads - $routes[ $this->base . '/(?P\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() ) ); - } - } -} diff --git a/includes/legacy/api/v2/class-wc-api-exception.php b/includes/legacy/api/v2/class-wc-api-exception.php deleted file mode 100644 index 834ed04d6eb..00000000000 --- a/includes/legacy/api/v2/class-wc-api-exception.php +++ /dev/null @@ -1,48 +0,0 @@ -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; - } -} diff --git a/includes/legacy/api/v2/class-wc-api-json-handler.php b/includes/legacy/api/v2/class-wc-api-json-handler.php deleted file mode 100644 index 672aa8850c2..00000000000 --- a/includes/legacy/api/v2/class-wc-api-json-handler.php +++ /dev/null @@ -1,73 +0,0 @@ -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 ); - } -} diff --git a/includes/legacy/api/v2/class-wc-api-orders.php b/includes/legacy/api/v2/class-wc-api-orders.php deleted file mode 100644 index 67fc745d364..00000000000 --- a/includes/legacy/api/v2/class-wc-api-orders.php +++ /dev/null @@ -1,1830 +0,0 @@ - - * GET /orders//notes - * - * @since 2.1 - * @param array $routes - * @return array - */ - public function register_routes( $routes ) { - - # GET|POST /orders - $routes[ $this->base ] = array( - array( array( $this, 'get_orders' ), WC_API_Server::READABLE ), - array( array( $this, 'create_order' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET /orders/count - $routes[ $this->base . '/count' ] = array( - array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ), - ); - - # GET /orders/statuses - $routes[ $this->base . '/statuses' ] = array( - array( array( $this, 'get_order_statuses' ), WC_API_Server::READABLE ), - ); - - # GET|PUT|DELETE /orders/ - $routes[ $this->base . '/(?P\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 ), - array( array( $this, 'delete_order' ), WC_API_Server::DELETABLE ), - ); - - # GET|POST /orders//notes - $routes[ $this->base . '/(?P\d+)/notes' ] = array( - array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ), - array( array( $this, 'create_order_note' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET|PUT|DELETE /orders//notes/ - $routes[ $this->base . '/(?P\d+)/notes/(?P\d+)' ] = array( - array( array( $this, 'get_order_note' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_order_note' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_order_note' ), WC_API_SERVER::DELETABLE ), - ); - - # GET|POST /orders//refunds - $routes[ $this->base . '/(?P\d+)/refunds' ] = array( - array( array( $this, 'get_order_refunds' ), WC_API_Server::READABLE ), - array( array( $this, 'create_order_refund' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET|PUT|DELETE /orders//refunds/ - $routes[ $this->base . '/(?P\d+)/refunds/(?P\d+)' ] = array( - array( array( $this, 'get_order_refund' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_order_refund' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_order_refund' ), WC_API_SERVER::DELETABLE ), - ); - - # POST|PUT /orders/bulk - $routes[ $this->base . '/bulk' ] = array( - array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - ); - - 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, $filter ) ); - } - - $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 - * @param array $filter - * @return array|WP_Error - */ - public function get_order( $id, $fields = null, $filter = array() ) { - - // ensure order ID is valid & user has permission to read - $id = $this->validate_request( $id, $this->post_type, 'read' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - // Get the decimal precession - $dp = ( isset( $filter['dp'] ) ? intval( $filter['dp'] ) : 2 ); - $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(), $dp ), - 'subtotal' => wc_format_decimal( $order->get_subtotal(), $dp ), - 'total_line_items_quantity' => $order->get_item_count(), - 'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ), - 'total_shipping' => wc_format_decimal( $order->get_shipping_total(), $dp ), - 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ), - 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ), - 'total_discount' => wc_format_decimal( $order->get_total_discount(), $dp ), - '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(); - $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; - $item_meta = $item->get_formatted_meta_data( $hideprefix ); - - foreach ( $item_meta as $key => $values ) { - $item_meta[ $key ]->label = $values->display_key; - unset( $item_meta[ $key ]->display_key ); - unset( $item_meta[ $key ]->display_value ); - } - - $order_data['line_items'][] = array( - 'id' => $item_id, - 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ), - 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), $dp ), - 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), - 'total_tax' => wc_format_decimal( $item->get_total_tax(), $dp ), - 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), - '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, - 'meta' => array_values( $item_meta ), - ); - } - - // 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(), $dp ), - ); - } - - // add taxes - foreach ( $order->get_tax_totals() as $tax_code => $tax ) { - $order_data['tax_lines'][] = array( - 'id' => $tax->id, - 'rate_id' => $tax->rate_id, - 'code' => $tax_code, - 'title' => $tax->label, - 'total' => wc_format_decimal( $tax->amount, $dp ), - '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 ), $dp ), - 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ), - ); - } - - // 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(), $dp ), - ); - } - - return array( 'order' => apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server ) ); - } - - /** - * Get the total number of orders - * - * @since 2.4 - * - * @param string $status - * @param array $filter - * - * @return array|WP_Error - */ - public function get_orders_count( $status = null, $filter = array() ) { - - try { - if ( ! current_user_can( 'read_private_shop_orders' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), 401 ); - } - - if ( ! empty( $status ) ) { - - if ( 'any' === $status ) { - - $order_statuses = array(); - - foreach ( wc_get_order_statuses() as $slug => $name ) { - $filter['status'] = str_replace( 'wc-', '', $slug ); - $query = $this->query_orders( $filter ); - $order_statuses[ str_replace( 'wc-', '', $slug ) ] = (int) $query->found_posts; - } - - return array( 'count' => $order_statuses ); - - } else { - $filter['status'] = $status; - } - } - - $query = $this->query_orders( $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 a list of valid order statuses - * - * Note this requires no specific permissions other than being an authenticated - * API user. Order statuses (particularly custom statuses) could be considered - * private information which is why it's not in the API index. - * - * @since 2.1 - * @return array - */ - public function get_order_statuses() { - - $order_statuses = array(); - - foreach ( wc_get_order_statuses() as $slug => $name ) { - $order_statuses[ str_replace( 'wc-', '', $slug ) ] = $name; - } - - return array( 'order_statuses' => apply_filters( 'woocommerce_api_order_statuses_response', $order_statuses, $this ) ); - } - - /** - * Create an order - * - * @since 2.2 - * - * @param array $data raw order data - * - * @return array|WP_Error - */ - public function create_order( $data ) { - global $wpdb; - - try { - if ( ! isset( $data['order'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order' ), 400 ); - } - - $data = $data['order']; - - // permission check - if ( ! current_user_can( 'publish_shop_orders' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order', __( 'You do not have permission to create orders', 'woocommerce' ), 401 ); - } - - $data = apply_filters( 'woocommerce_api_create_order_data', $data, $this ); - - // default order args, note that status is checked for validity in wc_create_order() - $default_order_args = array( - 'status' => isset( $data['status'] ) ? $data['status'] : '', - 'customer_note' => isset( $data['note'] ) ? $data['note'] : null, - ); - - // if creating order for existing customer - if ( ! empty( $data['customer_id'] ) ) { - - // make sure customer exists - if ( false === get_user_by( 'id', $data['customer_id'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); - } - - $default_order_args['customer_id'] = $data['customer_id']; - } - - // create the pending order - $order = $this->create_base_order( $default_order_args, $data ); - - if ( is_wp_error( $order ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_order', sprintf( __( 'Cannot create order: %s', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 ); - } - - // billing/shipping addresses - $this->set_order_addresses( $order, $data ); - - $lines = array( - 'line_item' => 'line_items', - 'shipping' => 'shipping_lines', - 'fee' => 'fee_lines', - 'coupon' => 'coupon_lines', - ); - - foreach ( $lines as $line_type => $line ) { - - if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { - - $set_item = "set_{$line_type}"; - - foreach ( $data[ $line ] as $item ) { - - $this->$set_item( $order, $item, 'create' ); - } - } - } - - // calculate totals and set them - $order->calculate_totals(); - - // payment method (and payment_complete() if `paid` == true) - if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { - - // method ID & title are required - if ( empty( $data['payment_details']['method_id'] ) || empty( $data['payment_details']['method_title'] ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_payment_details', __( 'Payment method ID and title are required', 'woocommerce' ), 400 ); - } - - update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); - update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); - - // mark as paid if set - if ( isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { - $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); - } - } - - // set order currency - if ( isset( $data['currency'] ) ) { - - if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); - } - - update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); - } - - // set order meta - if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { - $this->set_order_meta( $order->get_id(), $data['order_meta'] ); - } - - // HTTP 201 Created - $this->server->send_status( 201 ); - - wc_delete_shop_order_transients( $order ); - - do_action( 'woocommerce_api_create_order', $order->get_id(), $data, $this ); - do_action( 'woocommerce_new_order', $order->get_id() ); - - return $this->get_order( $order->get_id() ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Creates new WC_Order. - * - * Requires a separate function for classes that extend WC_API_Orders. - * - * @since 2.3 - * - * @param $args array - * @param $data - * - * @return WC_Order - */ - protected function create_base_order( $args, $data ) { - return wc_create_order( $args ); - } - - /** - * Edit an order - * - * @since 2.2 - * - * @param int $id the order ID - * @param array $data - * - * @return array|WP_Error - */ - public function edit_order( $id, $data ) { - try { - if ( ! isset( $data['order'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order' ), 400 ); - } - - $data = $data['order']; - - $update_totals = false; - - $id = $this->validate_request( $id, $this->post_type, 'edit' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - $data = apply_filters( 'woocommerce_api_edit_order_data', $data, $id, $this ); - $order = wc_get_order( $id ); - - if ( empty( $order ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); - } - - $order_args = array( 'order_id' => $order->get_id() ); - - // Customer note. - if ( isset( $data['note'] ) ) { - $order_args['customer_note'] = $data['note']; - } - - // Customer ID. - if ( isset( $data['customer_id'] ) && $data['customer_id'] != $order->get_user_id() ) { - // Make sure customer exists. - if ( false === get_user_by( 'id', $data['customer_id'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); - } - - update_post_meta( $order->get_id(), '_customer_user', $data['customer_id'] ); - } - - // Billing/shipping address. - $this->set_order_addresses( $order, $data ); - - $lines = array( - 'line_item' => 'line_items', - 'shipping' => 'shipping_lines', - 'fee' => 'fee_lines', - 'coupon' => 'coupon_lines', - ); - - foreach ( $lines as $line_type => $line ) { - - if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { - - $update_totals = true; - - foreach ( $data[ $line ] as $item ) { - - // Item ID is always required. - if ( ! array_key_exists( 'id', $item ) ) { - $item['id'] = null; - } - - // Create item. - if ( is_null( $item['id'] ) ) { - $this->set_item( $order, $line_type, $item, 'create' ); - } elseif ( $this->item_is_null( $item ) ) { - // Delete item. - wc_delete_order_item( $item['id'] ); - } else { - // Update item. - $this->set_item( $order, $line_type, $item, 'update' ); - } - } - } - } - - // Payment method (and payment_complete() if `paid` == true and order needs payment). - if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { - - // Method ID. - if ( isset( $data['payment_details']['method_id'] ) ) { - update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); - } - - // Method title. - if ( isset( $data['payment_details']['method_title'] ) ) { - update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); - } - - // Mark as paid if set. - if ( $order->needs_payment() && isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { - $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); - } - } - - // Set order currency. - if ( isset( $data['currency'] ) ) { - if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); - } - - update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); - } - - // If items have changed, recalculate order totals. - if ( $update_totals ) { - $order->calculate_totals(); - } - - // Update order meta. - if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { - $this->set_order_meta( $order->get_id(), $data['order_meta'] ); - } - - // Update the order post to set customer note/modified date. - wc_update_order( $order_args ); - - // Order status. - if ( ! empty( $data['status'] ) ) { - // Refresh the order instance. - $order = wc_get_order( $order->get_id() ); - $order->update_status( $data['status'], isset( $data['status_note'] ) ? $data['status_note'] : '', true ); - } - - wc_delete_shop_order_transients( $order ); - - do_action( 'woocommerce_api_edit_order', $order->get_id(), $data, $this ); - do_action( 'woocommerce_update_order', $order->get_id() ); - - return $this->get_order( $id ); - - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete an order - * - * @param int $id the order ID - * @param bool $force true to permanently delete order, false to move to trash - * @return array|WP_Error - */ - public function delete_order( $id, $force = false ) { - - $id = $this->validate_request( $id, $this->post_type, 'delete' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - wc_delete_shop_order_transients( $id ); - - do_action( 'woocommerce_api_delete_order', $id, $this ); - - return $this->delete( $id, 'order', ( 'true' === $force ) ); - } - - /** - * Helper method to get order post objects - * - * @since 2.1 - * @param array $args request arguments for filtering query - * @return WP_Query - */ - protected function query_orders( $args ) { - - // set base query arguments - $query_args = array( - 'fields' => 'ids', - 'post_type' => $this->post_type, - '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 set/update the billing & shipping addresses for - * an order - * - * @since 2.1 - * @param \WC_Order $order - * @param array $data - */ - protected function set_order_addresses( $order, $data ) { - - $address_fields = array( - 'first_name', - 'last_name', - 'company', - 'email', - 'phone', - 'address_1', - 'address_2', - 'city', - 'state', - 'postcode', - 'country', - ); - - $billing_address = $shipping_address = array(); - - // billing address - if ( isset( $data['billing_address'] ) && is_array( $data['billing_address'] ) ) { - - foreach ( $address_fields as $field ) { - - if ( isset( $data['billing_address'][ $field ] ) ) { - $billing_address[ $field ] = wc_clean( $data['billing_address'][ $field ] ); - } - } - - unset( $address_fields['email'] ); - unset( $address_fields['phone'] ); - } - - // shipping address - if ( isset( $data['shipping_address'] ) && is_array( $data['shipping_address'] ) ) { - - foreach ( $address_fields as $field ) { - - if ( isset( $data['shipping_address'][ $field ] ) ) { - $shipping_address[ $field ] = wc_clean( $data['shipping_address'][ $field ] ); - } - } - } - - $this->update_address( $order, $billing_address, 'billing' ); - $this->update_address( $order, $shipping_address, 'shipping' ); - - // update user meta - if ( $order->get_user_id() ) { - foreach ( $billing_address as $key => $value ) { - update_user_meta( $order->get_user_id(), 'billing_' . $key, $value ); - } - foreach ( $shipping_address as $key => $value ) { - update_user_meta( $order->get_user_id(), 'shipping_' . $key, $value ); - } - } - } - - /** - * Update address. - * - * @param WC_Order $order - * @param array $posted - * @param string $type - */ - protected function update_address( $order, $posted, $type = 'billing' ) { - foreach ( $posted as $key => $value ) { - if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { - $order->{"set_{$type}_{$key}"}( $value ); - } - } - } - - /** - * Helper method to add/update order meta, with two restrictions: - * - * 1) Only non-protected meta (no leading underscore) can be set - * 2) Meta values must be scalar (int, string, bool) - * - * @since 2.2 - * @param int $order_id valid order ID - * @param array $order_meta order meta in array( 'meta_key' => 'meta_value' ) format - */ - protected function set_order_meta( $order_id, $order_meta ) { - - foreach ( $order_meta as $meta_key => $meta_value ) { - - if ( is_string( $meta_key ) && ! is_protected_meta( $meta_key ) && is_scalar( $meta_value ) ) { - update_post_meta( $order_id, $meta_key, $meta_value ); - } - } - } - - /** - * Helper method to check if the resource ID associated with the provided item is null - * - * Items can be deleted by setting the resource ID to null - * - * @since 2.2 - * @param array $item item provided in the request body - * @return bool true if the item resource ID is null, false otherwise - */ - protected function item_is_null( $item ) { - - $keys = array( 'product_id', 'method_id', 'title', 'code' ); - - foreach ( $keys as $key ) { - if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { - return true; - } - } - - return false; - } - - /** - * Wrapper method to create/update order items - * - * When updating, the item ID provided is checked to ensure it is associated - * with the order. - * - * @since 2.2 - * @param \WC_Order $order order - * @param string $item_type - * @param array $item item provided in the request body - * @param string $action either 'create' or 'update' - * @throws WC_API_Exception if item ID is not associated with order - */ - protected function set_item( $order, $item_type, $item, $action ) { - global $wpdb; - - $set_method = "set_{$item_type}"; - - // verify provided line item ID is associated with order - if ( 'update' === $action ) { - - $result = $wpdb->get_row( - $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", - absint( $item['id'] ), - absint( $order->get_id() ) - ) ); - - if ( is_null( $result ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); - } - } - - $this->$set_method( $order, $item, $action ); - } - - /** - * Create or update a line item - * - * @since 2.2 - * @param \WC_Order $order - * @param array $item line item data - * @param string $action 'create' to add line item or 'update' to update it - * @throws WC_API_Exception invalid data, server error - */ - protected function set_line_item( $order, $item, $action ) { - $creating = ( 'create' === $action ); - - // product is always required - if ( ! isset( $item['product_id'] ) && ! isset( $item['sku'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID or SKU is required', 'woocommerce' ), 400 ); - } - - // when updating, ensure product ID provided matches - if ( 'update' === $action ) { - - $item_product_id = wc_get_order_item_meta( $item['id'], '_product_id' ); - $item_variation_id = wc_get_order_item_meta( $item['id'], '_variation_id' ); - - if ( $item['product_id'] != $item_product_id && $item['product_id'] != $item_variation_id ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID provided does not match this line item', 'woocommerce' ), 400 ); - } - } - - if ( isset( $item['product_id'] ) ) { - $product_id = $item['product_id']; - } elseif ( isset( $item['sku'] ) ) { - $product_id = wc_get_product_id_by_sku( $item['sku'] ); - } - - // variations must each have a key & value - $variation_id = 0; - if ( isset( $item['variations'] ) && is_array( $item['variations'] ) ) { - foreach ( $item['variations'] as $key => $value ) { - if ( ! $key || ! $value ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_variation', __( 'The product variation is invalid', 'woocommerce' ), 400 ); - } - } - $variation_id = $this->get_variation_id( wc_get_product( $product_id ), $item['variations'] ); - } - - $product = wc_get_product( $variation_id ? $variation_id : $product_id ); - - // must be a valid WC_Product - if ( ! is_object( $product ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product', __( 'Product is invalid.', 'woocommerce' ), 400 ); - } - - // quantity must be positive float - if ( isset( $item['quantity'] ) && floatval( $item['quantity'] ) <= 0 ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity must be a positive float.', 'woocommerce' ), 400 ); - } - - // quantity is required when creating - if ( $creating && ! isset( $item['quantity'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity is required.', 'woocommerce' ), 400 ); - } - - if ( $creating ) { - $line_item = new WC_Order_Item_Product(); - } else { - $line_item = new WC_Order_Item_Product( $item['id'] ); - } - - $line_item->set_product( $product ); - $line_item->set_order_id( $order->get_id() ); - - if ( isset( $item['quantity'] ) ) { - $line_item->set_quantity( $item['quantity'] ); - } - if ( isset( $item['total'] ) ) { - $line_item->set_total( floatval( $item['total'] ) ); - } elseif ( $creating ) { - $total = wc_get_price_excluding_tax( $product, array( 'qty' => $line_item->get_quantity() ) ); - $line_item->set_total( $total ); - $line_item->set_subtotal( $total ); - } - if ( isset( $item['total_tax'] ) ) { - $line_item->set_total_tax( floatval( $item['total_tax'] ) ); - } - if ( isset( $item['subtotal'] ) ) { - $line_item->set_subtotal( floatval( $item['subtotal'] ) ); - } - if ( isset( $item['subtotal_tax'] ) ) { - $line_item->set_subtotal_tax( floatval( $item['subtotal_tax'] ) ); - } - if ( $variation_id ) { - $line_item->set_variation_id( $variation_id ); - $line_item->set_variation( $item['variations'] ); - } - - // Save or add to order. - if ( $creating ) { - $order->add_item( $line_item ); - } else { - $item_id = $line_item->save(); - - if ( ! $item_id ) { - throw new WC_API_Exception( 'woocommerce_cannot_create_line_item', __( 'Cannot create line item, try again.', 'woocommerce' ), 500 ); - } - } - } - - /** - * Given a product ID & API provided variations, find the correct variation ID to use for calculation - * We can't just trust input from the API to pass a variation_id manually, otherwise you could pass - * the cheapest variation ID but provide other information so we have to look up the variation ID. - * - * @param WC_Product $product - * @param array $variations - * - * @return int returns an ID if a valid variation was found for this product - */ - function get_variation_id( $product, $variations = array() ) { - $variation_id = null; - $variations_normalized = array(); - - if ( $product->is_type( 'variable' ) && $product->has_child() ) { - if ( isset( $variations ) && is_array( $variations ) ) { - // start by normalizing the passed variations - foreach ( $variations as $key => $value ) { - $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); // from get_attributes in class-wc-api-products.php - $variations_normalized[ $key ] = strtolower( $value ); - } - // now search through each product child and see if our passed variations match anything - foreach ( $product->get_children() as $variation ) { - $meta = array(); - foreach ( get_post_meta( $variation ) as $key => $value ) { - $value = $value[0]; - $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); - $meta[ $key ] = strtolower( $value ); - } - // if the variation array is a part of the $meta array, we found our match - if ( $this->array_contains( $variations_normalized, $meta ) ) { - $variation_id = $variation; - break; - } - } - } - } - - return $variation_id; - } - - /** - * Utility function to see if the meta array contains data from variations - * - * @param array $needles - * @param array $haystack - * - * @return bool - */ - protected function array_contains( $needles, $haystack ) { - foreach ( $needles as $key => $value ) { - if ( $haystack[ $key ] !== $value ) { - return false; - } - } - return true; - } - - /** - * Create or update an order shipping method - * - * @since 2.2 - * @param \WC_Order $order - * @param array $shipping item data - * @param string $action 'create' to add shipping or 'update' to update it - * @throws WC_API_Exception invalid data, server error - */ - protected function set_shipping( $order, $shipping, $action ) { - - // total must be a positive float - if ( isset( $shipping['total'] ) && floatval( $shipping['total'] ) < 0 ) { - throw new WC_API_Exception( 'woocommerce_invalid_shipping_total', __( 'Shipping total must be a positive amount.', 'woocommerce' ), 400 ); - } - - if ( 'create' === $action ) { - - // method ID is required - if ( ! isset( $shipping['method_id'] ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); - } - - $rate = new WC_Shipping_Rate( $shipping['method_id'], isset( $shipping['method_title'] ) ? $shipping['method_title'] : '', isset( $shipping['total'] ) ? floatval( $shipping['total'] ) : 0, array(), $shipping['method_id'] ); - $item = new WC_Order_Item_Shipping(); - $item->set_order_id( $order->get_id() ); - $item->set_shipping_rate( $rate ); - $order->add_item( $item ); - } else { - - $item = new WC_Order_Item_Shipping( $shipping['id'] ); - - if ( isset( $shipping['method_id'] ) ) { - $item->set_method_id( $shipping['method_id'] ); - } - - if ( isset( $shipping['method_title'] ) ) { - $item->set_method_title( $shipping['method_title'] ); - } - - if ( isset( $shipping['total'] ) ) { - $item->set_total( floatval( $shipping['total'] ) ); - } - - $shipping_id = $item->save(); - - if ( ! $shipping_id ) { - throw new WC_API_Exception( 'woocommerce_cannot_update_shipping', __( 'Cannot update shipping method, try again.', 'woocommerce' ), 500 ); - } - } - } - - /** - * Create or update an order fee - * - * @since 2.2 - * @param \WC_Order $order - * @param array $fee item data - * @param string $action 'create' to add fee or 'update' to update it - * @throws WC_API_Exception invalid data, server error - */ - protected function set_fee( $order, $fee, $action ) { - - if ( 'create' === $action ) { - - // fee title is required - if ( ! isset( $fee['title'] ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee title is required', 'woocommerce' ), 400 ); - } - - $item = new WC_Order_Item_Fee(); - $item->set_order_id( $order->get_id() ); - $item->set_name( wc_clean( $fee['title'] ) ); - $item->set_total( isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0 ); - - // if taxable, tax class and total are required - if ( ! empty( $fee['taxable'] ) ) { - if ( ! isset( $fee['tax_class'] ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee tax class is required when fee is taxable.', 'woocommerce' ), 400 ); - } - - $item->set_tax_status( 'taxable' ); - $item->set_tax_class( $fee['tax_class'] ); - - if ( isset( $fee['total_tax'] ) ) { - $item->set_total_tax( isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0 ); - } - - if ( isset( $fee['tax_data'] ) ) { - $item->set_total_tax( wc_format_refund_total( array_sum( $fee['tax_data'] ) ) ); - $item->set_taxes( array_map( 'wc_format_refund_total', $fee['tax_data'] ) ); - } - } - - $order->add_item( $item ); - } else { - - $item = new WC_Order_Item_Fee( $fee['id'] ); - - if ( isset( $fee['title'] ) ) { - $item->set_name( wc_clean( $fee['title'] ) ); - } - - if ( isset( $fee['tax_class'] ) ) { - $item->set_tax_class( $fee['tax_class'] ); - } - - if ( isset( $fee['total'] ) ) { - $item->set_total( floatval( $fee['total'] ) ); - } - - if ( isset( $fee['total_tax'] ) ) { - $item->set_total_tax( floatval( $fee['total_tax'] ) ); - } - - $fee_id = $item->save(); - - if ( ! $fee_id ) { - throw new WC_API_Exception( 'woocommerce_cannot_update_fee', __( 'Cannot update fee, try again.', 'woocommerce' ), 500 ); - } - } - } - - /** - * Create or update an order coupon - * - * @since 2.2 - * @param \WC_Order $order - * @param array $coupon item data - * @param string $action 'create' to add coupon or 'update' to update it - * @throws WC_API_Exception invalid data, server error - */ - protected function set_coupon( $order, $coupon, $action ) { - - // coupon amount must be positive float - if ( isset( $coupon['amount'] ) && floatval( $coupon['amount'] ) < 0 ) { - throw new WC_API_Exception( 'woocommerce_invalid_coupon_total', __( 'Coupon discount total must be a positive amount.', 'woocommerce' ), 400 ); - } - - if ( 'create' === $action ) { - - // coupon code is required - if ( empty( $coupon['code'] ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); - } - - $item = new WC_Order_Item_Coupon(); - $item->set_props( array( - 'code' => $coupon['code'], - 'discount' => isset( $coupon['amount'] ) ? floatval( $coupon['amount'] ) : 0, - 'discount_tax' => 0, - 'order_id' => $order->get_id(), - ) ); - $order->add_item( $item ); - } else { - - $item = new WC_Order_Item_Coupon( $coupon['id'] ); - - if ( isset( $coupon['code'] ) ) { - $item->set_code( $coupon['code'] ); - } - - if ( isset( $coupon['amount'] ) ) { - $item->set_discount( floatval( $coupon['amount'] ) ); - } - - $coupon_id = $item->save(); - - if ( ! $coupon_id ) { - throw new WC_API_Exception( 'woocommerce_cannot_update_order_coupon', __( 'Cannot update coupon, try again.', 'woocommerce' ), 500 ); - } - } - } - - /** - * Get the admin order notes for an order - * - * @since 2.1 - * @param string $order_id order ID - * @param string|null $fields fields to include in response - * @return array|WP_Error - */ - public function get_order_notes( $order_id, $fields = null ) { - - // ensure ID is valid order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $args = array( - 'post_id' => $order_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[] = current( $this->get_order_note( $order_id, $note->comment_ID, $fields ) ); - } - - return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $order_id, $fields, $notes, $this->server ) ); - } - - /** - * Get an order note for the given order ID and ID - * - * @since 2.2 - * - * @param string $order_id order ID - * @param string $id order note ID - * @param string|null $fields fields to limit response to - * - * @return array|WP_Error - */ - public function get_order_note( $order_id, $id, $fields = null ) { - try { - // Validate order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); - } - - $note = get_comment( $id ); - - if ( is_null( $note ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $order_note = 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_note' => apply_filters( 'woocommerce_api_order_note_response', $order_note, $id, $fields, $note, $order_id, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Create a new order note for the given order - * - * @since 2.2 - * @param string $order_id order ID - * @param array $data raw request data - * @return WP_Error|array error or created note response data - */ - public function create_order_note( $order_id, $data ) { - try { - if ( ! isset( $data['order_note'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_note' ), 400 ); - } - - $data = $data['order_note']; - - // permission check - if ( ! current_user_can( 'publish_shop_orders' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_note', __( 'You do not have permission to create order notes', 'woocommerce' ), 401 ); - } - - $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $order = wc_get_order( $order_id ); - - $data = apply_filters( 'woocommerce_api_create_order_note_data', $data, $order_id, $this ); - - // note content is required - if ( ! isset( $data['note'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note', __( 'Order note is required', 'woocommerce' ), 400 ); - } - - $is_customer_note = ( isset( $data['customer_note'] ) && true === $data['customer_note'] ); - - // create the note - $note_id = $order->add_order_note( $data['note'], $is_customer_note ); - - if ( ! $note_id ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), 500 ); - } - - // HTTP 201 Created - $this->server->send_status( 201 ); - - do_action( 'woocommerce_api_create_order_note', $note_id, $order_id, $this ); - - return $this->get_order_note( $order->get_id(), $note_id ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Edit the order note - * - * @since 2.2 - * @param string $order_id order ID - * @param string $id note ID - * @param array $data parsed request data - * @return WP_Error|array error or edited note response data - */ - public function edit_order_note( $order_id, $id, $data ) { - try { - if ( ! isset( $data['order_note'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_note' ), 400 ); - } - - $data = $data['order_note']; - - // Validate order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $order = wc_get_order( $order_id ); - - // Validate note ID - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); - } - - // Ensure note ID is valid - $note = get_comment( $id ); - - if ( is_null( $note ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - // Ensure note ID is associated with given order - if ( $note->comment_post_ID != $order->get_id() ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); - } - - $data = apply_filters( 'woocommerce_api_edit_order_note_data', $data, $note->comment_ID, $order->get_id(), $this ); - - // Note content - if ( isset( $data['note'] ) ) { - - wp_update_comment( - array( - 'comment_ID' => $note->comment_ID, - 'comment_content' => $data['note'], - ) - ); - } - - // Customer note - if ( isset( $data['customer_note'] ) ) { - - update_comment_meta( $note->comment_ID, 'is_customer_note', true === $data['customer_note'] ? 1 : 0 ); - } - - do_action( 'woocommerce_api_edit_order_note', $note->comment_ID, $order->get_id(), $this ); - - return $this->get_order_note( $order->get_id(), $note->comment_ID ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete order note - * - * @since 2.2 - * @param string $order_id order ID - * @param string $id note ID - * @return WP_Error|array error or deleted message - */ - public function delete_order_note( $order_id, $id ) { - try { - $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - // Validate note ID - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); - } - - // Ensure note ID is valid - $note = get_comment( $id ); - - if ( is_null( $note ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - // Ensure note ID is associated with given order - if ( $note->comment_post_ID != $order_id ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); - } - - // Force delete since trashed order notes could not be managed through comments list table - $result = wc_delete_order_note( $note->comment_ID ); - - if ( ! $result ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_delete_order_note', __( 'This order note cannot be deleted', 'woocommerce' ), 500 ); - } - - do_action( 'woocommerce_api_delete_order_note', $note->comment_ID, $order_id, $this ); - - return array( 'message' => __( 'Permanently deleted order note', 'woocommerce' ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get the order refunds for an order - * - * @since 2.2 - * @param string $order_id order ID - * @param string|null $fields fields to include in response - * @return array|WP_Error - */ - public function get_order_refunds( $order_id, $fields = null ) { - - // Ensure ID is valid order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $refund_items = wc_get_orders( array( - 'type' => 'shop_order_refund', - 'parent' => $order_id, - 'limit' => -1, - 'return' => 'ids', - ) ); - $order_refunds = array(); - - foreach ( $refund_items as $refund_id ) { - $order_refunds[] = current( $this->get_order_refund( $order_id, $refund_id, $fields ) ); - } - - return array( 'order_refunds' => apply_filters( 'woocommerce_api_order_refunds_response', $order_refunds, $order_id, $fields, $refund_items, $this ) ); - } - - /** - * Get an order refund for the given order ID and ID - * - * @since 2.2 - * - * @param string $order_id order ID - * @param int $id - * @param string|null $fields fields to limit response to - * @param array $filter - * - * @return array|WP_Error - */ - public function get_order_refund( $order_id, $id, $fields = null, $filter = array() ) { - try { - // Validate order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); - } - - $order = wc_get_order( $order_id ); - $refund = wc_get_order( $id ); - - if ( ! $refund ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); - } - - $line_items = array(); - - // Add line items - foreach ( $refund->get_items( 'line_item' ) as $item_id => $item ) { - $product = $item->get_product(); - $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; - $item_meta = $item->get_formatted_meta_data( $hideprefix ); - - foreach ( $item_meta as $key => $values ) { - $item_meta[ $key ]->label = $values->display_key; - unset( $item_meta[ $key ]->display_key ); - unset( $item_meta[ $key ]->display_value ); - } - - $line_items[] = array( - 'id' => $item_id, - 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ), - 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), 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, - 'meta' => array_values( $item_meta ), - 'refunded_item_id' => (int) $item->get_meta( 'refunded_item_id' ), - ); - } - - $order_refund = array( - 'id' => $refund->get_id(), - 'created_at' => $this->server->format_datetime( $refund->get_date_created() ? $refund->get_date_created()->getTimestamp() : 0, false, false ), - 'amount' => wc_format_decimal( $refund->get_amount(), 2 ), - 'reason' => $refund->get_reason(), - 'line_items' => $line_items, - ); - - return array( 'order_refund' => apply_filters( 'woocommerce_api_order_refund_response', $order_refund, $id, $fields, $refund, $order_id, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Create a new order refund for the given order - * - * @since 2.2 - * @param string $order_id order ID - * @param array $data raw request data - * @param bool $api_refund do refund using a payment gateway API - * @return WP_Error|array error or created refund response data - */ - public function create_order_refund( $order_id, $data, $api_refund = true ) { - try { - if ( ! isset( $data['order_refund'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_refund' ), 400 ); - } - - $data = $data['order_refund']; - - // Permission check - if ( ! current_user_can( 'publish_shop_orders' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_refund', __( 'You do not have permission to create order refunds', 'woocommerce' ), 401 ); - } - - $order_id = absint( $order_id ); - - if ( empty( $order_id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); - } - - $data = apply_filters( 'woocommerce_api_create_order_refund_data', $data, $order_id, $this ); - - // Refund amount is required - if ( ! isset( $data['amount'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount is required.', 'woocommerce' ), 400 ); - } elseif ( 0 > $data['amount'] ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount must be positive.', 'woocommerce' ), 400 ); - } - - $data['order_id'] = $order_id; - $data['refund_id'] = 0; - - // Create the refund - $refund = wc_create_refund( $data ); - - if ( ! $refund ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); - } - - // Refund via API - if ( $api_refund ) { - if ( WC()->payment_gateways() ) { - $payment_gateways = WC()->payment_gateways->payment_gateways(); - } - - $order = wc_get_order( $order_id ); - - if ( isset( $payment_gateways[ $order->get_payment_method() ] ) && $payment_gateways[ $order->get_payment_method() ]->supports( 'refunds' ) ) { - $result = $payment_gateways[ $order->get_payment_method() ]->process_refund( $order_id, $refund->get_amount(), $refund->get_reason() ); - - if ( is_wp_error( $result ) ) { - return $result; - } elseif ( ! $result ) { - throw new WC_API_Exception( 'woocommerce_api_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ), 500 ); - } - } - } - - // HTTP 201 Created - $this->server->send_status( 201 ); - - do_action( 'woocommerce_api_create_order_refund', $refund->get_id(), $order_id, $this ); - - return $this->get_order_refund( $order_id, $refund->get_id() ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Edit an order refund - * - * @since 2.2 - * @param string $order_id order ID - * @param string $id refund ID - * @param array $data parsed request data - * @return WP_Error|array error or edited refund response data - */ - public function edit_order_refund( $order_id, $id, $data ) { - try { - if ( ! isset( $data['order_refund'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_refund' ), 400 ); - } - - $data = $data['order_refund']; - - // Validate order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - // Validate refund ID - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); - } - - // Ensure order ID is valid - $refund = get_post( $id ); - - if ( ! $refund ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); - } - - // Ensure refund ID is associated with given order - if ( $refund->post_parent != $order_id ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); - } - - $data = apply_filters( 'woocommerce_api_edit_order_refund_data', $data, $refund->ID, $order_id, $this ); - - // Update reason - if ( isset( $data['reason'] ) ) { - $updated_refund = wp_update_post( array( 'ID' => $refund->ID, 'post_excerpt' => $data['reason'] ) ); - - if ( is_wp_error( $updated_refund ) ) { - return $updated_refund; - } - } - - // Update refund amount - if ( isset( $data['amount'] ) && 0 < $data['amount'] ) { - update_post_meta( $refund->ID, '_refund_amount', wc_format_decimal( $data['amount'] ) ); - } - - do_action( 'woocommerce_api_edit_order_refund', $refund->ID, $order_id, $this ); - - return $this->get_order_refund( $order_id, $refund->ID ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete order refund - * - * @since 2.2 - * @param string $order_id order ID - * @param string $id refund ID - * @return WP_Error|array error or deleted message - */ - public function delete_order_refund( $order_id, $id ) { - try { - $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - // Validate refund ID - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); - } - - // Ensure refund ID is valid - $refund = get_post( $id ); - - if ( ! $refund ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); - } - - // Ensure refund ID is associated with given order - if ( $refund->post_parent != $order_id ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); - } - - wc_delete_shop_order_transients( $order_id ); - - do_action( 'woocommerce_api_delete_order_refund', $refund->ID, $order_id, $this ); - - return $this->delete( $refund->ID, 'refund', true ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Bulk update or insert orders - * Accepts an array with orders in the formats supported by - * WC_API_Orders->create_order() and WC_API_Orders->edit_order() - * - * @since 2.4.0 - * - * @param array $data - * - * @return array|WP_Error - */ - public function bulk( $data ) { - - try { - if ( ! isset( $data['orders'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_orders_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'orders' ), 400 ); - } - - $data = $data['orders']; - $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'orders' ); - - // Limit bulk operation - if ( count( $data ) > $limit ) { - throw new WC_API_Exception( 'woocommerce_api_orders_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); - } - - $orders = array(); - - foreach ( $data as $_order ) { - $order_id = 0; - - // Try to get the order ID - if ( isset( $_order['id'] ) ) { - $order_id = intval( $_order['id'] ); - } - - // Order exists / edit order - if ( $order_id ) { - $edit = $this->edit_order( $order_id, array( 'order' => $_order ) ); - - if ( is_wp_error( $edit ) ) { - $orders[] = array( - 'id' => $order_id, - 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), - ); - } else { - $orders[] = $edit['order']; - } - } else { - // Order don't exists / create order - $new = $this->create_order( array( 'order' => $_order ) ); - - if ( is_wp_error( $new ) ) { - $orders[] = array( - 'id' => $order_id, - 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), - ); - } else { - $orders[] = $new['order']; - } - } - } - - return array( 'orders' => apply_filters( 'woocommerce_api_orders_bulk_response', $orders, $this ) ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } -} diff --git a/includes/legacy/api/v2/class-wc-api-products.php b/includes/legacy/api/v2/class-wc-api-products.php deleted file mode 100644 index 35a0b4c0f98..00000000000 --- a/includes/legacy/api/v2/class-wc-api-products.php +++ /dev/null @@ -1,2312 +0,0 @@ - - * GET /products//reviews - * - * @since 2.1 - * @param array $routes - * @return array - */ - public function register_routes( $routes ) { - - # GET/POST /products - $routes[ $this->base ] = array( - array( array( $this, 'get_products' ), WC_API_Server::READABLE ), - array( array( $this, 'create_product' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET /products/count - $routes[ $this->base . '/count' ] = array( - array( array( $this, 'get_products_count' ), WC_API_Server::READABLE ), - ); - - # GET/PUT/DELETE /products/ - $routes[ $this->base . '/(?P\d+)' ] = array( - array( array( $this, 'get_product' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_product' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_product' ), WC_API_Server::DELETABLE ), - ); - - # GET /products//reviews - $routes[ $this->base . '/(?P\d+)/reviews' ] = array( - array( array( $this, 'get_product_reviews' ), WC_API_Server::READABLE ), - ); - - # GET /products//orders - $routes[ $this->base . '/(?P\d+)/orders' ] = array( - array( array( $this, 'get_product_orders' ), WC_API_Server::READABLE ), - ); - - # GET /products/categories - $routes[ $this->base . '/categories' ] = array( - array( array( $this, 'get_product_categories' ), WC_API_Server::READABLE ), - ); - - # GET /products/categories/ - $routes[ $this->base . '/categories/(?P\d+)' ] = array( - array( array( $this, 'get_product_category' ), WC_API_Server::READABLE ), - ); - - # GET/POST /products/attributes - $routes[ $this->base . '/attributes' ] = array( - array( array( $this, 'get_product_attributes' ), WC_API_Server::READABLE ), - array( array( $this, 'create_product_attribute' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET/PUT/DELETE /attributes/ - $routes[ $this->base . '/attributes/(?P\d+)' ] = array( - array( array( $this, 'get_product_attribute' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_product_attribute' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_product_attribute' ), WC_API_Server::DELETABLE ), - ); - - # GET /products/sku/ - $routes[ $this->base . '/sku/(?P\w[\w\s\-]*)' ] = array( - array( array( $this, 'get_product_by_sku' ), WC_API_Server::READABLE ), - ); - - # POST|PUT /products/bulk - $routes[ $this->base . '/bulk' ] = array( - array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - ); - - 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->get_parent_id() ) { - $_product = wc_get_product( $product->get_parent_id() ); - $product_data['parent'] = $this->get_product_data( $_product ); - } - - return array( 'product' => apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields, $this->server ) ); - } - - /** - * Get the total number of products - * - * @since 2.1 - * - * @param string $type - * @param array $filter - * - * @return array|WP_Error - */ - public function get_products_count( $type = null, $filter = array() ) { - try { - if ( ! current_user_can( 'read_private_products' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), 401 ); - } - - if ( ! empty( $type ) ) { - $filter['type'] = $type; - } - - $query = $this->query_products( $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() ) ); - } - } - - /** - * Create a new product - * - * @since 2.2 - * - * @param array $data posted data - * - * @return array|WP_Error - */ - public function create_product( $data ) { - $id = 0; - - try { - if ( ! isset( $data['product'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product' ), 400 ); - } - - $data = $data['product']; - - // Check permissions - if ( ! current_user_can( 'publish_products' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product', __( 'You do not have permission to create products', 'woocommerce' ), 401 ); - } - - $data = apply_filters( 'woocommerce_api_create_product_data', $data, $this ); - - // Check if product title is specified - if ( ! isset( $data['title'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_title', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'title' ), 400 ); - } - - // Check product type - if ( ! isset( $data['type'] ) ) { - $data['type'] = 'simple'; - } - - // Set visible visibility when not sent - if ( ! isset( $data['catalog_visibility'] ) ) { - $data['catalog_visibility'] = 'visible'; - } - - // Validate the product type - if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 ); - } - - // Enable description html tags. - $post_content = isset( $data['description'] ) ? wc_clean( $data['description'] ) : ''; - if ( $post_content && isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) { - - $post_content = wp_filter_post_kses( $data['description'] ); - } - - // Enable short description html tags. - $post_excerpt = isset( $data['short_description'] ) ? wc_clean( $data['short_description'] ) : ''; - if ( $post_excerpt && isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) { - $post_excerpt = wp_filter_post_kses( $data['short_description'] ); - } - - $classname = WC_Product_Factory::get_classname_from_product_type( $data['type'] ); - if ( ! class_exists( $classname ) ) { - $classname = 'WC_Product_Simple'; - } - $product = new $classname(); - - $product->set_name( wc_clean( $data['title'] ) ); - $product->set_status( isset( $data['status'] ) ? wc_clean( $data['status'] ) : 'publish' ); - $product->set_short_description( isset( $data['short_description'] ) ? $post_excerpt : '' ); - $product->set_description( isset( $data['description'] ) ? $post_content : '' ); - - // Attempts to create the new product. - $product->save(); - $id = $product->get_id(); - - // Checks for an error in the product creation - if ( 0 >= $id ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_product', $id->get_error_message(), 400 ); - } - - // Check for featured/gallery images, upload it and set it - if ( isset( $data['images'] ) ) { - $product = $this->save_product_images( $product, $data['images'] ); - } - - // Save product meta fields - $product = $this->save_product_meta( $product, $data ); - $product->save(); - - // Save variations - if ( isset( $data['type'] ) && 'variable' == $data['type'] && isset( $data['variations'] ) && is_array( $data['variations'] ) ) { - $this->save_variations( $product, $data ); - } - - do_action( 'woocommerce_api_create_product', $id, $data ); - - // Clear cache/transients - wc_delete_product_transients( $id ); - - $this->server->send_status( 201 ); - - return $this->get_product( $id ); - } catch ( WC_Data_Exception $e ) { - $this->clear_product( $id ); - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } catch ( WC_API_Exception $e ) { - $this->clear_product( $id ); - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Edit a product - * - * @since 2.2 - * - * @param int $id the product ID - * @param array $data - * - * @return array|WP_Error - */ - public function edit_product( $id, $data ) { - try { - if ( ! isset( $data['product'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product' ), 400 ); - } - - $data = $data['product']; - - $id = $this->validate_request( $id, 'product', 'edit' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - $product = wc_get_product( $id ); - - $data = apply_filters( 'woocommerce_api_edit_product_data', $data, $this ); - - // Product title. - if ( isset( $data['title'] ) ) { - $product->set_name( wc_clean( $data['title'] ) ); - } - - // Product name (slug). - if ( isset( $data['name'] ) ) { - $product->set_slug( wc_clean( $data['name'] ) ); - } - - // Product status. - if ( isset( $data['status'] ) ) { - $product->set_status( wc_clean( $data['status'] ) ); - } - - // Product short description. - if ( isset( $data['short_description'] ) ) { - // Enable short description html tags. - $post_excerpt = ( isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) ? wp_filter_post_kses( $data['short_description'] ) : wc_clean( $data['short_description'] ); - $product->set_short_description( $post_excerpt ); - } - - // Product description. - if ( isset( $data['description'] ) ) { - // Enable description html tags. - $post_content = ( isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) ? wp_filter_post_kses( $data['description'] ) : wc_clean( $data['description'] ); - $product->set_description( $post_content ); - } - - // Validate the product type. - if ( isset( $data['type'] ) && ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 ); - } - - // Check for featured/gallery images, upload it and set it. - if ( isset( $data['images'] ) ) { - $product = $this->save_product_images( $product, $data['images'] ); - } - - // Save product meta fields. - $product = $this->save_product_meta( $product, $data ); - - // Save variations. - if ( $product->is_type( 'variable' ) ) { - if ( isset( $data['variations'] ) && is_array( $data['variations'] ) ) { - $this->save_variations( $product, $data ); - } else { - // Just sync variations. - $product = WC_Product_Variable::sync( $product, false ); - } - } - - $product->save(); - - do_action( 'woocommerce_api_edit_product', $id, $data ); - - // Clear cache/transients. - wc_delete_product_transients( $id ); - - return $this->get_product( $id ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete a product. - * - * @since 2.2 - * - * @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; - } - - $product = wc_get_product( $id ); - - do_action( 'woocommerce_api_delete_product', $id, $this ); - - // If we're forcing, then delete permanently. - if ( $force ) { - if ( $product->is_type( 'variable' ) ) { - foreach ( $product->get_children() as $child_id ) { - $child = wc_get_product( $child_id ); - if ( ! empty( $child ) ) { - $child->delete( true ); - } - } - } else { - // For other product types, if the product has children, remove the relationship. - foreach ( $product->get_children() as $child_id ) { - $child = wc_get_product( $child_id ); - if ( ! empty( $child ) ) { - $child->set_parent_id( 0 ); - $child->save(); - } - } - } - - $product->delete( true ); - $result = ! ( $product->get_id() > 0 ); - } else { - $product->delete(); - $result = 'trash' === $product->get_status(); - } - - if ( ! $result ) { - return new WP_Error( 'woocommerce_api_cannot_delete_product', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product' ), array( 'status' => 500 ) ); - } - - // Delete parent product transients. - if ( $parent_id = wp_get_post_parent_id( $id ) ) { - wc_delete_product_transients( $parent_id ); - } - - if ( $force ) { - return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), 'product' ) ); - } else { - $this->server->send_status( '202' ); - - return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product' ) ); - } - } - - /** - * 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; - } - - $comments = get_approved_comments( $id ); - $reviews = array(); - - foreach ( $comments as $comment ) { - - $reviews[] = array( - 'id' => intval( $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 ) ); - } - - /** - * Get the orders for a product - * - * @since 2.4.0 - * @param int $id the product ID to get orders for - * @param string fields fields to retrieve - * @param array $filter filters to include in response - * @param string $status the order status to retrieve - * @param $page $page page to retrieve - * @return array|WP_Error - */ - public function get_product_orders( $id, $fields = null, $filter = array(), $status = null, $page = 1 ) { - global $wpdb; - - $id = $this->validate_request( $id, 'product', 'read' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - $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' - ", $id ) ); - - if ( empty( $order_ids ) ) { - return array( 'orders' => array() ); - } - - $filter = array_merge( $filter, array( - 'in' => implode( ',', $order_ids ), - ) ); - - $orders = WC()->api->WC_API_Orders->get_orders( $fields, $filter, $status, $page ); - - return array( 'orders' => apply_filters( 'woocommerce_api_product_orders_response', $orders['orders'], $id, $filter, $fields, $this->server ) ); - } - - /** - * Get a listing of product categories - * - * @since 2.2 - * - * @param string|null $fields fields to limit response to - * - * @return array|WP_Error - */ - public function get_product_categories( $fields = null ) { - try { - // Permissions check - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); - } - - $product_categories = array(); - - $terms = get_terms( 'product_cat', array( 'hide_empty' => false, 'fields' => 'ids' ) ); - - foreach ( $terms as $term_id ) { - $product_categories[] = current( $this->get_product_category( $term_id, $fields ) ); - } - - return array( 'product_categories' => apply_filters( 'woocommerce_api_product_categories_response', $product_categories, $terms, $fields, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get the product category for the given ID - * - * @since 2.2 - * - * @param string $id product category term ID - * @param string|null $fields fields to limit response to - * - * @return array|WP_Error - */ - public function get_product_category( $id, $fields = null ) { - try { - $id = absint( $id ); - - // Validate ID - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'Invalid product category ID', 'woocommerce' ), 400 ); - } - - // Permissions check - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); - } - - $term = get_term( $id, 'product_cat' ); - - if ( is_wp_error( $term ) || is_null( $term ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'A product category with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $term_id = intval( $term->term_id ); - - // Get category display type - $display_type = get_term_meta( $term_id, 'display_type', true ); - - // Get category image - $image = ''; - if ( $image_id = get_term_meta( $term_id, 'thumbnail_id', true ) ) { - $image = wp_get_attachment_url( $image_id ); - } - - $product_category = array( - 'id' => $term_id, - 'name' => $term->name, - 'slug' => $term->slug, - 'parent' => $term->parent, - 'description' => $term->description, - 'display' => $display_type ? $display_type : 'default', - 'image' => $image ? esc_url( $image ) : '', - 'count' => intval( $term->count ), - ); - - return array( 'product_category' => apply_filters( 'woocommerce_api_product_category_response', $product_category, $id, $fields, $term, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * 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'] ); - } - - // Filter products by category - if ( ! empty( $args['category'] ) ) { - $query_args['product_cat'] = $args['category']; - } - - // Filter by specific sku - if ( ! empty( $args['sku'] ) ) { - if ( ! is_array( $query_args['meta_query'] ) ) { - $query_args['meta_query'] = array(); - } - - $query_args['meta_query'][] = array( - 'key' => '_sku', - 'value' => $args['sku'], - 'compare' => '=', - ); - - $query_args['post_type'] = array( 'product', 'product_variation' ); - } - - $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(); - } - - $prices_precision = wc_get_price_decimals(); - 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(), $prices_precision ), - 'regular_price' => wc_format_decimal( $product->get_regular_price(), $prices_precision ), - 'sale_price' => $product->get_sale_price() ? wc_format_decimal( $product->get_sale_price(), $prices_precision ) : 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(), - 'product_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '', - 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '', - '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' => wpautop( do_shortcode( $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() ), - 'parent_id' => $product->get_parent_id(), - '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' => wpautop( do_shortcode( wp_kses_post( $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 ) { - $prices_precision = wc_get_price_decimals(); - $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(), $prices_precision ), - 'regular_price' => wc_format_decimal( $variation->get_regular_price(), $prices_precision ), - 'sale_price' => $variation->get_sale_price() ? wc_format_decimal( $variation->get_sale_price(), $prices_precision ) : null, - 'taxable' => $variation->is_taxable(), - 'tax_status' => $variation->get_tax_status(), - 'tax_class' => $variation->get_tax_class(), - 'managing_stock' => $variation->managing_stock(), - '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; - } - - /** - * Save default attributes. - * - * @since 3.0.0 - * @param WC_Product $product - * @param array $request - * @return WC_Product - */ - protected function save_default_attributes( $product, $request ) { - // Update default attributes options setting. - if ( isset( $request['default_attribute'] ) ) { - $request['default_attributes'] = $request['default_attribute']; - } - - if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { - $attributes = $product->get_attributes(); - $default_attributes = array(); - - foreach ( $request['default_attributes'] as $default_attr_key => $default_attr ) { - if ( ! isset( $default_attr['name'] ) ) { - continue; - } - - $taxonomy = sanitize_title( $default_attr['name'] ); - - if ( isset( $default_attr['slug'] ) ) { - $taxonomy = $this->get_attribute_taxonomy_by_slug( $default_attr['slug'] ); - } - - if ( isset( $attributes[ $taxonomy ] ) ) { - $_attribute = $attributes[ $taxonomy ]; - - if ( $_attribute['is_variation'] ) { - $value = ''; - - if ( isset( $default_attr['option'] ) ) { - if ( $_attribute['is_taxonomy'] ) { - // Don't use wc_clean as it destroys sanitized characters. - $value = sanitize_title( trim( stripslashes( $default_attr['option'] ) ) ); - } else { - $value = wc_clean( trim( stripslashes( $default_attr['option'] ) ) ); - } - } - - if ( $value ) { - $default_attributes[ $taxonomy ] = $value; - } - } - } - } - - $product->set_default_attributes( $default_attributes ); - } - - return $product; - } - - /** - * Save product meta - * - * @since 2.2 - * @param WC_Product $product - * @param array $data - * @return WC_Product - * @throws WC_API_Exception - */ - protected function save_product_meta( $product, $data ) { - global $wpdb; - - // Virtual - if ( isset( $data['virtual'] ) ) { - $product->set_virtual( $data['virtual'] ); - } - - // Tax status - if ( isset( $data['tax_status'] ) ) { - $product->set_tax_status( wc_clean( $data['tax_status'] ) ); - } - - // Tax Class - if ( isset( $data['tax_class'] ) ) { - $product->set_tax_class( wc_clean( $data['tax_class'] ) ); - } - - // Catalog Visibility - if ( isset( $data['catalog_visibility'] ) ) { - $product->set_catalog_visibility( wc_clean( $data['catalog_visibility'] ) ); - } - - // Purchase Note - if ( isset( $data['purchase_note'] ) ) { - $product->set_purchase_note( wc_clean( $data['purchase_note'] ) ); - } - - // Featured Product - if ( isset( $data['featured'] ) ) { - $product->set_featured( $data['featured'] ); - } - - // Shipping data - $product = $this->save_product_shipping_data( $product, $data ); - - // SKU - if ( isset( $data['sku'] ) ) { - $sku = $product->get_sku(); - $new_sku = wc_clean( $data['sku'] ); - - if ( '' == $new_sku ) { - $product->set_sku( '' ); - } elseif ( $new_sku !== $sku ) { - if ( ! empty( $new_sku ) ) { - $unique_sku = wc_product_has_unique_sku( $product->get_id(), $new_sku ); - if ( ! $unique_sku ) { - throw new WC_API_Exception( 'woocommerce_api_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400 ); - } else { - $product->set_sku( $new_sku ); - } - } else { - $product->set_sku( '' ); - } - } - } - - // Attributes - if ( isset( $data['attributes'] ) ) { - $attributes = array(); - - foreach ( $data['attributes'] as $attribute ) { - $is_taxonomy = 0; - $taxonomy = 0; - - if ( ! isset( $attribute['name'] ) ) { - continue; - } - - $attribute_slug = sanitize_title( $attribute['name'] ); - - if ( isset( $attribute['slug'] ) ) { - $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); - $attribute_slug = sanitize_title( $attribute['slug'] ); - } - - if ( $taxonomy ) { - $is_taxonomy = 1; - } - - if ( $is_taxonomy ) { - - $attribute_id = wc_attribute_taxonomy_id_by_name( $attribute['name'] ); - - 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(); - } - - // Update post terms - if ( taxonomy_exists( $taxonomy ) ) { - wp_set_object_terms( $product->get_id(), $values, $taxonomy ); - } - - 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( $taxonomy ); - $attribute_object->set_options( $values ); - $attribute_object->set_position( isset( $attribute['position'] ) ? 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'] ) ) { - // Array based - if ( is_array( $attribute['options'] ) ) { - $values = $attribute['options']; - - // Text based, separate by pipe - } else { - $values = array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) ); - } - - // Custom attribute - Add attribute to array and set the values. - $attribute_object = new WC_Product_Attribute(); - $attribute_object->set_name( $attribute['name'] ); - $attribute_object->set_options( $values ); - $attribute_object->set_position( isset( $attribute['position'] ) ? 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; - } - } - - uasort( $attributes, 'wc_product_attribute_uasort_comparison' ); - - $product->set_attributes( $attributes ); - } - - // Sales and prices. - if ( in_array( $product->get_type(), array( 'variable', 'grouped' ) ) ) { - - // Variable and grouped products have no prices. - $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( $data['regular_price'] ) ) { - $regular_price = ( '' === $data['regular_price'] ) ? '' : $data['regular_price']; - $product->set_regular_price( $regular_price ); - } - - // Sale Price. - if ( isset( $data['sale_price'] ) ) { - $sale_price = ( '' === $data['sale_price'] ) ? '' : $data['sale_price']; - $product->set_sale_price( $sale_price ); - } - - if ( isset( $data['sale_price_dates_from'] ) ) { - $date_from = $data['sale_price_dates_from']; - } else { - $date_from = $product->get_date_on_sale_from() ? date( 'Y-m-d', $product->get_date_on_sale_from()->getTimestamp() ) : ''; - } - - if ( isset( $data['sale_price_dates_to'] ) ) { - $date_to = $data['sale_price_dates_to']; - } else { - $date_to = $product->get_date_on_sale_to() ? date( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ) : ''; - } - - if ( $date_to && ! $date_from ) { - $date_from = strtotime( 'NOW', current_time( 'timestamp', true ) ); - } - - $product->set_date_on_sale_to( $date_to ); - $product->set_date_on_sale_from( $date_from ); - - if ( $product->is_on_sale( 'edit' ) ) { - $product->set_price( $product->get_sale_price( 'edit' ) ); - } else { - $product->set_price( $product->get_regular_price( 'edit' ) ); - } - } - - // Product parent ID for groups - if ( isset( $data['parent_id'] ) ) { - $product->set_parent_id( absint( $data['parent_id'] ) ); - } - - // Sold Individually - if ( isset( $data['sold_individually'] ) ) { - $product->set_sold_individually( true === $data['sold_individually'] ? 'yes' : '' ); - } - - // Stock status - if ( isset( $data['in_stock'] ) ) { - $stock_status = ( true === $data['in_stock'] ) ? 'instock' : 'outofstock'; - } else { - $stock_status = $product->get_stock_status(); - - if ( '' === $stock_status ) { - $stock_status = 'instock'; - } - } - - // Stock Data - if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) { - // Manage stock - if ( isset( $data['managing_stock'] ) ) { - $managing_stock = ( true === $data['managing_stock'] ) ? 'yes' : 'no'; - $product->set_manage_stock( $managing_stock ); - } else { - $managing_stock = $product->get_manage_stock() ? 'yes' : 'no'; - } - - // Backorders - if ( isset( $data['backorders'] ) ) { - if ( 'notify' == $data['backorders'] ) { - $backorders = 'notify'; - } else { - $backorders = ( true === $data['backorders'] ) ? 'yes' : 'no'; - } - - $product->set_backorders( $backorders ); - } else { - $backorders = $product->get_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 ( 'yes' == $managing_stock ) { - $product->set_backorders( $backorders ); - - // 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( $data['stock_quantity'] ) ) { - $product->set_stock_quantity( wc_stock_amount( $data['stock_quantity'] ) ); - } - } else { - // Don't manage stock. - $product->set_manage_stock( 'no' ); - $product->set_backorders( $backorders ); - $product->set_stock_quantity( '' ); - $product->set_stock_status( $stock_status ); - } - } elseif ( ! $product->is_type( 'variable' ) ) { - $product->set_stock_status( $stock_status ); - } - - // Upsells - if ( isset( $data['upsell_ids'] ) ) { - $upsells = array(); - $ids = $data['upsell_ids']; - - if ( ! empty( $ids ) ) { - foreach ( $ids as $id ) { - if ( $id && $id > 0 ) { - $upsells[] = $id; - } - } - - $product->set_upsell_ids( $upsells ); - } else { - $product->set_upsell_ids( array() ); - } - } - - // Cross sells - if ( isset( $data['cross_sell_ids'] ) ) { - $crosssells = array(); - $ids = $data['cross_sell_ids']; - - if ( ! empty( $ids ) ) { - foreach ( $ids as $id ) { - if ( $id && $id > 0 ) { - $crosssells[] = $id; - } - } - - $product->set_cross_sell_ids( $crosssells ); - } else { - $product->set_cross_sell_ids( array() ); - } - } - - // Product categories - if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) { - $product->set_category_ids( $data['categories'] ); - } - - // Product tags - if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) { - $product->set_tag_ids( $data['tags'] ); - } - - // Downloadable - if ( isset( $data['downloadable'] ) ) { - $is_downloadable = ( true === $data['downloadable'] ) ? 'yes' : 'no'; - $product->set_downloadable( $is_downloadable ); - } else { - $is_downloadable = $product->get_downloadable() ? 'yes' : 'no'; - } - - // Downloadable options - if ( 'yes' == $is_downloadable ) { - - // Downloadable files - if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { - $product = $this->save_downloadable_files( $product, $data['downloads'] ); - } - - // Download limit - if ( isset( $data['download_limit'] ) ) { - $product->set_download_limit( $data['download_limit'] ); - } - - // Download expiry - if ( isset( $data['download_expiry'] ) ) { - $product->set_download_expiry( $data['download_expiry'] ); - } - } - - // Product url - if ( $product->is_type( 'external' ) ) { - if ( isset( $data['product_url'] ) ) { - $product->set_product_url( $data['product_url'] ); - } - - if ( isset( $data['button_text'] ) ) { - $product->set_button_text( $data['button_text'] ); - } - } - - // Reviews allowed - if ( isset( $data['reviews_allowed'] ) ) { - $product->set_reviews_allowed( $data['reviews_allowed'] ); - } - - // Save default attributes for variable products. - if ( $product->is_type( 'variable' ) ) { - $product = $this->save_default_attributes( $product, $data ); - } - - // Do action for product type - do_action( 'woocommerce_api_process_product_meta_' . $product->get_type(), $product->get_id(), $data ); - - return $product; - } - - /** - * Save variations - * - * @since 2.2 - * @param WC_Product $product - * @param array $request - * - * @return true - * - * @throws WC_API_Exception - */ - protected function save_variations( $product, $request ) { - global $wpdb; - - $id = $product->get_id(); - $attributes = $product->get_attributes(); - - foreach ( $request['variations'] as $menu_order => $data ) { - $variation_id = isset( $data['id'] ) ? absint( $data['id'] ) : 0; - $variation = new WC_Product_Variation( $variation_id ); - - // 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 = current( $data['image'] ); - if ( is_array( $image ) ) { - $image['position'] = 0; - } - - $variation = $this->save_product_images( $variation, array( $image ) ); - } - - // Virtual variation. - if ( isset( $data['virtual'] ) ) { - $variation->set_virtual( $data['virtual'] ); - } - - // Downloadable variation. - if ( isset( $data['downloadable'] ) ) { - $is_downloadable = $data['downloadable']; - $variation->set_downloadable( $is_downloadable ); - } else { - $is_downloadable = $variation->get_downloadable(); - } - - // Downloads. - if ( $is_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. - $manage_stock = (bool) $variation->get_manage_stock(); - if ( isset( $data['managing_stock'] ) ) { - $manage_stock = $data['managing_stock']; - } - $variation->set_manage_stock( $manage_stock ); - - $stock_status = $variation->get_stock_status(); - if ( isset( $data['in_stock'] ) ) { - $stock_status = true === $data['in_stock'] ? 'instock' : 'outofstock'; - } - $variation->set_stock_status( $stock_status ); - - $backorders = $variation->get_backorders(); - if ( isset( $data['backorders'] ) ) { - $backorders = $data['backorders']; - } - $variation->set_backorders( $backorders ); - - if ( $manage_stock ) { - if ( isset( $data['stock_quantity'] ) ) { - $variation->set_stock_quantity( $data['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['sale_price_dates_from'] ) ) { - $variation->set_date_on_sale_from( $data['sale_price_dates_from'] ); - } - - if ( isset( $data['sale_price_dates_to'] ) ) { - $variation->set_date_on_sale_to( $data['sale_price_dates_to'] ); - } - - // Tax class. - if ( isset( $data['tax_class'] ) ) { - $variation->set_tax_class( $data['tax_class'] ); - } - - // Update taxonomies. - if ( isset( $data['attributes'] ) ) { - $_attributes = array(); - - foreach ( $data['attributes'] as $attribute_key => $attribute ) { - if ( ! isset( $attribute['name'] ) ) { - continue; - } - - $taxonomy = 0; - $_attribute = array(); - - if ( isset( $attribute['slug'] ) ) { - $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); - } - - if ( ! $taxonomy ) { - $taxonomy = sanitize_title( $attribute['name'] ); - } - - if ( isset( $attributes[ $taxonomy ] ) ) { - $_attribute = $attributes[ $taxonomy ]; - } - - if ( isset( $_attribute['is_variation'] ) && $_attribute['is_variation'] ) { - $_attribute_key = sanitize_title( $_attribute['name'] ); - - if ( isset( $_attribute['is_taxonomy'] ) && $_attribute['is_taxonomy'] ) { - // Don't use wc_clean as it destroys sanitized characters - $_attribute_value = isset( $attribute['option'] ) ? sanitize_title( stripslashes( $attribute['option'] ) ) : ''; - } else { - $_attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; - } - - $_attributes[ $_attribute_key ] = $_attribute_value; - } - } - - $variation->set_attributes( $_attributes ); - } - - $variation->save(); - - do_action( 'woocommerce_api_save_product_variation', $variation_id, $menu_order, $variation ); - } - - return true; - } - - /** - * Save product shipping data - * - * @since 2.2 - * @param WC_Product $product - * @param array $data - * @return WC_Product - */ - private function save_product_shipping_data( $product, $data ) { - if ( isset( $data['weight'] ) ) { - $product->set_weight( '' === $data['weight'] ? '' : wc_format_decimal( $data['weight'] ) ); - } - - // Product dimensions - if ( isset( $data['dimensions'] ) ) { - // Height - if ( isset( $data['dimensions']['height'] ) ) { - $product->set_height( '' === $data['dimensions']['height'] ? '' : wc_format_decimal( $data['dimensions']['height'] ) ); - } - - // Width - if ( isset( $data['dimensions']['width'] ) ) { - $product->set_width( '' === $data['dimensions']['width'] ? '' : wc_format_decimal( $data['dimensions']['width'] ) ); - } - - // Length - if ( isset( $data['dimensions']['length'] ) ) { - $product->set_length( '' === $data['dimensions']['length'] ? '' : wc_format_decimal( $data['dimensions']['length'] ) ); - } - } - - // Virtual - if ( isset( $data['virtual'] ) ) { - $virtual = ( true === $data['virtual'] ) ? 'yes' : 'no'; - - if ( 'yes' == $virtual ) { - $product->set_weight( '' ); - $product->set_height( '' ); - $product->set_length( '' ); - $product->set_width( '' ); - } - } - - // Shipping class - if ( isset( $data['shipping_class'] ) ) { - $data_store = $product->get_data_store(); - $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) ); - $product->set_shipping_class_id( $shipping_class_id ); - } - - return $product; - } - - /** - * Save downloadable files - * - * @since 2.2 - * @param WC_Product $product - * @param array $downloads - * @param int $deprecated Deprecated since 3.0. - * @return WC_Product - */ - private function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { - if ( $deprecated ) { - wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() does not require a variation_id anymore.' ); - } - - $files = array(); - foreach ( $downloads as $key => $file ) { - if ( isset( $file['url'] ) ) { - $file['file'] = $file['url']; - } - - if ( empty( $file['file'] ) ) { - continue; - } - - $download = new WC_Product_Download(); - $download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() ); - $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); - $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); - $files[] = $download; - } - $product->set_downloads( $files ); - - return $product; - } - - /** - * Get attribute taxonomy by slug. - * - * @since 2.2 - * @param string $slug - * @return string|null - */ - private function get_attribute_taxonomy_by_slug( $slug ) { - $taxonomy = null; - $attribute_taxonomies = wc_get_attribute_taxonomies(); - - foreach ( $attribute_taxonomies as $key => $tax ) { - if ( $slug == $tax->attribute_name ) { - $taxonomy = 'pa_' . $tax->attribute_name; - - break; - } - } - - return $taxonomy; - } - - /** - * 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' => (int) $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; - } - - /** - * Save product images - * - * @since 2.2 - * - * @param WC_Product $product - * @param array $images - * - * @return WC_Product - * @throws WC_API_Exception - */ - protected function save_product_images( $product, $images ) { - if ( is_array( $images ) ) { - $gallery = array(); - - foreach ( $images as $image ) { - if ( isset( $image['position'] ) && 0 == $image['position'] ) { - $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; - - if ( 0 === $attachment_id && isset( $image['src'] ) ) { - $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); - - if ( is_wp_error( $upload ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 ); - } - - $attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() ); - } - - $product->set_image_id( $attachment_id ); - } else { - $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; - - if ( 0 === $attachment_id && isset( $image['src'] ) ) { - $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); - - if ( is_wp_error( $upload ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 ); - } - - $gallery[] = $this->set_product_image_as_attachment( $upload, $product->get_id() ); - } else { - $gallery[] = $attachment_id; - } - } - } - - if ( ! empty( $gallery ) ) { - $product->set_gallery_image_ids( $gallery ); - } - } else { - $product->set_image_id( '' ); - $product->set_gallery_image_ids( array() ); - } - - return $product; - } - - /** - * Upload image from URL - * - * @since 2.2 - * - * @param string $image_url - * - * @return array - * - * @throws WC_API_Exception - */ - public function upload_product_image( $image_url ) { - $upload = wc_rest_upload_image_from_url( $image_url ); - if ( is_wp_error( $upload ) ) { - throw new WC_API_Exception( 'woocommerce_api_product_image_upload_error', $upload->get_error_message(), 400 ); - } - - return $upload; - } - - /** - * Sets product image as attachment and returns the attachment ID. - * - * @since 2.2 - * @param array $upload - * @param int $id - * @return int - */ - protected function set_product_image_as_attachment( $upload, $id ) { - $info = wp_check_filetype( $upload['file'] ); - $title = ''; - $content = ''; - - if ( $image_meta = @wp_read_image_metadata( $upload['file'] ) ) { - if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { - $title = wc_clean( $image_meta['title'] ); - } - if ( trim( $image_meta['caption'] ) ) { - $content = wc_clean( $image_meta['caption'] ); - } - } - - $attachment = array( - 'post_mime_type' => $info['type'], - 'guid' => $upload['url'], - 'post_parent' => $id, - 'post_title' => $title, - 'post_content' => $content, - ); - - $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id ); - if ( ! is_wp_error( $attachment_id ) ) { - wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) ); - } - - return $attachment_id; - } - - /** - * 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' => wc_attribute_label( str_replace( 'attribute_', '', $attribute_name ) ), - 'slug' => str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $attribute_name ) ), - 'option' => $attribute, - ); - } - } else { - - foreach ( $product->get_attributes() as $attribute ) { - $attributes[] = array( - 'name' => wc_attribute_label( $attribute['name'] ), - 'slug' => wc_attribute_taxonomy_slug( $attribute['name'] ), - 'position' => (int) $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; - } - - /** - * Get a listing of product attributes - * - * @since 2.4.0 - * - * @param string|null $fields fields to limit response to - * - * @return array|WP_Error - */ - public function get_product_attributes( $fields = null ) { - try { - // Permissions check - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 ); - } - - $product_attributes = array(); - $attribute_taxonomies = wc_get_attribute_taxonomies(); - - foreach ( $attribute_taxonomies as $attribute ) { - $product_attributes[] = array( - 'id' => intval( $attribute->attribute_id ), - 'name' => $attribute->attribute_label, - 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ), - 'type' => $attribute->attribute_type, - 'order_by' => $attribute->attribute_orderby, - 'has_archives' => (bool) $attribute->attribute_public, - ); - } - - return array( 'product_attributes' => apply_filters( 'woocommerce_api_product_attributes_response', $product_attributes, $attribute_taxonomies, $fields, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get the product attribute for the given ID - * - * @since 2.4.0 - * - * @param string $id product attribute term ID - * @param string|null $fields fields to limit response to - * - * @return array|WP_Error - */ - public function get_product_attribute( $id, $fields = null ) { - global $wpdb; - - try { - $id = absint( $id ); - - // Validate ID - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 ); - } - - // Permissions check - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 ); - } - - $attribute = $wpdb->get_row( $wpdb->prepare( " - SELECT * - FROM {$wpdb->prefix}woocommerce_attribute_taxonomies - WHERE attribute_id = %d - ", $id ) ); - - if ( is_wp_error( $attribute ) || is_null( $attribute ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $product_attribute = array( - 'id' => intval( $attribute->attribute_id ), - 'name' => $attribute->attribute_label, - 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ), - 'type' => $attribute->attribute_type, - 'order_by' => $attribute->attribute_orderby, - 'has_archives' => (bool) $attribute->attribute_public, - ); - - return array( 'product_attribute' => apply_filters( 'woocommerce_api_product_attribute_response', $product_attribute, $id, $fields, $attribute, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Validate attribute data. - * - * @since 2.4.0 - * @param string $name - * @param string $slug - * @param string $type - * @param string $order_by - * @param bool $new_data - * @return bool - * @throws WC_API_Exception - */ - protected function validate_attribute_data( $name, $slug, $type, $order_by, $new_data = true ) { - if ( empty( $name ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 ); - } - - if ( strlen( $slug ) >= 28 ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), 400 ); - } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), 400 ); - } elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), 400 ); - } - - // Validate the attribute type - if ( ! in_array( wc_clean( $type ), array_keys( wc_get_attribute_types() ) ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_type', sprintf( __( 'Invalid product attribute type - the product attribute type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_attribute_types() ) ) ), 400 ); - } - - // Validate the attribute order by - if ( ! in_array( wc_clean( $order_by ), array( 'menu_order', 'name', 'name_num', 'id' ) ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_order_by', sprintf( __( 'Invalid product attribute order_by type - the product attribute order_by type must be any of these: %s', 'woocommerce' ), implode( ', ', array( 'menu_order', 'name', 'name_num', 'id' ) ) ), 400 ); - } - - return true; - } - - /** - * Create a new product attribute - * - * @since 2.4.0 - * - * @param array $data posted data - * - * @return array|WP_Error - */ - public function create_product_attribute( $data ) { - global $wpdb; - - try { - if ( ! isset( $data['product_attribute'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute' ), 400 ); - } - - $data = $data['product_attribute']; - - // Check permissions - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 ); - } - - $data = apply_filters( 'woocommerce_api_create_product_attribute_data', $data, $this ); - - if ( ! isset( $data['name'] ) ) { - $data['name'] = ''; - } - - // Set the attribute slug - if ( ! isset( $data['slug'] ) ) { - $data['slug'] = wc_sanitize_taxonomy_name( stripslashes( $data['name'] ) ); - } else { - $data['slug'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ) ); - } - - // Set attribute type when not sent - if ( ! isset( $data['type'] ) ) { - $data['type'] = 'select'; - } - - // Set order by when not sent - if ( ! isset( $data['order_by'] ) ) { - $data['order_by'] = 'menu_order'; - } - - // Validate the attribute data - $this->validate_attribute_data( $data['name'], $data['slug'], $data['type'], $data['order_by'], true ); - - $insert = $wpdb->insert( - $wpdb->prefix . 'woocommerce_attribute_taxonomies', - array( - 'attribute_label' => $data['name'], - 'attribute_name' => $data['slug'], - 'attribute_type' => $data['type'], - 'attribute_orderby' => $data['order_by'], - 'attribute_public' => isset( $data['has_archives'] ) && true === $data['has_archives'] ? 1 : 0, - ), - array( '%s', '%s', '%s', '%s', '%d' ) - ); - - // Checks for an error in the product creation - if ( is_wp_error( $insert ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $insert->get_error_message(), 400 ); - } - - $id = $wpdb->insert_id; - - do_action( 'woocommerce_api_create_product_attribute', $id, $data ); - - // Clear transients - delete_transient( 'wc_attribute_taxonomies' ); - WC_Cache_Helper::incr_cache_prefix( 'woocommerce-attributes' ); - - $this->server->send_status( 201 ); - - return $this->get_product_attribute( $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Edit a product attribute - * - * @since 2.4.0 - * - * @param int $id the attribute ID - * @param array $data - * - * @return array|WP_Error - */ - public function edit_product_attribute( $id, $data ) { - global $wpdb; - - try { - if ( ! isset( $data['product_attribute'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute' ), 400 ); - } - - $id = absint( $id ); - $data = $data['product_attribute']; - - // Check permissions - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 ); - } - - $data = apply_filters( 'woocommerce_api_edit_product_attribute_data', $data, $this ); - $attribute = $this->get_product_attribute( $id ); - - if ( is_wp_error( $attribute ) ) { - return $attribute; - } - - $attribute_name = isset( $data['name'] ) ? $data['name'] : $attribute['product_attribute']['name']; - $attribute_type = isset( $data['type'] ) ? $data['type'] : $attribute['product_attribute']['type']; - $attribute_order_by = isset( $data['order_by'] ) ? $data['order_by'] : $attribute['product_attribute']['order_by']; - - if ( isset( $data['slug'] ) ) { - $attribute_slug = wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ); - } else { - $attribute_slug = $attribute['product_attribute']['slug']; - } - $attribute_slug = preg_replace( '/^pa\_/', '', $attribute_slug ); - - if ( isset( $data['has_archives'] ) ) { - $attribute_public = true === $data['has_archives'] ? 1 : 0; - } else { - $attribute_public = $attribute['product_attribute']['has_archives']; - } - - // Validate the attribute data - $this->validate_attribute_data( $attribute_name, $attribute_slug, $attribute_type, $attribute_order_by, false ); - - $update = $wpdb->update( - $wpdb->prefix . 'woocommerce_attribute_taxonomies', - array( - 'attribute_label' => $attribute_name, - 'attribute_name' => $attribute_slug, - 'attribute_type' => $attribute_type, - 'attribute_orderby' => $attribute_order_by, - 'attribute_public' => $attribute_public, - ), - array( 'attribute_id' => $id ), - array( '%s', '%s', '%s', '%s', '%d' ), - array( '%d' ) - ); - - // Checks for an error in the product creation - if ( false === $update ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute', __( 'Could not edit the attribute', 'woocommerce' ), 400 ); - } - - do_action( 'woocommerce_api_edit_product_attribute', $id, $data ); - - // Clear transients - delete_transient( 'wc_attribute_taxonomies' ); - WC_Cache_Helper::incr_cache_prefix( 'woocommerce-attributes' ); - - return $this->get_product_attribute( $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete a product attribute - * - * @since 2.4.0 - * - * @param int $id the product attribute ID - * - * @return array|WP_Error - */ - public function delete_product_attribute( $id ) { - global $wpdb; - - try { - // Check permissions - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute', __( 'You do not have permission to delete product attributes', 'woocommerce' ), 401 ); - } - - $id = absint( $id ); - - $attribute_name = $wpdb->get_var( $wpdb->prepare( " - SELECT attribute_name - FROM {$wpdb->prefix}woocommerce_attribute_taxonomies - WHERE attribute_id = %d - ", $id ) ); - - if ( is_null( $attribute_name ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $deleted = $wpdb->delete( - $wpdb->prefix . 'woocommerce_attribute_taxonomies', - array( 'attribute_id' => $id ), - array( '%d' ) - ); - - if ( false === $deleted ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute', __( 'Could not delete the attribute', 'woocommerce' ), 401 ); - } - - $taxonomy = wc_attribute_taxonomy_name( $attribute_name ); - - if ( taxonomy_exists( $taxonomy ) ) { - $terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' ); - foreach ( $terms as $term ) { - wp_delete_term( $term->term_id, $taxonomy ); - } - } - - do_action( 'woocommerce_attribute_deleted', $id, $attribute_name, $taxonomy ); - do_action( 'woocommerce_api_delete_product_attribute', $id, $this ); - - // Clear transients - delete_transient( 'wc_attribute_taxonomies' ); - WC_Cache_Helper::incr_cache_prefix( 'woocommerce-attributes' ); - - return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get product by SKU - * - * @deprecated 2.4.0 - * - * @since 2.3.0 - * - * @param int $sku the product SKU - * @param string $fields - * - * @return array|WP_Error - */ - public function get_product_by_sku( $sku, $fields = null ) { - try { - $id = wc_get_product_id_by_sku( $sku ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_sku', __( 'Invalid product SKU', 'woocommerce' ), 404 ); - } - - return $this->get_product( $id, $fields ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Clear product - * - * @param int $product_id - */ - protected function clear_product( $product_id ) { - if ( ! is_numeric( $product_id ) || 0 >= $product_id ) { - return; - } - - // Delete product attachments - $attachments = get_children( array( - 'post_parent' => $product_id, - 'post_status' => 'any', - 'post_type' => 'attachment', - ) ); - - foreach ( (array) $attachments as $attachment ) { - wp_delete_attachment( $attachment->ID, true ); - } - - // Delete product - $product = wc_get_product( $product_id ); - $product->delete(); - } - - /** - * Bulk update or insert products - * Accepts an array with products in the formats supported by - * WC_API_Products->create_product() and WC_API_Products->edit_product() - * - * @since 2.4.0 - * - * @param array $data - * - * @return array|WP_Error - */ - public function bulk( $data ) { - - try { - if ( ! isset( $data['products'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_products_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'products' ), 400 ); - } - - $data = $data['products']; - $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'products' ); - - // Limit bulk operation - if ( count( $data ) > $limit ) { - throw new WC_API_Exception( 'woocommerce_api_products_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); - } - - $products = array(); - - foreach ( $data as $_product ) { - $product_id = 0; - $product_sku = ''; - - // Try to get the product ID - if ( isset( $_product['id'] ) ) { - $product_id = intval( $_product['id'] ); - } - - if ( ! $product_id && isset( $_product['sku'] ) ) { - $product_sku = wc_clean( $_product['sku'] ); - $product_id = wc_get_product_id_by_sku( $product_sku ); - } - - if ( $product_id ) { - - // Product exists / edit product - $edit = $this->edit_product( $product_id, array( 'product' => $_product ) ); - - if ( is_wp_error( $edit ) ) { - $products[] = array( - 'id' => $product_id, - 'sku' => $product_sku, - 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), - ); - } else { - $products[] = $edit['product']; - } - } else { - - // Product don't exists / create product - $new = $this->create_product( array( 'product' => $_product ) ); - - if ( is_wp_error( $new ) ) { - $products[] = array( - 'id' => $product_id, - 'sku' => $product_sku, - 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), - ); - } else { - $products[] = $new['product']; - } - } - } - - return array( 'products' => apply_filters( 'woocommerce_api_products_bulk_response', $products, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } -} diff --git a/includes/legacy/api/v2/class-wc-api-reports.php b/includes/legacy/api/v2/class-wc-api-reports.php deleted file mode 100644 index 8387a2e7b9b..00000000000 --- a/includes/legacy/api/v2/class-wc-api-reports.php +++ /dev/null @@ -1,329 +0,0 @@ -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; - } - } -} diff --git a/includes/legacy/api/v2/class-wc-api-resource.php b/includes/legacy/api/v2/class-wc-api-resource.php deleted file mode 100644 index cd2a9ecde6f..00000000000 --- a/includes/legacy/api/v2/class-wc-api-resource.php +++ /dev/null @@ -1,466 +0,0 @@ -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 - * `_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; - } - } -} diff --git a/includes/legacy/api/v2/class-wc-api-server.php b/includes/legacy/api/v2/class-wc-api-server.php deleted file mode 100644 index 4df7e3bfcb3..00000000000 --- a/includes/legacy/api/v2/class-wc-api-server.php +++ /dev/null @@ -1,775 +0,0 @@ - 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; - } -} diff --git a/includes/legacy/api/v2/class-wc-api-webhooks.php b/includes/legacy/api/v2/class-wc-api-webhooks.php deleted file mode 100644 index 83121936eaf..00000000000 --- a/includes/legacy/api/v2/class-wc-api-webhooks.php +++ /dev/null @@ -1,509 +0,0 @@ -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/ - $routes[ $this->base . '/(?P\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//deliveries - $routes[ $this->base . '/(?P\d+)/deliveries' ] = array( - array( array( $this, 'get_webhook_deliveries' ), WC_API_Server::READABLE ), - ); - - # GET /webhooks//deliveries/ - $routes[ $this->base . '/(?P\d+)/deliveries/(?P\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; - } -} diff --git a/includes/legacy/api/v2/interface-wc-api-handler.php b/includes/legacy/api/v2/interface-wc-api-handler.php deleted file mode 100644 index 484f9f57f02..00000000000 --- a/includes/legacy/api/v2/interface-wc-api-handler.php +++ /dev/null @@ -1,47 +0,0 @@ -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' ) - ); - } -} diff --git a/includes/legacy/api/v3/class-wc-api-coupons.php b/includes/legacy/api/v3/class-wc-api-coupons.php deleted file mode 100644 index 43c71cb817c..00000000000 --- a/includes/legacy/api/v3/class-wc-api-coupons.php +++ /dev/null @@ -1,576 +0,0 @@ - - * - * @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/ - $routes[ $this->base . '/(?P\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/, note that coupon codes can contain spaces, dashes and underscores - $routes[ $this->base . '/code/(?P\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() ) ); - } - } -} diff --git a/includes/legacy/api/v3/class-wc-api-customers.php b/includes/legacy/api/v3/class-wc-api-customers.php deleted file mode 100644 index 8e6c5a13d0b..00000000000 --- a/includes/legacy/api/v3/class-wc-api-customers.php +++ /dev/null @@ -1,829 +0,0 @@ - - * GET /customers//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/ - $routes[ $this->base . '/(?P\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/ - $routes[ $this->base . '/email/(?P.+)' ] = array( - array( array( $this, 'get_customer_by_email' ), WC_API_SERVER::READABLE ), - ); - - # GET /customers//orders - $routes[ $this->base . '/(?P\d+)/orders' ] = array( - array( array( $this, 'get_customer_orders' ), WC_API_SERVER::READABLE ), - ); - - # GET /customers//downloads - $routes[ $this->base . '/(?P\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() ) ); - } - } -} diff --git a/includes/legacy/api/v3/class-wc-api-exception.php b/includes/legacy/api/v3/class-wc-api-exception.php deleted file mode 100644 index 834ed04d6eb..00000000000 --- a/includes/legacy/api/v3/class-wc-api-exception.php +++ /dev/null @@ -1,48 +0,0 @@ -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; - } -} diff --git a/includes/legacy/api/v3/class-wc-api-json-handler.php b/includes/legacy/api/v3/class-wc-api-json-handler.php deleted file mode 100644 index 672aa8850c2..00000000000 --- a/includes/legacy/api/v3/class-wc-api-json-handler.php +++ /dev/null @@ -1,73 +0,0 @@ -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 ); - } -} diff --git a/includes/legacy/api/v3/class-wc-api-orders.php b/includes/legacy/api/v3/class-wc-api-orders.php deleted file mode 100644 index aa2f69219f6..00000000000 --- a/includes/legacy/api/v3/class-wc-api-orders.php +++ /dev/null @@ -1,1877 +0,0 @@ - - * GET /orders//notes - * - * @since 2.1 - * @param array $routes - * @return array - */ - public function register_routes( $routes ) { - - # GET|POST /orders - $routes[ $this->base ] = array( - array( array( $this, 'get_orders' ), WC_API_Server::READABLE ), - array( array( $this, 'create_order' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET /orders/count - $routes[ $this->base . '/count' ] = array( - array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ), - ); - - # GET /orders/statuses - $routes[ $this->base . '/statuses' ] = array( - array( array( $this, 'get_order_statuses' ), WC_API_Server::READABLE ), - ); - - # GET|PUT|DELETE /orders/ - $routes[ $this->base . '/(?P\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 ), - array( array( $this, 'delete_order' ), WC_API_Server::DELETABLE ), - ); - - # GET|POST /orders//notes - $routes[ $this->base . '/(?P\d+)/notes' ] = array( - array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ), - array( array( $this, 'create_order_note' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET|PUT|DELETE /orders//notes/ - $routes[ $this->base . '/(?P\d+)/notes/(?P\d+)' ] = array( - array( array( $this, 'get_order_note' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_order_note' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_order_note' ), WC_API_SERVER::DELETABLE ), - ); - - # GET|POST /orders//refunds - $routes[ $this->base . '/(?P\d+)/refunds' ] = array( - array( array( $this, 'get_order_refunds' ), WC_API_Server::READABLE ), - array( array( $this, 'create_order_refund' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET|PUT|DELETE /orders//refunds/ - $routes[ $this->base . '/(?P\d+)/refunds/(?P\d+)' ] = array( - array( array( $this, 'get_order_refund' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_order_refund' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_order_refund' ), WC_API_SERVER::DELETABLE ), - ); - - # POST|PUT /orders/bulk - $routes[ $this->base . '/bulk' ] = array( - array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - ); - - 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, $filter ) ); - } - - $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 Request fields. - * @param array $filter Request filters. - * @return array|WP_Error - */ - public function get_order( $id, $fields = null, $filter = array() ) { - - // Ensure order ID is valid & user has permission to read. - $id = $this->validate_request( $id, $this->post_type, 'read' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - // Get the decimal precession. - $dp = ( isset( $filter['dp'] ) ? intval( $filter['dp'] ) : 2 ); - $order = wc_get_order( $id ); - $expand = array(); - - if ( ! empty( $filter['expand'] ) ) { - $expand = explode( ',', $filter['expand'] ); - } - - $order_data = array( - 'id' => $order->get_id(), - 'order_number' => $order->get_order_number(), - 'order_key' => $order->get_order_key(), - '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(), $dp ), - 'subtotal' => wc_format_decimal( $order->get_subtotal(), $dp ), - 'total_line_items_quantity' => $order->get_item_count(), - 'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ), - 'total_shipping' => wc_format_decimal( $order->get_shipping_total(), $dp ), - 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ), - 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ), - 'total_discount' => wc_format_decimal( $order->get_total_discount(), $dp ), - '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(); - $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; - $item_meta = $item->get_formatted_meta_data( $hideprefix ); - - foreach ( $item_meta as $key => $values ) { - $item_meta[ $key ]->label = $values->display_key; - unset( $item_meta[ $key ]->display_key ); - unset( $item_meta[ $key ]->display_value ); - } - - $line_item = array( - 'id' => $item_id, - 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ), - 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), $dp ), - 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), - 'total_tax' => wc_format_decimal( $item->get_total_tax(), $dp ), - 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), - '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, - 'meta' => array_values( $item_meta ), - ); - - if ( in_array( 'products', $expand ) && is_object( $product ) ) { - $_product_data = WC()->api->WC_API_Products->get_product( $product->get_id() ); - - if ( isset( $_product_data['product'] ) ) { - $line_item['product_data'] = $_product_data['product']; - } - } - - $order_data['line_items'][] = $line_item; - } - - // 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(), $dp ), - ); - } - - // Add taxes. - foreach ( $order->get_tax_totals() as $tax_code => $tax ) { - $tax_line = array( - 'id' => $tax->id, - 'rate_id' => $tax->rate_id, - 'code' => $tax_code, - 'title' => $tax->label, - 'total' => wc_format_decimal( $tax->amount, $dp ), - 'compound' => (bool) $tax->is_compound, - ); - - if ( in_array( 'taxes', $expand ) ) { - $_rate_data = WC()->api->WC_API_Taxes->get_tax( $tax->rate_id ); - - if ( isset( $_rate_data['tax'] ) ) { - $tax_line['rate_data'] = $_rate_data['tax']; - } - } - - $order_data['tax_lines'][] = $tax_line; - } - - // 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 ), $dp ), - 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ), - ); - } - - // Add coupons. - foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { - $coupon_line = array( - 'id' => $coupon_item_id, - 'code' => $coupon_item->get_code(), - 'amount' => wc_format_decimal( $coupon_item->get_discount(), $dp ), - ); - - if ( in_array( 'coupons', $expand ) ) { - $_coupon_data = WC()->api->WC_API_Coupons->get_coupon_by_code( $coupon_item->get_code() ); - - if ( ! is_wp_error( $_coupon_data ) && isset( $_coupon_data['coupon'] ) ) { - $coupon_line['coupon_data'] = $_coupon_data['coupon']; - } - } - - $order_data['coupon_lines'][] = $coupon_line; - } - - return array( 'order' => apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server ) ); - } - - /** - * Get the total number of orders - * - * @since 2.4 - * - * @param string $status - * @param array $filter - * - * @return array|WP_Error - */ - public function get_orders_count( $status = null, $filter = array() ) { - - try { - if ( ! current_user_can( 'read_private_shop_orders' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), 401 ); - } - - if ( ! empty( $status ) ) { - - if ( 'any' === $status ) { - - $order_statuses = array(); - - foreach ( wc_get_order_statuses() as $slug => $name ) { - $filter['status'] = str_replace( 'wc-', '', $slug ); - $query = $this->query_orders( $filter ); - $order_statuses[ str_replace( 'wc-', '', $slug ) ] = (int) $query->found_posts; - } - - return array( 'count' => $order_statuses ); - - } else { - $filter['status'] = $status; - } - } - - $query = $this->query_orders( $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 a list of valid order statuses - * - * Note this requires no specific permissions other than being an authenticated - * API user. Order statuses (particularly custom statuses) could be considered - * private information which is why it's not in the API index. - * - * @since 2.1 - * @return array - */ - public function get_order_statuses() { - - $order_statuses = array(); - - foreach ( wc_get_order_statuses() as $slug => $name ) { - $order_statuses[ str_replace( 'wc-', '', $slug ) ] = $name; - } - - return array( 'order_statuses' => apply_filters( 'woocommerce_api_order_statuses_response', $order_statuses, $this ) ); - } - - /** - * Create an order - * - * @since 2.2 - * @param array $data raw order data - * @return array|WP_Error - */ - public function create_order( $data ) { - global $wpdb; - - try { - if ( ! isset( $data['order'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order' ), 400 ); - } - - $data = $data['order']; - - // permission check - if ( ! current_user_can( 'publish_shop_orders' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order', __( 'You do not have permission to create orders', 'woocommerce' ), 401 ); - } - - $data = apply_filters( 'woocommerce_api_create_order_data', $data, $this ); - - // default order args, note that status is checked for validity in wc_create_order() - $default_order_args = array( - 'status' => isset( $data['status'] ) ? $data['status'] : '', - 'customer_note' => isset( $data['note'] ) ? $data['note'] : null, - ); - - // if creating order for existing customer - if ( ! empty( $data['customer_id'] ) ) { - - // make sure customer exists - if ( false === get_user_by( 'id', $data['customer_id'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); - } - - $default_order_args['customer_id'] = $data['customer_id']; - } - - // create the pending order - $order = $this->create_base_order( $default_order_args, $data ); - - if ( is_wp_error( $order ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_order', sprintf( __( 'Cannot create order: %s', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 ); - } - - // billing/shipping addresses - $this->set_order_addresses( $order, $data ); - - $lines = array( - 'line_item' => 'line_items', - 'shipping' => 'shipping_lines', - 'fee' => 'fee_lines', - 'coupon' => 'coupon_lines', - ); - - foreach ( $lines as $line_type => $line ) { - - if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { - - $set_item = "set_{$line_type}"; - - foreach ( $data[ $line ] as $item ) { - - $this->$set_item( $order, $item, 'create' ); - } - } - } - - // set is vat exempt - if ( isset( $data['is_vat_exempt'] ) ) { - update_post_meta( $order->get_id(), '_is_vat_exempt', $data['is_vat_exempt'] ? 'yes' : 'no' ); - } - - // calculate totals and set them - $order->calculate_totals(); - - // payment method (and payment_complete() if `paid` == true) - if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { - - // method ID & title are required - if ( empty( $data['payment_details']['method_id'] ) || empty( $data['payment_details']['method_title'] ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_payment_details', __( 'Payment method ID and title are required', 'woocommerce' ), 400 ); - } - - update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); - update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); - - // mark as paid if set - if ( isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { - $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); - } - } - - // set order currency - if ( isset( $data['currency'] ) ) { - - if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); - } - - update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); - } - - // set order meta - if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { - $this->set_order_meta( $order->get_id(), $data['order_meta'] ); - } - - // HTTP 201 Created - $this->server->send_status( 201 ); - - wc_delete_shop_order_transients( $order ); - - do_action( 'woocommerce_api_create_order', $order->get_id(), $data, $this ); - do_action( 'woocommerce_new_order', $order->get_id() ); - - return $this->get_order( $order->get_id() ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Creates new WC_Order. - * - * Requires a separate function for classes that extend WC_API_Orders. - * - * @since 2.3 - * - * @param $args array - * @param $data - * - * @return WC_Order - */ - protected function create_base_order( $args, $data ) { - return wc_create_order( $args ); - } - - /** - * Edit an order - * - * @since 2.2 - * @param int $id the order ID - * @param array $data - * @return array|WP_Error - */ - public function edit_order( $id, $data ) { - try { - if ( ! isset( $data['order'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order' ), 400 ); - } - - $data = $data['order']; - - $update_totals = false; - - $id = $this->validate_request( $id, $this->post_type, 'edit' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - $data = apply_filters( 'woocommerce_api_edit_order_data', $data, $id, $this ); - $order = wc_get_order( $id ); - - if ( empty( $order ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); - } - - $order_args = array( 'order_id' => $order->get_id() ); - - // Customer note. - if ( isset( $data['note'] ) ) { - $order_args['customer_note'] = $data['note']; - } - - // Customer ID. - if ( isset( $data['customer_id'] ) && $data['customer_id'] != $order->get_user_id() ) { - // Make sure customer exists. - if ( false === get_user_by( 'id', $data['customer_id'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); - } - - update_post_meta( $order->get_id(), '_customer_user', $data['customer_id'] ); - } - - // Billing/shipping address. - $this->set_order_addresses( $order, $data ); - - $lines = array( - 'line_item' => 'line_items', - 'shipping' => 'shipping_lines', - 'fee' => 'fee_lines', - 'coupon' => 'coupon_lines', - ); - - foreach ( $lines as $line_type => $line ) { - - if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { - - $update_totals = true; - - foreach ( $data[ $line ] as $item ) { - // Item ID is always required. - if ( ! array_key_exists( 'id', $item ) ) { - $item['id'] = null; - } - - // Create item. - if ( is_null( $item['id'] ) ) { - $this->set_item( $order, $line_type, $item, 'create' ); - } elseif ( $this->item_is_null( $item ) ) { - // Delete item. - wc_delete_order_item( $item['id'] ); - } else { - // Update item. - $this->set_item( $order, $line_type, $item, 'update' ); - } - } - } - } - - // Payment method (and payment_complete() if `paid` == true and order needs payment). - if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { - - // Method ID. - if ( isset( $data['payment_details']['method_id'] ) ) { - update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); - } - - // Method title. - if ( isset( $data['payment_details']['method_title'] ) ) { - update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); - } - - // Mark as paid if set. - if ( $order->needs_payment() && isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { - $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); - } - } - - // Set order currency. - if ( isset( $data['currency'] ) ) { - if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); - } - - update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); - } - - // If items have changed, recalculate order totals. - if ( $update_totals ) { - $order->calculate_totals(); - } - - // Update order meta. - if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { - $this->set_order_meta( $order->get_id(), $data['order_meta'] ); - } - - // Update the order post to set customer note/modified date. - wc_update_order( $order_args ); - - // Order status. - if ( ! empty( $data['status'] ) ) { - // Refresh the order instance. - $order = wc_get_order( $order->get_id() ); - $order->update_status( $data['status'], isset( $data['status_note'] ) ? $data['status_note'] : '', true ); - } - - wc_delete_shop_order_transients( $order ); - - do_action( 'woocommerce_api_edit_order', $order->get_id(), $data, $this ); - do_action( 'woocommerce_update_order', $order->get_id() ); - - return $this->get_order( $id ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete an order - * - * @param int $id the order ID - * @param bool $force true to permanently delete order, false to move to trash - * @return array|WP_Error - */ - public function delete_order( $id, $force = false ) { - - $id = $this->validate_request( $id, $this->post_type, 'delete' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - wc_delete_shop_order_transients( $id ); - - do_action( 'woocommerce_api_delete_order', $id, $this ); - - return $this->delete( $id, 'order', ( 'true' === $force ) ); - } - - /** - * Helper method to get order post objects - * - * @since 2.1 - * @param array $args request arguments for filtering query - * @return WP_Query - */ - protected function query_orders( $args ) { - - // set base query arguments - $query_args = array( - 'fields' => 'ids', - 'post_type' => $this->post_type, - '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'] ); - } - - if ( ! empty( $args['customer_id'] ) ) { - $query_args['meta_query'] = array( - array( - 'key' => '_customer_user', - 'value' => absint( $args['customer_id'] ), - 'compare' => '=', - ), - ); - } - - $query_args = $this->merge_query_args( $query_args, $args ); - - return new WP_Query( $query_args ); - } - - /** - * Helper method to set/update the billing & shipping addresses for - * an order - * - * @since 2.1 - * @param \WC_Order $order - * @param array $data - */ - protected function set_order_addresses( $order, $data ) { - - $address_fields = array( - 'first_name', - 'last_name', - 'company', - 'email', - 'phone', - 'address_1', - 'address_2', - 'city', - 'state', - 'postcode', - 'country', - ); - - $billing_address = $shipping_address = array(); - - // billing address - if ( isset( $data['billing_address'] ) && is_array( $data['billing_address'] ) ) { - - foreach ( $address_fields as $field ) { - - if ( isset( $data['billing_address'][ $field ] ) ) { - $billing_address[ $field ] = wc_clean( $data['billing_address'][ $field ] ); - } - } - - unset( $address_fields['email'] ); - unset( $address_fields['phone'] ); - } - - // shipping address - if ( isset( $data['shipping_address'] ) && is_array( $data['shipping_address'] ) ) { - - foreach ( $address_fields as $field ) { - - if ( isset( $data['shipping_address'][ $field ] ) ) { - $shipping_address[ $field ] = wc_clean( $data['shipping_address'][ $field ] ); - } - } - } - - $this->update_address( $order, $billing_address, 'billing' ); - $this->update_address( $order, $shipping_address, 'shipping' ); - - // update user meta - if ( $order->get_user_id() ) { - foreach ( $billing_address as $key => $value ) { - update_user_meta( $order->get_user_id(), 'billing_' . $key, $value ); - } - foreach ( $shipping_address as $key => $value ) { - update_user_meta( $order->get_user_id(), 'shipping_' . $key, $value ); - } - } - } - - /** - * Update address. - * - * @param WC_Order $order - * @param array $posted - * @param string $type - */ - protected function update_address( $order, $posted, $type = 'billing' ) { - foreach ( $posted as $key => $value ) { - if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { - $order->{"set_{$type}_{$key}"}( $value ); - } - } - } - - /** - * Helper method to add/update order meta, with two restrictions: - * - * 1) Only non-protected meta (no leading underscore) can be set - * 2) Meta values must be scalar (int, string, bool) - * - * @since 2.2 - * @param int $order_id valid order ID - * @param array $order_meta order meta in array( 'meta_key' => 'meta_value' ) format - */ - protected function set_order_meta( $order_id, $order_meta ) { - - foreach ( $order_meta as $meta_key => $meta_value ) { - - if ( is_string( $meta_key ) && ! is_protected_meta( $meta_key ) && is_scalar( $meta_value ) ) { - update_post_meta( $order_id, $meta_key, $meta_value ); - } - } - } - - /** - * Helper method to check if the resource ID associated with the provided item is null - * - * Items can be deleted by setting the resource ID to null - * - * @since 2.2 - * @param array $item item provided in the request body - * @return bool true if the item resource ID is null, false otherwise - */ - protected function item_is_null( $item ) { - - $keys = array( 'product_id', 'method_id', 'title', 'code' ); - - foreach ( $keys as $key ) { - if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { - return true; - } - } - - return false; - } - - /** - * Wrapper method to create/update order items - * - * When updating, the item ID provided is checked to ensure it is associated - * with the order. - * - * @since 2.2 - * @param \WC_Order $order order - * @param string $item_type - * @param array $item item provided in the request body - * @param string $action either 'create' or 'update' - * @throws WC_API_Exception if item ID is not associated with order - */ - protected function set_item( $order, $item_type, $item, $action ) { - global $wpdb; - - $set_method = "set_{$item_type}"; - - // verify provided line item ID is associated with order - if ( 'update' === $action ) { - - $result = $wpdb->get_row( - $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", - absint( $item['id'] ), - absint( $order->get_id() ) - ) ); - - if ( is_null( $result ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); - } - } - - $this->$set_method( $order, $item, $action ); - } - - /** - * Create or update a line item - * - * @since 2.2 - * @param \WC_Order $order - * @param array $item line item data - * @param string $action 'create' to add line item or 'update' to update it - * @throws WC_API_Exception invalid data, server error - */ - protected function set_line_item( $order, $item, $action ) { - $creating = ( 'create' === $action ); - - // product is always required - if ( ! isset( $item['product_id'] ) && ! isset( $item['sku'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID or SKU is required', 'woocommerce' ), 400 ); - } - - // when updating, ensure product ID provided matches - if ( 'update' === $action ) { - - $item_product_id = wc_get_order_item_meta( $item['id'], '_product_id' ); - $item_variation_id = wc_get_order_item_meta( $item['id'], '_variation_id' ); - - if ( $item['product_id'] != $item_product_id && $item['product_id'] != $item_variation_id ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID provided does not match this line item', 'woocommerce' ), 400 ); - } - } - - if ( isset( $item['product_id'] ) ) { - $product_id = $item['product_id']; - } elseif ( isset( $item['sku'] ) ) { - $product_id = wc_get_product_id_by_sku( $item['sku'] ); - } - - // variations must each have a key & value - $variation_id = 0; - if ( isset( $item['variations'] ) && is_array( $item['variations'] ) ) { - foreach ( $item['variations'] as $key => $value ) { - if ( ! $key || ! $value ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_variation', __( 'The product variation is invalid', 'woocommerce' ), 400 ); - } - } - $variation_id = $this->get_variation_id( wc_get_product( $product_id ), $item['variations'] ); - } - - $product = wc_get_product( $variation_id ? $variation_id : $product_id ); - - // must be a valid WC_Product - if ( ! is_object( $product ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product', __( 'Product is invalid.', 'woocommerce' ), 400 ); - } - - // quantity must be positive float - if ( isset( $item['quantity'] ) && floatval( $item['quantity'] ) <= 0 ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity must be a positive float.', 'woocommerce' ), 400 ); - } - - // quantity is required when creating - if ( $creating && ! isset( $item['quantity'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity is required.', 'woocommerce' ), 400 ); - } - - // quantity - if ( $creating ) { - $line_item = new WC_Order_Item_Product(); - } else { - $line_item = new WC_Order_Item_Product( $item['id'] ); - } - - $line_item->set_product( $product ); - $line_item->set_order_id( $order->get_id() ); - - if ( isset( $item['quantity'] ) ) { - $line_item->set_quantity( $item['quantity'] ); - } - if ( isset( $item['total'] ) ) { - $line_item->set_total( floatval( $item['total'] ) ); - } elseif ( $creating ) { - $total = wc_get_price_excluding_tax( $product, array( 'qty' => $line_item->get_quantity() ) ); - $line_item->set_total( $total ); - $line_item->set_subtotal( $total ); - } - if ( isset( $item['total_tax'] ) ) { - $line_item->set_total_tax( floatval( $item['total_tax'] ) ); - } - if ( isset( $item['subtotal'] ) ) { - $line_item->set_subtotal( floatval( $item['subtotal'] ) ); - } - if ( isset( $item['subtotal_tax'] ) ) { - $line_item->set_subtotal_tax( floatval( $item['subtotal_tax'] ) ); - } - if ( $variation_id ) { - $line_item->set_variation_id( $variation_id ); - $line_item->set_variation( $item['variations'] ); - } - - // Save or add to order. - if ( $creating ) { - $order->add_item( $line_item ); - } else { - $item_id = $line_item->save(); - - if ( ! $item_id ) { - throw new WC_API_Exception( 'woocommerce_cannot_create_line_item', __( 'Cannot create line item, try again.', 'woocommerce' ), 500 ); - } - } - } - - /** - * Given a product ID & API provided variations, find the correct variation ID to use for calculation - * We can't just trust input from the API to pass a variation_id manually, otherwise you could pass - * the cheapest variation ID but provide other information so we have to look up the variation ID. - * - * @param WC_Product $product Product instance - * @param array $variations - * - * @return int Returns an ID if a valid variation was found for this product - */ - public function get_variation_id( $product, $variations = array() ) { - $variation_id = null; - $variations_normalized = array(); - - if ( $product->is_type( 'variable' ) && $product->has_child() ) { - if ( isset( $variations ) && is_array( $variations ) ) { - // start by normalizing the passed variations - foreach ( $variations as $key => $value ) { - $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); // from get_attributes in class-wc-api-products.php - $variations_normalized[ $key ] = strtolower( $value ); - } - // now search through each product child and see if our passed variations match anything - foreach ( $product->get_children() as $variation ) { - $meta = array(); - foreach ( get_post_meta( $variation ) as $key => $value ) { - $value = $value[0]; - $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); - $meta[ $key ] = strtolower( $value ); - } - // if the variation array is a part of the $meta array, we found our match - if ( $this->array_contains( $variations_normalized, $meta ) ) { - $variation_id = $variation; - break; - } - } - } - } - - return $variation_id; - } - - /** - * Utility function to see if the meta array contains data from variations - * - * @param array $needles - * @param array $haystack - * - * @return bool - */ - protected function array_contains( $needles, $haystack ) { - foreach ( $needles as $key => $value ) { - if ( $haystack[ $key ] !== $value ) { - return false; - } - } - return true; - } - - /** - * Create or update an order shipping method - * - * @since 2.2 - * @param \WC_Order $order - * @param array $shipping item data - * @param string $action 'create' to add shipping or 'update' to update it - * @throws WC_API_Exception invalid data, server error - */ - protected function set_shipping( $order, $shipping, $action ) { - - // total must be a positive float - if ( isset( $shipping['total'] ) && floatval( $shipping['total'] ) < 0 ) { - throw new WC_API_Exception( 'woocommerce_invalid_shipping_total', __( 'Shipping total must be a positive amount.', 'woocommerce' ), 400 ); - } - - if ( 'create' === $action ) { - - // method ID is required - if ( ! isset( $shipping['method_id'] ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); - } - - $rate = new WC_Shipping_Rate( $shipping['method_id'], isset( $shipping['method_title'] ) ? $shipping['method_title'] : '', isset( $shipping['total'] ) ? floatval( $shipping['total'] ) : 0, array(), $shipping['method_id'] ); - $item = new WC_Order_Item_Shipping(); - $item->set_order_id( $order->get_id() ); - $item->set_shipping_rate( $rate ); - $order->add_item( $item ); - } else { - - $item = new WC_Order_Item_Shipping( $shipping['id'] ); - - if ( isset( $shipping['method_id'] ) ) { - $item->set_method_id( $shipping['method_id'] ); - } - - if ( isset( $shipping['method_title'] ) ) { - $item->set_method_title( $shipping['method_title'] ); - } - - if ( isset( $shipping['total'] ) ) { - $item->set_total( floatval( $shipping['total'] ) ); - } - - $shipping_id = $item->save(); - - if ( ! $shipping_id ) { - throw new WC_API_Exception( 'woocommerce_cannot_update_shipping', __( 'Cannot update shipping method, try again.', 'woocommerce' ), 500 ); - } - } - } - - /** - * Create or update an order fee - * - * @since 2.2 - * @param \WC_Order $order - * @param array $fee item data - * @param string $action 'create' to add fee or 'update' to update it - * @throws WC_API_Exception invalid data, server error - */ - protected function set_fee( $order, $fee, $action ) { - - if ( 'create' === $action ) { - - // fee title is required - if ( ! isset( $fee['title'] ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee title is required', 'woocommerce' ), 400 ); - } - - $item = new WC_Order_Item_Fee(); - $item->set_order_id( $order->get_id() ); - $item->set_name( wc_clean( $fee['title'] ) ); - $item->set_total( isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0 ); - - // if taxable, tax class and total are required - if ( ! empty( $fee['taxable'] ) ) { - if ( ! isset( $fee['tax_class'] ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee tax class is required when fee is taxable.', 'woocommerce' ), 400 ); - } - - $item->set_tax_status( 'taxable' ); - $item->set_tax_class( $fee['tax_class'] ); - - if ( isset( $fee['total_tax'] ) ) { - $item->set_total_tax( isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0 ); - } - - if ( isset( $fee['tax_data'] ) ) { - $item->set_total_tax( wc_format_refund_total( array_sum( $fee['tax_data'] ) ) ); - $item->set_taxes( array_map( 'wc_format_refund_total', $fee['tax_data'] ) ); - } - } - - $order->add_item( $item ); - } else { - - $item = new WC_Order_Item_Fee( $fee['id'] ); - - if ( isset( $fee['title'] ) ) { - $item->set_name( wc_clean( $fee['title'] ) ); - } - - if ( isset( $fee['tax_class'] ) ) { - $item->set_tax_class( $fee['tax_class'] ); - } - - if ( isset( $fee['total'] ) ) { - $item->set_total( floatval( $fee['total'] ) ); - } - - if ( isset( $fee['total_tax'] ) ) { - $item->set_total_tax( floatval( $fee['total_tax'] ) ); - } - - $fee_id = $item->save(); - - if ( ! $fee_id ) { - throw new WC_API_Exception( 'woocommerce_cannot_update_fee', __( 'Cannot update fee, try again.', 'woocommerce' ), 500 ); - } - } - } - - /** - * Create or update an order coupon - * - * @since 2.2 - * @param \WC_Order $order - * @param array $coupon item data - * @param string $action 'create' to add coupon or 'update' to update it - * @throws WC_API_Exception invalid data, server error - */ - protected function set_coupon( $order, $coupon, $action ) { - - // coupon amount must be positive float - if ( isset( $coupon['amount'] ) && floatval( $coupon['amount'] ) < 0 ) { - throw new WC_API_Exception( 'woocommerce_invalid_coupon_total', __( 'Coupon discount total must be a positive amount.', 'woocommerce' ), 400 ); - } - - if ( 'create' === $action ) { - - // coupon code is required - if ( empty( $coupon['code'] ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); - } - - $item = new WC_Order_Item_Coupon(); - $item->set_props( array( - 'code' => $coupon['code'], - 'discount' => isset( $coupon['amount'] ) ? floatval( $coupon['amount'] ) : 0, - 'discount_tax' => 0, - 'order_id' => $order->get_id(), - ) ); - $order->add_item( $item ); - } else { - - $item = new WC_Order_Item_Coupon( $coupon['id'] ); - - if ( isset( $coupon['code'] ) ) { - $item->set_code( $coupon['code'] ); - } - - if ( isset( $coupon['amount'] ) ) { - $item->set_discount( floatval( $coupon['amount'] ) ); - } - - $coupon_id = $item->save(); - - if ( ! $coupon_id ) { - throw new WC_API_Exception( 'woocommerce_cannot_update_order_coupon', __( 'Cannot update coupon, try again.', 'woocommerce' ), 500 ); - } - } - } - - /** - * Get the admin order notes for an order - * - * @since 2.1 - * @param string $order_id order ID - * @param string|null $fields fields to include in response - * @return array|WP_Error - */ - public function get_order_notes( $order_id, $fields = null ) { - - // ensure ID is valid order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $args = array( - 'post_id' => $order_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[] = current( $this->get_order_note( $order_id, $note->comment_ID, $fields ) ); - } - - return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $order_id, $fields, $notes, $this->server ) ); - } - - /** - * Get an order note for the given order ID and ID - * - * @since 2.2 - * - * @param string $order_id order ID - * @param string $id order note ID - * @param string|null $fields fields to limit response to - * - * @return array|WP_Error - */ - public function get_order_note( $order_id, $id, $fields = null ) { - try { - // Validate order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); - } - - $note = get_comment( $id ); - - if ( is_null( $note ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $order_note = 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_note' => apply_filters( 'woocommerce_api_order_note_response', $order_note, $id, $fields, $note, $order_id, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Create a new order note for the given order - * - * @since 2.2 - * @param string $order_id order ID - * @param array $data raw request data - * @return WP_Error|array error or created note response data - */ - public function create_order_note( $order_id, $data ) { - try { - if ( ! isset( $data['order_note'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_note' ), 400 ); - } - - $data = $data['order_note']; - - // permission check - if ( ! current_user_can( 'publish_shop_orders' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_note', __( 'You do not have permission to create order notes', 'woocommerce' ), 401 ); - } - - $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $order = wc_get_order( $order_id ); - - $data = apply_filters( 'woocommerce_api_create_order_note_data', $data, $order_id, $this ); - - // note content is required - if ( ! isset( $data['note'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note', __( 'Order note is required', 'woocommerce' ), 400 ); - } - - $is_customer_note = ( isset( $data['customer_note'] ) && true === $data['customer_note'] ); - - // create the note - $note_id = $order->add_order_note( $data['note'], $is_customer_note ); - - if ( ! $note_id ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), 500 ); - } - - // HTTP 201 Created - $this->server->send_status( 201 ); - - do_action( 'woocommerce_api_create_order_note', $note_id, $order_id, $this ); - - return $this->get_order_note( $order->get_id(), $note_id ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Edit the order note - * - * @since 2.2 - * @param string $order_id order ID - * @param string $id note ID - * @param array $data parsed request data - * @return WP_Error|array error or edited note response data - */ - public function edit_order_note( $order_id, $id, $data ) { - try { - if ( ! isset( $data['order_note'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_note' ), 400 ); - } - - $data = $data['order_note']; - - // Validate order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $order = wc_get_order( $order_id ); - - // Validate note ID - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); - } - - // Ensure note ID is valid - $note = get_comment( $id ); - - if ( is_null( $note ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - // Ensure note ID is associated with given order - if ( $note->comment_post_ID != $order->get_id() ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); - } - - $data = apply_filters( 'woocommerce_api_edit_order_note_data', $data, $note->comment_ID, $order->get_id(), $this ); - - // Note content - if ( isset( $data['note'] ) ) { - - wp_update_comment( - array( - 'comment_ID' => $note->comment_ID, - 'comment_content' => $data['note'], - ) - ); - } - - // Customer note - if ( isset( $data['customer_note'] ) ) { - - update_comment_meta( $note->comment_ID, 'is_customer_note', true === $data['customer_note'] ? 1 : 0 ); - } - - do_action( 'woocommerce_api_edit_order_note', $note->comment_ID, $order->get_id(), $this ); - - return $this->get_order_note( $order->get_id(), $note->comment_ID ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete order note - * - * @since 2.2 - * @param string $order_id order ID - * @param string $id note ID - * @return WP_Error|array error or deleted message - */ - public function delete_order_note( $order_id, $id ) { - try { - $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - // Validate note ID - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); - } - - // Ensure note ID is valid - $note = get_comment( $id ); - - if ( is_null( $note ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - // Ensure note ID is associated with given order - if ( $note->comment_post_ID != $order_id ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); - } - - // Force delete since trashed order notes could not be managed through comments list table - $result = wc_delete_order_note( $note->comment_ID ); - - if ( ! $result ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_delete_order_note', __( 'This order note cannot be deleted', 'woocommerce' ), 500 ); - } - - do_action( 'woocommerce_api_delete_order_note', $note->comment_ID, $order_id, $this ); - - return array( 'message' => __( 'Permanently deleted order note', 'woocommerce' ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get the order refunds for an order - * - * @since 2.2 - * @param string $order_id order ID - * @param string|null $fields fields to include in response - * @return array|WP_Error - */ - public function get_order_refunds( $order_id, $fields = null ) { - - // Ensure ID is valid order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $refund_items = wc_get_orders( array( - 'type' => 'shop_order_refund', - 'parent' => $order_id, - 'limit' => -1, - 'return' => 'ids', - ) ); - $order_refunds = array(); - - foreach ( $refund_items as $refund_id ) { - $order_refunds[] = current( $this->get_order_refund( $order_id, $refund_id, $fields ) ); - } - - return array( 'order_refunds' => apply_filters( 'woocommerce_api_order_refunds_response', $order_refunds, $order_id, $fields, $refund_items, $this ) ); - } - - /** - * Get an order refund for the given order ID and ID - * - * @since 2.2 - * - * @param string $order_id order ID - * @param int $id - * @param string|null $fields fields to limit response to - * @param array $filter - * - * @return array|WP_Error - */ - public function get_order_refund( $order_id, $id, $fields = null, $filter = array() ) { - try { - // Validate order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); - } - - $order = wc_get_order( $order_id ); - $refund = wc_get_order( $id ); - - if ( ! $refund ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); - } - - $line_items = array(); - - // Add line items - foreach ( $refund->get_items( 'line_item' ) as $item_id => $item ) { - $product = $item->get_product(); - $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; - $item_meta = $item->get_formatted_meta_data( $hideprefix ); - - foreach ( $item_meta as $key => $values ) { - $item_meta[ $key ]->label = $values->display_key; - unset( $item_meta[ $key ]->display_key ); - unset( $item_meta[ $key ]->display_value ); - } - - $line_items[] = array( - 'id' => $item_id, - 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ), - 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), 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, - 'meta' => array_values( $item_meta ), - 'refunded_item_id' => (int) $item->get_meta( 'refunded_item_id' ), - ); - } - - $order_refund = array( - 'id' => $refund->get_id(), - 'created_at' => $this->server->format_datetime( $refund->get_date_created() ? $refund->get_date_created()->getTimestamp() : 0, false, false ), - 'amount' => wc_format_decimal( $refund->get_amount(), 2 ), - 'reason' => $refund->get_reason(), - 'line_items' => $line_items, - ); - - return array( 'order_refund' => apply_filters( 'woocommerce_api_order_refund_response', $order_refund, $id, $fields, $refund, $order_id, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Create a new order refund for the given order - * - * @since 2.2 - * @param string $order_id order ID - * @param array $data raw request data - * @param bool $api_refund do refund using a payment gateway API - * @return WP_Error|array error or created refund response data - */ - public function create_order_refund( $order_id, $data, $api_refund = true ) { - try { - if ( ! isset( $data['order_refund'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_refund' ), 400 ); - } - - $data = $data['order_refund']; - - // Permission check - if ( ! current_user_can( 'publish_shop_orders' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_refund', __( 'You do not have permission to create order refunds', 'woocommerce' ), 401 ); - } - - $order_id = absint( $order_id ); - - if ( empty( $order_id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); - } - - $data = apply_filters( 'woocommerce_api_create_order_refund_data', $data, $order_id, $this ); - - // Refund amount is required - if ( ! isset( $data['amount'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount is required.', 'woocommerce' ), 400 ); - } elseif ( 0 > $data['amount'] ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount must be positive.', 'woocommerce' ), 400 ); - } - - $data['order_id'] = $order_id; - $data['refund_id'] = 0; - - // Create the refund - $refund = wc_create_refund( $data ); - - if ( ! $refund ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); - } - - // Refund via API - if ( $api_refund ) { - if ( WC()->payment_gateways() ) { - $payment_gateways = WC()->payment_gateways->payment_gateways(); - } - - $order = wc_get_order( $order_id ); - - if ( isset( $payment_gateways[ $order->get_payment_method() ] ) && $payment_gateways[ $order->get_payment_method() ]->supports( 'refunds' ) ) { - $result = $payment_gateways[ $order->get_payment_method() ]->process_refund( $order_id, $refund->get_amount(), $refund->get_reason() ); - - if ( is_wp_error( $result ) ) { - return $result; - } elseif ( ! $result ) { - throw new WC_API_Exception( 'woocommerce_api_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ), 500 ); - } - } - } - - // HTTP 201 Created - $this->server->send_status( 201 ); - - do_action( 'woocommerce_api_create_order_refund', $refund->get_id(), $order_id, $this ); - - return $this->get_order_refund( $order_id, $refund->get_id() ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Edit an order refund - * - * @since 2.2 - * @param string $order_id order ID - * @param string $id refund ID - * @param array $data parsed request data - * @return WP_Error|array error or edited refund response data - */ - public function edit_order_refund( $order_id, $id, $data ) { - try { - if ( ! isset( $data['order_refund'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_refund' ), 400 ); - } - - $data = $data['order_refund']; - - // Validate order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - // Validate refund ID - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); - } - - // Ensure order ID is valid - $refund = get_post( $id ); - - if ( ! $refund ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); - } - - // Ensure refund ID is associated with given order - if ( $refund->post_parent != $order_id ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); - } - - $data = apply_filters( 'woocommerce_api_edit_order_refund_data', $data, $refund->ID, $order_id, $this ); - - // Update reason - if ( isset( $data['reason'] ) ) { - $updated_refund = wp_update_post( array( 'ID' => $refund->ID, 'post_excerpt' => $data['reason'] ) ); - - if ( is_wp_error( $updated_refund ) ) { - return $updated_refund; - } - } - - // Update refund amount - if ( isset( $data['amount'] ) && 0 < $data['amount'] ) { - update_post_meta( $refund->ID, '_refund_amount', wc_format_decimal( $data['amount'] ) ); - } - - do_action( 'woocommerce_api_edit_order_refund', $refund->ID, $order_id, $this ); - - return $this->get_order_refund( $order_id, $refund->ID ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete order refund - * - * @since 2.2 - * @param string $order_id order ID - * @param string $id refund ID - * @return WP_Error|array error or deleted message - */ - public function delete_order_refund( $order_id, $id ) { - try { - $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - // Validate refund ID - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); - } - - // Ensure refund ID is valid - $refund = get_post( $id ); - - if ( ! $refund ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); - } - - // Ensure refund ID is associated with given order - if ( $refund->post_parent != $order_id ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); - } - - wc_delete_shop_order_transients( $order_id ); - - do_action( 'woocommerce_api_delete_order_refund', $refund->ID, $order_id, $this ); - - return $this->delete( $refund->ID, 'refund', true ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Bulk update or insert orders - * Accepts an array with orders in the formats supported by - * WC_API_Orders->create_order() and WC_API_Orders->edit_order() - * - * @since 2.4.0 - * - * @param array $data - * - * @return array|WP_Error - */ - public function bulk( $data ) { - - try { - if ( ! isset( $data['orders'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_orders_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'orders' ), 400 ); - } - - $data = $data['orders']; - $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'orders' ); - - // Limit bulk operation - if ( count( $data ) > $limit ) { - throw new WC_API_Exception( 'woocommerce_api_orders_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); - } - - $orders = array(); - - foreach ( $data as $_order ) { - $order_id = 0; - - // Try to get the order ID - if ( isset( $_order['id'] ) ) { - $order_id = intval( $_order['id'] ); - } - - if ( $order_id ) { - - // Order exists / edit order - $edit = $this->edit_order( $order_id, array( 'order' => $_order ) ); - - if ( is_wp_error( $edit ) ) { - $orders[] = array( - 'id' => $order_id, - 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), - ); - } else { - $orders[] = $edit['order']; - } - } else { - - // Order don't exists / create order - $new = $this->create_order( array( 'order' => $_order ) ); - - if ( is_wp_error( $new ) ) { - $orders[] = array( - 'id' => $order_id, - 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), - ); - } else { - $orders[] = $new['order']; - } - } - } - - return array( 'orders' => apply_filters( 'woocommerce_api_orders_bulk_response', $orders, $this ) ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } -} diff --git a/includes/legacy/api/v3/class-wc-api-products.php b/includes/legacy/api/v3/class-wc-api-products.php deleted file mode 100644 index 8f084074078..00000000000 --- a/includes/legacy/api/v3/class-wc-api-products.php +++ /dev/null @@ -1,3308 +0,0 @@ - - * GET /products//reviews - * - * @since 2.1 - * @param array $routes - * @return array - */ - public function register_routes( $routes ) { - - # GET/POST /products - $routes[ $this->base ] = array( - array( array( $this, 'get_products' ), WC_API_Server::READABLE ), - array( array( $this, 'create_product' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET /products/count - $routes[ $this->base . '/count' ] = array( - array( array( $this, 'get_products_count' ), WC_API_Server::READABLE ), - ); - - # GET/PUT/DELETE /products/ - $routes[ $this->base . '/(?P\d+)' ] = array( - array( array( $this, 'get_product' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_product' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_product' ), WC_API_Server::DELETABLE ), - ); - - # GET /products//reviews - $routes[ $this->base . '/(?P\d+)/reviews' ] = array( - array( array( $this, 'get_product_reviews' ), WC_API_Server::READABLE ), - ); - - # GET /products//orders - $routes[ $this->base . '/(?P\d+)/orders' ] = array( - array( array( $this, 'get_product_orders' ), WC_API_Server::READABLE ), - ); - - # GET/POST /products/categories - $routes[ $this->base . '/categories' ] = array( - array( array( $this, 'get_product_categories' ), WC_API_Server::READABLE ), - array( array( $this, 'create_product_category' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET/PUT/DELETE /products/categories/ - $routes[ $this->base . '/categories/(?P\d+)' ] = array( - array( array( $this, 'get_product_category' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_product_category' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_product_category' ), WC_API_Server::DELETABLE ), - ); - - # GET/POST /products/tags - $routes[ $this->base . '/tags' ] = array( - array( array( $this, 'get_product_tags' ), WC_API_Server::READABLE ), - array( array( $this, 'create_product_tag' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET/PUT/DELETE /products/tags/ - $routes[ $this->base . '/tags/(?P\d+)' ] = array( - array( array( $this, 'get_product_tag' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_product_tag' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_product_tag' ), WC_API_Server::DELETABLE ), - ); - - # GET/POST /products/shipping_classes - $routes[ $this->base . '/shipping_classes' ] = array( - array( array( $this, 'get_product_shipping_classes' ), WC_API_Server::READABLE ), - array( array( $this, 'create_product_shipping_class' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET/PUT/DELETE /products/shipping_classes/ - $routes[ $this->base . '/shipping_classes/(?P\d+)' ] = array( - array( array( $this, 'get_product_shipping_class' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_product_shipping_class' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_product_shipping_class' ), WC_API_Server::DELETABLE ), - ); - - # GET/POST /products/attributes - $routes[ $this->base . '/attributes' ] = array( - array( array( $this, 'get_product_attributes' ), WC_API_Server::READABLE ), - array( array( $this, 'create_product_attribute' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET/PUT/DELETE /products/attributes/ - $routes[ $this->base . '/attributes/(?P\d+)' ] = array( - array( array( $this, 'get_product_attribute' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_product_attribute' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_product_attribute' ), WC_API_Server::DELETABLE ), - ); - - # GET/POST /products/attributes//terms - $routes[ $this->base . '/attributes/(?P\d+)/terms' ] = array( - array( array( $this, 'get_product_attribute_terms' ), WC_API_Server::READABLE ), - array( array( $this, 'create_product_attribute_term' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET/PUT/DELETE /products/attributes//terms/ - $routes[ $this->base . '/attributes/(?P\d+)/terms/(?P\d+)' ] = array( - array( array( $this, 'get_product_attribute_term' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_product_attribute_term' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_product_attribute_term' ), WC_API_Server::DELETABLE ), - ); - - # POST|PUT /products/bulk - $routes[ $this->base . '/bulk' ] = array( - array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - ); - - 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->get_parent_id() ) { - $product_data['parent'] = $this->get_product_data( $product->get_parent_id() ); - } - - // Add grouped products data - if ( $product->is_type( 'grouped' ) && $product->has_child() ) { - $product_data['grouped_products'] = $this->get_grouped_products_data( $product ); - } - - if ( $product->is_type( 'simple' ) ) { - $parent_id = $product->get_parent_id(); - if ( ! empty( $parent_id ) ) { - $_product = wc_get_product( $parent_id ); - $product_data['parent'] = $this->get_product_data( $_product ); - } - } - - return array( 'product' => apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields, $this->server ) ); - } - - /** - * Get the total number of products - * - * @since 2.1 - * - * @param string $type - * @param array $filter - * - * @return array|WP_Error - */ - public function get_products_count( $type = null, $filter = array() ) { - try { - if ( ! current_user_can( 'read_private_products' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), 401 ); - } - - if ( ! empty( $type ) ) { - $filter['type'] = $type; - } - - $query = $this->query_products( $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() ) ); - } - } - - /** - * Create a new product. - * - * @since 2.2 - * - * @param array $data posted data - * - * @return array|WP_Error - */ - public function create_product( $data ) { - $id = 0; - - try { - if ( ! isset( $data['product'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product' ), 400 ); - } - - $data = $data['product']; - - // Check permissions. - if ( ! current_user_can( 'publish_products' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product', __( 'You do not have permission to create products', 'woocommerce' ), 401 ); - } - - $data = apply_filters( 'woocommerce_api_create_product_data', $data, $this ); - - // Check if product title is specified. - if ( ! isset( $data['title'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_title', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'title' ), 400 ); - } - - // Check product type. - if ( ! isset( $data['type'] ) ) { - $data['type'] = 'simple'; - } - - // Set visible visibility when not sent. - if ( ! isset( $data['catalog_visibility'] ) ) { - $data['catalog_visibility'] = 'visible'; - } - - // Validate the product type. - if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 ); - } - - // Enable description html tags. - $post_content = isset( $data['description'] ) ? wc_clean( $data['description'] ) : ''; - if ( $post_content && isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) { - - $post_content = wp_filter_post_kses( $data['description'] ); - } - - // Enable short description html tags. - $post_excerpt = isset( $data['short_description'] ) ? wc_clean( $data['short_description'] ) : ''; - if ( $post_excerpt && isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) { - $post_excerpt = wp_filter_post_kses( $data['short_description'] ); - } - - $classname = WC_Product_Factory::get_classname_from_product_type( $data['type'] ); - if ( ! class_exists( $classname ) ) { - $classname = 'WC_Product_Simple'; - } - $product = new $classname(); - - $product->set_name( wc_clean( $data['title'] ) ); - $product->set_status( isset( $data['status'] ) ? wc_clean( $data['status'] ) : 'publish' ); - $product->set_short_description( isset( $data['short_description'] ) ? $post_excerpt : '' ); - $product->set_description( isset( $data['description'] ) ? $post_content : '' ); - $product->set_menu_order( isset( $data['menu_order'] ) ? intval( $data['menu_order'] ) : 0 ); - - if ( ! empty( $data['name'] ) ) { - $product->set_slug( sanitize_title( $data['name'] ) ); - } - - // Attempts to create the new product. - $product->save(); - $id = $product->get_id(); - - // Checks for an error in the product creation. - if ( 0 >= $id ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_product', $id->get_error_message(), 400 ); - } - - // Check for featured/gallery images, upload it and set it. - if ( isset( $data['images'] ) ) { - $product = $this->save_product_images( $product, $data['images'] ); - } - - // Save product meta fields. - $product = $this->save_product_meta( $product, $data ); - $product->save(); - - // Save variations. - if ( isset( $data['type'] ) && 'variable' == $data['type'] && isset( $data['variations'] ) && is_array( $data['variations'] ) ) { - $this->save_variations( $product, $data ); - } - - do_action( 'woocommerce_api_create_product', $id, $data ); - - // Clear cache/transients. - wc_delete_product_transients( $id ); - - $this->server->send_status( 201 ); - - return $this->get_product( $id ); - } catch ( WC_Data_Exception $e ) { - $this->clear_product( $id ); - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } catch ( WC_API_Exception $e ) { - $this->clear_product( $id ); - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Edit a product - * - * @since 2.2 - * - * @param int $id the product ID - * @param array $data - * - * @return array|WP_Error - */ - public function edit_product( $id, $data ) { - try { - if ( ! isset( $data['product'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product' ), 400 ); - } - - $data = $data['product']; - - $id = $this->validate_request( $id, 'product', 'edit' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - $product = wc_get_product( $id ); - - $data = apply_filters( 'woocommerce_api_edit_product_data', $data, $this ); - - // Product title. - if ( isset( $data['title'] ) ) { - $product->set_name( wc_clean( $data['title'] ) ); - } - - // Product name (slug). - if ( isset( $data['name'] ) ) { - $product->set_slug( wc_clean( $data['name'] ) ); - } - - // Product status. - if ( isset( $data['status'] ) ) { - $product->set_status( wc_clean( $data['status'] ) ); - } - - // Product short description. - if ( isset( $data['short_description'] ) ) { - // Enable short description html tags. - $post_excerpt = ( isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) ? wp_filter_post_kses( $data['short_description'] ) : wc_clean( $data['short_description'] ); - $product->set_short_description( $post_excerpt ); - } - - // Product description. - if ( isset( $data['description'] ) ) { - // Enable description html tags. - $post_content = ( isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) ? wp_filter_post_kses( $data['description'] ) : wc_clean( $data['description'] ); - $product->set_description( $post_content ); - } - - // Validate the product type. - if ( isset( $data['type'] ) && ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 ); - } - - // Menu order. - if ( isset( $data['menu_order'] ) ) { - $product->set_menu_order( intval( $data['menu_order'] ) ); - } - - // Check for featured/gallery images, upload it and set it. - if ( isset( $data['images'] ) ) { - $product = $this->save_product_images( $product, $data['images'] ); - } - - // Save product meta fields. - $product = $this->save_product_meta( $product, $data ); - - // Save variations. - if ( $product->is_type( 'variable' ) ) { - if ( isset( $data['variations'] ) && is_array( $data['variations'] ) ) { - $this->save_variations( $product, $data ); - } else { - // Just sync variations. - $product = WC_Product_Variable::sync( $product, false ); - } - } - - $product->save(); - - do_action( 'woocommerce_api_edit_product', $id, $data ); - - // Clear cache/transients. - wc_delete_product_transients( $id ); - - return $this->get_product( $id ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete a product. - * - * @since 2.2 - * - * @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; - } - - $product = wc_get_product( $id ); - - do_action( 'woocommerce_api_delete_product', $id, $this ); - - // If we're forcing, then delete permanently. - if ( $force ) { - if ( $product->is_type( 'variable' ) ) { - foreach ( $product->get_children() as $child_id ) { - $child = wc_get_product( $child_id ); - if ( ! empty( $child ) ) { - $child->delete( true ); - } - } - } else { - // For other product types, if the product has children, remove the relationship. - foreach ( $product->get_children() as $child_id ) { - $child = wc_get_product( $child_id ); - if ( ! empty( $child ) ) { - $child->set_parent_id( 0 ); - $child->save(); - } - } - } - - $product->delete( true ); - $result = ! ( $product->get_id() > 0 ); - } else { - $product->delete(); - $result = 'trash' === $product->get_status(); - } - - if ( ! $result ) { - return new WP_Error( 'woocommerce_api_cannot_delete_product', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product' ), array( 'status' => 500 ) ); - } - - // Delete parent product transients. - if ( $parent_id = wp_get_post_parent_id( $id ) ) { - wc_delete_product_transients( $parent_id ); - } - - if ( $force ) { - return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), 'product' ) ); - } else { - $this->server->send_status( '202' ); - - return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product' ) ); - } - } - - /** - * 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; - } - - $comments = get_approved_comments( $id ); - $reviews = array(); - - foreach ( $comments as $comment ) { - - $reviews[] = array( - 'id' => intval( $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 ) ); - } - - /** - * Get the orders for a product - * - * @since 2.4.0 - * @param int $id the product ID to get orders for - * @param string fields fields to retrieve - * @param array $filter filters to include in response - * @param string $status the order status to retrieve - * @param $page $page page to retrieve - * @return array|WP_Error - */ - public function get_product_orders( $id, $fields = null, $filter = array(), $status = null, $page = 1 ) { - global $wpdb; - - $id = $this->validate_request( $id, 'product', 'read' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - $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' - ", $id ) ); - - if ( empty( $order_ids ) ) { - return array( 'orders' => array() ); - } - - $filter = array_merge( $filter, array( - 'in' => implode( ',', $order_ids ), - ) ); - - $orders = WC()->api->WC_API_Orders->get_orders( $fields, $filter, $status, $page ); - - return array( 'orders' => apply_filters( 'woocommerce_api_product_orders_response', $orders['orders'], $id, $filter, $fields, $this->server ) ); - } - - /** - * Get a listing of product categories - * - * @since 2.2 - * - * @param string|null $fields fields to limit response to - * - * @return array|WP_Error - */ - public function get_product_categories( $fields = null ) { - try { - // Permissions check - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); - } - - $product_categories = array(); - - $terms = get_terms( 'product_cat', array( 'hide_empty' => false, 'fields' => 'ids' ) ); - - foreach ( $terms as $term_id ) { - $product_categories[] = current( $this->get_product_category( $term_id, $fields ) ); - } - - return array( 'product_categories' => apply_filters( 'woocommerce_api_product_categories_response', $product_categories, $terms, $fields, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get the product category for the given ID - * - * @since 2.2 - * - * @param string $id product category term ID - * @param string|null $fields fields to limit response to - * - * @return array|WP_Error - */ - public function get_product_category( $id, $fields = null ) { - try { - $id = absint( $id ); - - // Validate ID - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'Invalid product category ID', 'woocommerce' ), 400 ); - } - - // Permissions check - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); - } - - $term = get_term( $id, 'product_cat' ); - - if ( is_wp_error( $term ) || is_null( $term ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'A product category with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $term_id = intval( $term->term_id ); - - // Get category display type - $display_type = get_term_meta( $term_id, 'display_type', true ); - - // Get category image - $image = ''; - if ( $image_id = get_term_meta( $term_id, 'thumbnail_id', true ) ) { - $image = wp_get_attachment_url( $image_id ); - } - - $product_category = array( - 'id' => $term_id, - 'name' => $term->name, - 'slug' => $term->slug, - 'parent' => $term->parent, - 'description' => $term->description, - 'display' => $display_type ? $display_type : 'default', - 'image' => $image ? esc_url( $image ) : '', - 'count' => intval( $term->count ), - ); - - return array( 'product_category' => apply_filters( 'woocommerce_api_product_category_response', $product_category, $id, $fields, $term, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Create a new product category. - * - * @since 2.5.0 - * @param array $data Posted data - * @return array|WP_Error Product category if succeed, otherwise WP_Error - * will be returned - */ - public function create_product_category( $data ) { - global $wpdb; - - try { - if ( ! isset( $data['product_category'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_category_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_category' ), 400 ); - } - - // Check permissions - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_category', __( 'You do not have permission to create product categories', 'woocommerce' ), 401 ); - } - - $defaults = array( - 'name' => '', - 'slug' => '', - 'description' => '', - 'parent' => 0, - 'display' => 'default', - 'image' => '', - ); - - $data = wp_parse_args( $data['product_category'], $defaults ); - $data = apply_filters( 'woocommerce_api_create_product_category_data', $data, $this ); - - // Check parent. - $data['parent'] = absint( $data['parent'] ); - if ( $data['parent'] ) { - $parent = get_term_by( 'id', $data['parent'], 'product_cat' ); - if ( ! $parent ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_parent', __( 'Product category parent is invalid', 'woocommerce' ), 400 ); - } - } - - // If value of image is numeric, assume value as image_id. - $image = $data['image']; - $image_id = 0; - if ( is_numeric( $image ) ) { - $image_id = absint( $image ); - } elseif ( ! empty( $image ) ) { - $upload = $this->upload_product_category_image( esc_url_raw( $image ) ); - $image_id = $this->set_product_category_image_as_attachment( $upload ); - } - - $insert = wp_insert_term( $data['name'], 'product_cat', $data ); - if ( is_wp_error( $insert ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_category', $insert->get_error_message(), 400 ); - } - - $id = $insert['term_id']; - - update_term_meta( $id, 'display_type', 'default' === $data['display'] ? '' : sanitize_text_field( $data['display'] ) ); - - // Check if image_id is a valid image attachment before updating the term meta. - if ( $image_id && wp_attachment_is_image( $image_id ) ) { - update_term_meta( $id, 'thumbnail_id', $image_id ); - } - - do_action( 'woocommerce_api_create_product_category', $id, $data ); - - $this->server->send_status( 201 ); - - return $this->get_product_category( $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Edit a product category. - * - * @since 2.5.0 - * @param int $id Product category term ID - * @param array $data Posted data - * @return array|WP_Error Product category if succeed, otherwise WP_Error - * will be returned - */ - public function edit_product_category( $id, $data ) { - global $wpdb; - - try { - if ( ! isset( $data['product_category'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_category', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_category' ), 400 ); - } - - $id = absint( $id ); - $data = $data['product_category']; - - // Check permissions. - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_category', __( 'You do not have permission to edit product categories', 'woocommerce' ), 401 ); - } - - $data = apply_filters( 'woocommerce_api_edit_product_category_data', $data, $this ); - $category = $this->get_product_category( $id ); - - if ( is_wp_error( $category ) ) { - return $category; - } - - if ( isset( $data['image'] ) ) { - $image_id = 0; - - // If value of image is numeric, assume value as image_id. - $image = $data['image']; - if ( is_numeric( $image ) ) { - $image_id = absint( $image ); - } elseif ( ! empty( $image ) ) { - $upload = $this->upload_product_category_image( esc_url_raw( $image ) ); - $image_id = $this->set_product_category_image_as_attachment( $upload ); - } - - // In case client supplies invalid image or wants to unset category image. - if ( ! wp_attachment_is_image( $image_id ) ) { - $image_id = ''; - } - } - - $update = wp_update_term( $id, 'product_cat', $data ); - if ( is_wp_error( $update ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_catgory', __( 'Could not edit the category', 'woocommerce' ), 400 ); - } - - if ( ! empty( $data['display'] ) ) { - update_term_meta( $id, 'display_type', 'default' === $data['display'] ? '' : sanitize_text_field( $data['display'] ) ); - } - - if ( isset( $image_id ) ) { - update_term_meta( $id, 'thumbnail_id', $image_id ); - } - - do_action( 'woocommerce_api_edit_product_category', $id, $data ); - - return $this->get_product_category( $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete a product category. - * - * @since 2.5.0 - * @param int $id Product category term ID - * @return array|WP_Error Success message if succeed, otherwise WP_Error - * will be returned - */ - public function delete_product_category( $id ) { - global $wpdb; - - try { - // Check permissions - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_category', __( 'You do not have permission to delete product category', 'woocommerce' ), 401 ); - } - - $id = absint( $id ); - $deleted = wp_delete_term( $id, 'product_cat' ); - if ( ! $deleted || is_wp_error( $deleted ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_category', __( 'Could not delete the category', 'woocommerce' ), 401 ); - } - - do_action( 'woocommerce_api_delete_product_category', $id, $this ); - - return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_category' ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get a listing of product tags. - * - * @since 2.5.0 - * - * @param string|null $fields Fields to limit response to - * - * @return array|WP_Error - */ - public function get_product_tags( $fields = null ) { - try { - // Permissions check - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_tags', __( 'You do not have permission to read product tags', 'woocommerce' ), 401 ); - } - - $product_tags = array(); - - $terms = get_terms( 'product_tag', array( 'hide_empty' => false, 'fields' => 'ids' ) ); - - foreach ( $terms as $term_id ) { - $product_tags[] = current( $this->get_product_tag( $term_id, $fields ) ); - } - - return array( 'product_tags' => apply_filters( 'woocommerce_api_product_tags_response', $product_tags, $terms, $fields, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get the product tag for the given ID. - * - * @since 2.5.0 - * - * @param string $id Product tag term ID - * @param string|null $fields Fields to limit response to - * - * @return array|WP_Error - */ - public function get_product_tag( $id, $fields = null ) { - try { - $id = absint( $id ); - - // Validate ID - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_tag_id', __( 'Invalid product tag ID', 'woocommerce' ), 400 ); - } - - // Permissions check - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_tags', __( 'You do not have permission to read product tags', 'woocommerce' ), 401 ); - } - - $term = get_term( $id, 'product_tag' ); - - if ( is_wp_error( $term ) || is_null( $term ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_tag_id', __( 'A product tag with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $term_id = intval( $term->term_id ); - - $tag = array( - 'id' => $term_id, - 'name' => $term->name, - 'slug' => $term->slug, - 'description' => $term->description, - 'count' => intval( $term->count ), - ); - - return array( 'product_tag' => apply_filters( 'woocommerce_api_product_tag_response', $tag, $id, $fields, $term, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Create a new product tag. - * - * @since 2.5.0 - * @param array $data Posted data - * @return array|WP_Error Product tag if succeed, otherwise WP_Error - * will be returned - */ - public function create_product_tag( $data ) { - try { - if ( ! isset( $data['product_tag'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_tag_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_tag' ), 400 ); - } - - // Check permissions - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_tag', __( 'You do not have permission to create product tags', 'woocommerce' ), 401 ); - } - - $defaults = array( - 'name' => '', - 'slug' => '', - 'description' => '', - ); - - $data = wp_parse_args( $data['product_tag'], $defaults ); - $data = apply_filters( 'woocommerce_api_create_product_tag_data', $data, $this ); - - $insert = wp_insert_term( $data['name'], 'product_tag', $data ); - if ( is_wp_error( $insert ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_tag', $insert->get_error_message(), 400 ); - } - $id = $insert['term_id']; - - do_action( 'woocommerce_api_create_product_tag', $id, $data ); - - $this->server->send_status( 201 ); - - return $this->get_product_tag( $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Edit a product tag. - * - * @since 2.5.0 - * @param int $id Product tag term ID - * @param array $data Posted data - * @return array|WP_Error Product tag if succeed, otherwise WP_Error - * will be returned - */ - public function edit_product_tag( $id, $data ) { - try { - if ( ! isset( $data['product_tag'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_tag', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_tag' ), 400 ); - } - - $id = absint( $id ); - $data = $data['product_tag']; - - // Check permissions. - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_tag', __( 'You do not have permission to edit product tags', 'woocommerce' ), 401 ); - } - - $data = apply_filters( 'woocommerce_api_edit_product_tag_data', $data, $this ); - $tag = $this->get_product_tag( $id ); - - if ( is_wp_error( $tag ) ) { - return $tag; - } - - $update = wp_update_term( $id, 'product_tag', $data ); - if ( is_wp_error( $update ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_tag', __( 'Could not edit the tag', 'woocommerce' ), 400 ); - } - - do_action( 'woocommerce_api_edit_product_tag', $id, $data ); - - return $this->get_product_tag( $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete a product tag. - * - * @since 2.5.0 - * @param int $id Product tag term ID - * @return array|WP_Error Success message if succeed, otherwise WP_Error - * will be returned - */ - public function delete_product_tag( $id ) { - try { - // Check permissions - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_tag', __( 'You do not have permission to delete product tag', 'woocommerce' ), 401 ); - } - - $id = absint( $id ); - $deleted = wp_delete_term( $id, 'product_tag' ); - if ( ! $deleted || is_wp_error( $deleted ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_tag', __( 'Could not delete the tag', 'woocommerce' ), 401 ); - } - - do_action( 'woocommerce_api_delete_product_tag', $id, $this ); - - return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_tag' ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * 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(), - ); - - // 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_arg_map = array( - 'product_type' => 'type', - 'product_cat' => 'category', - 'product_tag' => 'tag', - 'product_shipping_class' => 'shipping_class', - ); - - // Add attribute taxonomy names into the map. - foreach ( wc_get_attribute_taxonomy_names() as $attribute_name ) { - $taxonomies_arg_map[ $attribute_name ] = $attribute_name; - } - - // Set tax_query for each passed arg. - foreach ( $taxonomies_arg_map as $tax_name => $arg ) { - if ( ! empty( $args[ $arg ] ) ) { - $terms = explode( ',', $args[ $arg ] ); - - $tax_query[] = array( - 'taxonomy' => $tax_name, - 'field' => 'slug', - 'terms' => $terms, - ); - - unset( $args[ $arg ] ); - } - } - - if ( ! empty( $tax_query ) ) { - $query_args['tax_query'] = $tax_query; - } - - // Filter by specific sku - if ( ! empty( $args['sku'] ) ) { - if ( ! is_array( $query_args['meta_query'] ) ) { - $query_args['meta_query'] = array(); - } - - $query_args['meta_query'][] = array( - 'key' => '_sku', - 'value' => $args['sku'], - 'compare' => '=', - ); - - $query_args['post_type'] = array( 'product', 'product_variation' ); - } - - $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' => $product->get_price(), - 'regular_price' => $product->get_regular_price(), - 'sale_price' => $product->get_sale_price() ? $product->get_sale_price() : 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(), - 'product_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '', - 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '', - 'weight' => $product->get_weight() ? $product->get_weight() : 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' => wpautop( do_shortcode( $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() ), - 'parent_id' => $product->get_parent_id(), - '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' => wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ), - 'total_sales' => $product->get_total_sales(), - 'variations' => array(), - 'parent' => array(), - 'grouped_products' => array(), - 'menu_order' => $this->get_product_menu_order( $product ), - ); - } - - /** - * Get product menu order. - * - * @since 2.5.3 - * @param WC_Product $product - * @return int - */ - private function get_product_menu_order( $product ) { - $menu_order = $product->get_menu_order(); - - return apply_filters( 'woocommerce_api_product_menu_order', $menu_order, $product ); - } - - /** - * 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' => $variation->get_price(), - 'regular_price' => $variation->get_regular_price(), - 'sale_price' => $variation->get_sale_price() ? $variation->get_sale_price() : null, - 'taxable' => $variation->is_taxable(), - 'tax_status' => $variation->get_tax_status(), - 'tax_class' => $variation->get_tax_class(), - 'managing_stock' => $variation->managing_stock(), - 'stock_quantity' => $variation->get_stock_quantity(), - 'in_stock' => $variation->is_in_stock(), - 'backorders_allowed' => $variation->backorders_allowed(), - 'backordered' => $variation->is_on_backorder(), - 'purchaseable' => $variation->is_purchasable(), - 'visible' => $variation->variation_is_visible(), - 'on_sale' => $variation->is_on_sale(), - 'weight' => $variation->get_weight() ? $variation->get_weight() : 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 grouped products data - * - * @since 2.5.0 - * @param WC_Product $product - * - * @return array - */ - private function get_grouped_products_data( $product ) { - $products = array(); - - foreach ( $product->get_children() as $child_id ) { - $_product = wc_get_product( $child_id ); - - if ( ! $_product || ! $_product->exists() ) { - continue; - } - - $products[] = $this->get_product_data( $_product ); - - } - - return $products; - } - - /** - * Save default attributes. - * - * @since 3.0.0 - * - * @param WC_Product $product - * @param WP_REST_Request $request - * @return WC_Product - */ - protected function save_default_attributes( $product, $request ) { - // Update default attributes options setting. - if ( isset( $request['default_attribute'] ) ) { - $request['default_attributes'] = $request['default_attribute']; - } - - if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { - $attributes = $product->get_attributes(); - $default_attributes = array(); - - foreach ( $request['default_attributes'] as $default_attr_key => $default_attr ) { - if ( ! isset( $default_attr['name'] ) ) { - continue; - } - - $taxonomy = sanitize_title( $default_attr['name'] ); - - if ( isset( $default_attr['slug'] ) ) { - $taxonomy = $this->get_attribute_taxonomy_by_slug( $default_attr['slug'] ); - } - - if ( isset( $attributes[ $taxonomy ] ) ) { - $_attribute = $attributes[ $taxonomy ]; - - if ( $_attribute['is_variation'] ) { - $value = ''; - - if ( isset( $default_attr['option'] ) ) { - if ( $_attribute['is_taxonomy'] ) { - // Don't use wc_clean as it destroys sanitized characters - $value = sanitize_title( trim( stripslashes( $default_attr['option'] ) ) ); - } else { - $value = wc_clean( trim( stripslashes( $default_attr['option'] ) ) ); - } - } - - if ( $value ) { - $default_attributes[ $taxonomy ] = $value; - } - } - } - } - - $product->set_default_attributes( $default_attributes ); - } - - return $product; - } - - /** - * Save product meta. - * - * @since 2.2 - * @param WC_Product $product - * @param array $data - * @return WC_Product - * @throws WC_API_Exception - */ - protected function save_product_meta( $product, $data ) { - global $wpdb; - - // Virtual. - if ( isset( $data['virtual'] ) ) { - $product->set_virtual( $data['virtual'] ); - } - - // Tax status. - if ( isset( $data['tax_status'] ) ) { - $product->set_tax_status( wc_clean( $data['tax_status'] ) ); - } - - // Tax Class. - if ( isset( $data['tax_class'] ) ) { - $product->set_tax_class( wc_clean( $data['tax_class'] ) ); - } - - // Catalog Visibility. - if ( isset( $data['catalog_visibility'] ) ) { - $product->set_catalog_visibility( wc_clean( $data['catalog_visibility'] ) ); - } - - // Purchase Note. - if ( isset( $data['purchase_note'] ) ) { - $product->set_purchase_note( wc_clean( $data['purchase_note'] ) ); - } - - // Featured Product. - if ( isset( $data['featured'] ) ) { - $product->set_featured( $data['featured'] ); - } - - // Shipping data. - $product = $this->save_product_shipping_data( $product, $data ); - - // SKU. - if ( isset( $data['sku'] ) ) { - $sku = $product->get_sku(); - $new_sku = wc_clean( $data['sku'] ); - - if ( '' == $new_sku ) { - $product->set_sku( '' ); - } elseif ( $new_sku !== $sku ) { - if ( ! empty( $new_sku ) ) { - $unique_sku = wc_product_has_unique_sku( $product->get_id(), $new_sku ); - if ( ! $unique_sku ) { - throw new WC_API_Exception( 'woocommerce_api_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400 ); - } else { - $product->set_sku( $new_sku ); - } - } else { - $product->set_sku( '' ); - } - } - } - - // Attributes. - if ( isset( $data['attributes'] ) ) { - $attributes = array(); - - foreach ( $data['attributes'] as $attribute ) { - $is_taxonomy = 0; - $taxonomy = 0; - - if ( ! isset( $attribute['name'] ) ) { - continue; - } - - $attribute_slug = sanitize_title( $attribute['name'] ); - - if ( isset( $attribute['slug'] ) ) { - $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); - $attribute_slug = sanitize_title( $attribute['slug'] ); - } - - if ( $taxonomy ) { - $is_taxonomy = 1; - } - - if ( $is_taxonomy ) { - - $attribute_id = wc_attribute_taxonomy_id_by_name( $attribute['name'] ); - - 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(); - } - - // Update post terms - if ( taxonomy_exists( $taxonomy ) ) { - wp_set_object_terms( $product->get_id(), $values, $taxonomy ); - } - - 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( $taxonomy ); - $attribute_object->set_options( $values ); - $attribute_object->set_position( isset( $attribute['position'] ) ? 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'] ) ) { - // Array based. - if ( is_array( $attribute['options'] ) ) { - $values = $attribute['options']; - - // Text based, separate by pipe. - } else { - $values = array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) ); - } - - // Custom attribute - Add attribute to array and set the values. - $attribute_object = new WC_Product_Attribute(); - $attribute_object->set_name( $attribute['name'] ); - $attribute_object->set_options( $values ); - $attribute_object->set_position( isset( $attribute['position'] ) ? 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; - } - } - - uasort( $attributes, 'wc_product_attribute_uasort_comparison' ); - - $product->set_attributes( $attributes ); - } - - // Sales and prices. - if ( in_array( $product->get_type(), array( 'variable', 'grouped' ) ) ) { - - // Variable and grouped products have no prices. - $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( $data['regular_price'] ) ) { - $regular_price = ( '' === $data['regular_price'] ) ? '' : $data['regular_price']; - $product->set_regular_price( $regular_price ); - } - - // Sale Price. - if ( isset( $data['sale_price'] ) ) { - $sale_price = ( '' === $data['sale_price'] ) ? '' : $data['sale_price']; - $product->set_sale_price( $sale_price ); - } - - if ( isset( $data['sale_price_dates_from'] ) ) { - $date_from = $data['sale_price_dates_from']; - } else { - $date_from = $product->get_date_on_sale_from() ? date( 'Y-m-d', $product->get_date_on_sale_from()->getTimestamp() ) : ''; - } - - if ( isset( $data['sale_price_dates_to'] ) ) { - $date_to = $data['sale_price_dates_to']; - } else { - $date_to = $product->get_date_on_sale_to() ? date( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ) : ''; - } - - if ( $date_to && ! $date_from ) { - $date_from = strtotime( 'NOW', current_time( 'timestamp', true ) ); - } - - $product->set_date_on_sale_to( $date_to ); - $product->set_date_on_sale_from( $date_from ); - - if ( $product->is_on_sale( 'edit' ) ) { - $product->set_price( $product->get_sale_price( 'edit' ) ); - } else { - $product->set_price( $product->get_regular_price( 'edit' ) ); - } - } - - // Product parent ID for groups. - if ( isset( $data['parent_id'] ) ) { - $product->set_parent_id( absint( $data['parent_id'] ) ); - } - - // Sold Individually. - if ( isset( $data['sold_individually'] ) ) { - $product->set_sold_individually( true === $data['sold_individually'] ? 'yes' : '' ); - } - - // Stock status. - if ( isset( $data['in_stock'] ) ) { - $stock_status = ( true === $data['in_stock'] ) ? 'instock' : 'outofstock'; - } else { - $stock_status = $product->get_stock_status(); - - if ( '' === $stock_status ) { - $stock_status = 'instock'; - } - } - - // Stock Data. - if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) { - // Manage stock. - if ( isset( $data['managing_stock'] ) ) { - $managing_stock = ( true === $data['managing_stock'] ) ? 'yes' : 'no'; - $product->set_manage_stock( $managing_stock ); - } else { - $managing_stock = $product->get_manage_stock() ? 'yes' : 'no'; - } - - // Backorders. - if ( isset( $data['backorders'] ) ) { - if ( 'notify' === $data['backorders'] ) { - $backorders = 'notify'; - } else { - $backorders = ( true === $data['backorders'] ) ? 'yes' : 'no'; - } - - $product->set_backorders( $backorders ); - } else { - $backorders = $product->get_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 ( 'yes' == $managing_stock ) { - $product->set_backorders( $backorders ); - - // 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( $data['stock_quantity'] ) ) { - $product->set_stock_quantity( wc_stock_amount( $data['stock_quantity'] ) ); - } elseif ( isset( $data['inventory_delta'] ) ) { - $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); - $stock_quantity += wc_stock_amount( $data['inventory_delta'] ); - $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); - } - } else { - // Don't manage stock. - $product->set_manage_stock( 'no' ); - $product->set_backorders( $backorders ); - $product->set_stock_quantity( '' ); - $product->set_stock_status( $stock_status ); - } - } elseif ( ! $product->is_type( 'variable' ) ) { - $product->set_stock_status( $stock_status ); - } - - // Upsells. - if ( isset( $data['upsell_ids'] ) ) { - $upsells = array(); - $ids = $data['upsell_ids']; - - if ( ! empty( $ids ) ) { - foreach ( $ids as $id ) { - if ( $id && $id > 0 ) { - $upsells[] = $id; - } - } - - $product->set_upsell_ids( $upsells ); - } else { - $product->set_upsell_ids( array() ); - } - } - - // Cross sells. - if ( isset( $data['cross_sell_ids'] ) ) { - $crosssells = array(); - $ids = $data['cross_sell_ids']; - - if ( ! empty( $ids ) ) { - foreach ( $ids as $id ) { - if ( $id && $id > 0 ) { - $crosssells[] = $id; - } - } - - $product->set_cross_sell_ids( $crosssells ); - } else { - $product->set_cross_sell_ids( array() ); - } - } - - // Product categories. - if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) { - $product->set_category_ids( $data['categories'] ); - } - - // Product tags. - if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) { - $product->set_tag_ids( $data['tags'] ); - } - - // Downloadable. - if ( isset( $data['downloadable'] ) ) { - $is_downloadable = ( true === $data['downloadable'] ) ? 'yes' : 'no'; - $product->set_downloadable( $is_downloadable ); - } else { - $is_downloadable = $product->get_downloadable() ? 'yes' : 'no'; - } - - // Downloadable options. - if ( 'yes' == $is_downloadable ) { - - // Downloadable files. - if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { - $product = $this->save_downloadable_files( $product, $data['downloads'] ); - } - - // Download limit. - if ( isset( $data['download_limit'] ) ) { - $product->set_download_limit( $data['download_limit'] ); - } - - // Download expiry. - if ( isset( $data['download_expiry'] ) ) { - $product->set_download_expiry( $data['download_expiry'] ); - } - } - - // Product url. - if ( $product->is_type( 'external' ) ) { - if ( isset( $data['product_url'] ) ) { - $product->set_product_url( $data['product_url'] ); - } - - if ( isset( $data['button_text'] ) ) { - $product->set_button_text( $data['button_text'] ); - } - } - - // Reviews allowed. - if ( isset( $data['reviews_allowed'] ) ) { - $product->set_reviews_allowed( $data['reviews_allowed'] ); - } - - // Save default attributes for variable products. - if ( $product->is_type( 'variable' ) ) { - $product = $this->save_default_attributes( $product, $data ); - } - - // Do action for product type - do_action( 'woocommerce_api_process_product_meta_' . $product->get_type(), $product->get_id(), $data ); - - return $product; - } - - /** - * Save variations. - * - * @since 2.2 - * - * @param WC_Product $product - * @param array $request - * - * @return bool - * @throws WC_API_Exception - */ - protected function save_variations( $product, $request ) { - global $wpdb; - - $id = $product->get_id(); - $variations = $request['variations']; - $attributes = $product->get_attributes(); - - foreach ( $variations as $menu_order => $data ) { - $variation_id = isset( $data['id'] ) ? absint( $data['id'] ) : 0; - $variation = new WC_Product_Variation( $variation_id ); - - // 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 = current( $data['image'] ); - if ( is_array( $image ) ) { - $image['position'] = 0; - } - - $variation = $this->save_product_images( $variation, array( $image ) ); - } - - // Virtual variation. - if ( isset( $data['virtual'] ) ) { - $variation->set_virtual( $data['virtual'] ); - } - - // Downloadable variation. - if ( isset( $data['downloadable'] ) ) { - $is_downloadable = $data['downloadable']; - $variation->set_downloadable( $is_downloadable ); - } else { - $is_downloadable = $variation->get_downloadable(); - } - - // Downloads. - if ( $is_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. - $manage_stock = (bool) $variation->get_manage_stock(); - if ( isset( $data['managing_stock'] ) ) { - $manage_stock = $data['managing_stock']; - } - $variation->set_manage_stock( $manage_stock ); - - $stock_status = $variation->get_stock_status(); - if ( isset( $data['in_stock'] ) ) { - $stock_status = true === $data['in_stock'] ? 'instock' : 'outofstock'; - } - $variation->set_stock_status( $stock_status ); - - $backorders = $variation->get_backorders(); - if ( isset( $data['backorders'] ) ) { - $backorders = $data['backorders']; - } - $variation->set_backorders( $backorders ); - - if ( $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['sale_price_dates_from'] ) ) { - $variation->set_date_on_sale_from( $data['sale_price_dates_from'] ); - } - - if ( isset( $data['sale_price_dates_to'] ) ) { - $variation->set_date_on_sale_to( $data['sale_price_dates_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(); - - foreach ( $data['attributes'] as $attribute_key => $attribute ) { - if ( ! isset( $attribute['name'] ) ) { - continue; - } - - $taxonomy = 0; - $_attribute = array(); - - if ( isset( $attribute['slug'] ) ) { - $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); - } - - if ( ! $taxonomy ) { - $taxonomy = sanitize_title( $attribute['name'] ); - } - - if ( isset( $attributes[ $taxonomy ] ) ) { - $_attribute = $attributes[ $taxonomy ]; - } - - if ( isset( $_attribute['is_variation'] ) && $_attribute['is_variation'] ) { - $_attribute_key = sanitize_title( $_attribute['name'] ); - - if ( isset( $_attribute['is_taxonomy'] ) && $_attribute['is_taxonomy'] ) { - // Don't use wc_clean as it destroys sanitized characters. - $_attribute_value = isset( $attribute['option'] ) ? sanitize_title( stripslashes( $attribute['option'] ) ) : ''; - } else { - $_attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; - } - - $_attributes[ $_attribute_key ] = $_attribute_value; - } - } - - $variation->set_attributes( $_attributes ); - } - - $variation->save(); - - do_action( 'woocommerce_api_save_product_variation', $variation_id, $menu_order, $variation ); - } - - return true; - } - - /** - * Save product shipping data - * - * @since 2.2 - * @param WC_Product $product - * @param array $data - * @return WC_Product - */ - private function save_product_shipping_data( $product, $data ) { - if ( isset( $data['weight'] ) ) { - $product->set_weight( '' === $data['weight'] ? '' : wc_format_decimal( $data['weight'] ) ); - } - - // Product dimensions - if ( isset( $data['dimensions'] ) ) { - // Height - if ( isset( $data['dimensions']['height'] ) ) { - $product->set_height( '' === $data['dimensions']['height'] ? '' : wc_format_decimal( $data['dimensions']['height'] ) ); - } - - // Width - if ( isset( $data['dimensions']['width'] ) ) { - $product->set_width( '' === $data['dimensions']['width'] ? '' : wc_format_decimal( $data['dimensions']['width'] ) ); - } - - // Length - if ( isset( $data['dimensions']['length'] ) ) { - $product->set_length( '' === $data['dimensions']['length'] ? '' : wc_format_decimal( $data['dimensions']['length'] ) ); - } - } - - // Virtual - if ( isset( $data['virtual'] ) ) { - $virtual = ( true === $data['virtual'] ) ? 'yes' : 'no'; - - if ( 'yes' == $virtual ) { - $product->set_weight( '' ); - $product->set_height( '' ); - $product->set_length( '' ); - $product->set_width( '' ); - } - } - - // Shipping class - if ( isset( $data['shipping_class'] ) ) { - $data_store = $product->get_data_store(); - $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) ); - $product->set_shipping_class_id( $shipping_class_id ); - } - - return $product; - } - - /** - * Save downloadable files - * - * @since 2.2 - * @param WC_Product $product - * @param array $downloads - * @param int $deprecated Deprecated since 3.0. - * @return WC_Product - */ - private function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { - if ( $deprecated ) { - wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() does not require a variation_id anymore.' ); - } - - $files = array(); - foreach ( $downloads as $key => $file ) { - if ( isset( $file['url'] ) ) { - $file['file'] = $file['url']; - } - - if ( empty( $file['file'] ) ) { - continue; - } - - $download = new WC_Product_Download(); - $download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() ); - $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); - $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); - $files[] = $download; - } - $product->set_downloads( $files ); - - return $product; - } - - /** - * Get attribute taxonomy by slug. - * - * @since 2.2 - * @param string $slug - * @return string|null - */ - private function get_attribute_taxonomy_by_slug( $slug ) { - $taxonomy = null; - $attribute_taxonomies = wc_get_attribute_taxonomies(); - - foreach ( $attribute_taxonomies as $key => $tax ) { - if ( $slug == $tax->attribute_name ) { - $taxonomy = 'pa_' . $tax->attribute_name; - - break; - } - } - - return $taxonomy; - } - - /** - * 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' => (int) $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; - } - - /** - * Save product images. - * - * @since 2.2 - * @param WC_Product $product - * @param array $images - * @throws WC_API_Exception - * @return WC_Product - */ - protected function save_product_images( $product, $images ) { - if ( is_array( $images ) ) { - $gallery = array(); - - foreach ( $images as $image ) { - if ( isset( $image['position'] ) && 0 == $image['position'] ) { - $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; - - if ( 0 === $attachment_id && isset( $image['src'] ) ) { - $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); - - if ( is_wp_error( $upload ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 ); - } - - $attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() ); - } - - $product->set_image_id( $attachment_id ); - } else { - $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; - - if ( 0 === $attachment_id && isset( $image['src'] ) ) { - $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); - - if ( is_wp_error( $upload ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 ); - } - - $attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() ); - } - - $gallery[] = $attachment_id; - } - - // Set the image alt if present. - if ( ! empty( $image['alt'] ) && $attachment_id ) { - update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); - } - - // Set the image title if present. - if ( ! empty( $image['title'] ) && $attachment_id ) { - wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['title'] ) ); - } - } - - if ( ! empty( $gallery ) ) { - $product->set_gallery_image_ids( $gallery ); - } - } else { - $product->set_image_id( '' ); - $product->set_gallery_image_ids( array() ); - } - - return $product; - } - - /** - * Upload image from URL - * - * @since 2.2 - * @param string $image_url - * @return int|WP_Error attachment id - */ - public function upload_product_image( $image_url ) { - return $this->upload_image_from_url( $image_url, 'product_image' ); - } - - /** - * Upload product category image from URL. - * - * @since 2.5.0 - * @param string $image_url - * @return int|WP_Error attachment id - */ - public function upload_product_category_image( $image_url ) { - return $this->upload_image_from_url( $image_url, 'product_category_image' ); - } - - /** - * Upload image from URL. - * - * @throws WC_API_Exception - * - * @since 2.5.0 - * @param string $image_url - * @param string $upload_for - * @return array - */ - protected function upload_image_from_url( $image_url, $upload_for = 'product_image' ) { - $upload = wc_rest_upload_image_from_url( $image_url ); - if ( is_wp_error( $upload ) ) { - throw new WC_API_Exception( 'woocommerce_api_' . $upload_for . '_upload_error', $upload->get_error_message(), 400 ); - } - - do_action( 'woocommerce_api_uploaded_image_from_url', $upload, $image_url, $upload_for ); - - return $upload; - } - - /** - * Sets product image as attachment and returns the attachment ID. - * - * @since 2.2 - * @param array $upload - * @param int $id - * @return int - */ - protected function set_product_image_as_attachment( $upload, $id ) { - return $this->set_uploaded_image_as_attachment( $upload, $id ); - } - - /** - * Sets uploaded category image as attachment and returns the attachment ID. - * - * @since 2.5.0 - * @param integer $upload Upload information from wp_upload_bits - * @return int Attachment ID - */ - protected function set_product_category_image_as_attachment( $upload ) { - return $this->set_uploaded_image_as_attachment( $upload ); - } - - /** - * Set uploaded image as attachment. - * - * @since 2.5.0 - * @param array $upload Upload information from wp_upload_bits - * @param int $id Post ID. Default to 0. - * @return int Attachment ID - */ - protected function set_uploaded_image_as_attachment( $upload, $id = 0 ) { - $info = wp_check_filetype( $upload['file'] ); - $title = ''; - $content = ''; - - if ( $image_meta = @wp_read_image_metadata( $upload['file'] ) ) { - if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { - $title = wc_clean( $image_meta['title'] ); - } - if ( trim( $image_meta['caption'] ) ) { - $content = wc_clean( $image_meta['caption'] ); - } - } - - $attachment = array( - 'post_mime_type' => $info['type'], - 'guid' => $upload['url'], - 'post_parent' => $id, - 'post_title' => $title, - 'post_content' => $content, - ); - - $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id ); - if ( ! is_wp_error( $attachment_id ) ) { - wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) ); - } - - return $attachment_id; - } - - /** - * 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' => wc_attribute_label( str_replace( 'attribute_', '', $attribute_name ), $product ), - 'slug' => str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $attribute_name ) ), - 'option' => $attribute, - ); - } - } else { - - foreach ( $product->get_attributes() as $attribute ) { - $attributes[] = array( - 'name' => wc_attribute_label( $attribute['name'], $product ), - 'slug' => wc_attribute_taxonomy_slug( $attribute['name'] ), - 'position' => (int) $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; - } - - /** - * Get a listing of product attributes - * - * @since 2.5.0 - * - * @param string|null $fields fields to limit response to - * - * @return array|WP_Error - */ - public function get_product_attributes( $fields = null ) { - try { - // Permissions check. - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 ); - } - - $product_attributes = array(); - $attribute_taxonomies = wc_get_attribute_taxonomies(); - - foreach ( $attribute_taxonomies as $attribute ) { - $product_attributes[] = array( - 'id' => intval( $attribute->attribute_id ), - 'name' => $attribute->attribute_label, - 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ), - 'type' => $attribute->attribute_type, - 'order_by' => $attribute->attribute_orderby, - 'has_archives' => (bool) $attribute->attribute_public, - ); - } - - return array( 'product_attributes' => apply_filters( 'woocommerce_api_product_attributes_response', $product_attributes, $attribute_taxonomies, $fields, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get the product attribute for the given ID - * - * @since 2.5.0 - * - * @param string $id product attribute term ID - * @param string|null $fields fields to limit response to - * - * @return array|WP_Error - */ - public function get_product_attribute( $id, $fields = null ) { - global $wpdb; - - try { - $id = absint( $id ); - - // Validate ID - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 ); - } - - // Permissions check - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 ); - } - - $attribute = $wpdb->get_row( $wpdb->prepare( " - SELECT * - FROM {$wpdb->prefix}woocommerce_attribute_taxonomies - WHERE attribute_id = %d - ", $id ) ); - - if ( is_wp_error( $attribute ) || is_null( $attribute ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $product_attribute = array( - 'id' => intval( $attribute->attribute_id ), - 'name' => $attribute->attribute_label, - 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ), - 'type' => $attribute->attribute_type, - 'order_by' => $attribute->attribute_orderby, - 'has_archives' => (bool) $attribute->attribute_public, - ); - - return array( 'product_attribute' => apply_filters( 'woocommerce_api_product_attribute_response', $product_attribute, $id, $fields, $attribute, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Validate attribute data. - * - * @since 2.5.0 - * @param string $name - * @param string $slug - * @param string $type - * @param string $order_by - * @param bool $new_data - * @return bool - * @throws WC_API_Exception - */ - protected function validate_attribute_data( $name, $slug, $type, $order_by, $new_data = true ) { - if ( empty( $name ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 ); - } - - if ( strlen( $slug ) >= 28 ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), 400 ); - } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), 400 ); - } elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), 400 ); - } - - // Validate the attribute type - if ( ! in_array( wc_clean( $type ), array_keys( wc_get_attribute_types() ) ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_type', sprintf( __( 'Invalid product attribute type - the product attribute type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_attribute_types() ) ) ), 400 ); - } - - // Validate the attribute order by - if ( ! in_array( wc_clean( $order_by ), array( 'menu_order', 'name', 'name_num', 'id' ) ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_order_by', sprintf( __( 'Invalid product attribute order_by type - the product attribute order_by type must be any of these: %s', 'woocommerce' ), implode( ', ', array( 'menu_order', 'name', 'name_num', 'id' ) ) ), 400 ); - } - - return true; - } - - /** - * Create a new product attribute. - * - * @since 2.5.0 - * - * @param array $data Posted data. - * - * @return array|WP_Error - */ - public function create_product_attribute( $data ) { - global $wpdb; - - try { - if ( ! isset( $data['product_attribute'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute' ), 400 ); - } - - $data = $data['product_attribute']; - - // Check permissions. - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 ); - } - - $data = apply_filters( 'woocommerce_api_create_product_attribute_data', $data, $this ); - - if ( ! isset( $data['name'] ) ) { - $data['name'] = ''; - } - - // Set the attribute slug. - if ( ! isset( $data['slug'] ) ) { - $data['slug'] = wc_sanitize_taxonomy_name( stripslashes( $data['name'] ) ); - } else { - $data['slug'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ) ); - } - - // Set attribute type when not sent. - if ( ! isset( $data['type'] ) ) { - $data['type'] = 'select'; - } - - // Set order by when not sent. - if ( ! isset( $data['order_by'] ) ) { - $data['order_by'] = 'menu_order'; - } - - // Validate the attribute data. - $this->validate_attribute_data( $data['name'], $data['slug'], $data['type'], $data['order_by'], true ); - - $insert = $wpdb->insert( - $wpdb->prefix . 'woocommerce_attribute_taxonomies', - array( - 'attribute_label' => $data['name'], - 'attribute_name' => $data['slug'], - 'attribute_type' => $data['type'], - 'attribute_orderby' => $data['order_by'], - 'attribute_public' => isset( $data['has_archives'] ) && true === $data['has_archives'] ? 1 : 0, - ), - array( '%s', '%s', '%s', '%s', '%d' ) - ); - - // Checks for an error in the product creation. - if ( is_wp_error( $insert ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $insert->get_error_message(), 400 ); - } - - $id = $wpdb->insert_id; - - do_action( 'woocommerce_api_create_product_attribute', $id, $data ); - - // Clear transients. - wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); - delete_transient( 'wc_attribute_taxonomies' ); - WC_Cache_Helper::incr_cache_prefix( 'woocommerce-attributes' ); - - $this->server->send_status( 201 ); - - return $this->get_product_attribute( $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Edit a product attribute. - * - * @since 2.5.0 - * - * @param int $id the attribute ID. - * @param array $data - * - * @return array|WP_Error - */ - public function edit_product_attribute( $id, $data ) { - global $wpdb; - - try { - if ( ! isset( $data['product_attribute'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute' ), 400 ); - } - - $id = absint( $id ); - $data = $data['product_attribute']; - - // Check permissions. - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 ); - } - - $data = apply_filters( 'woocommerce_api_edit_product_attribute_data', $data, $this ); - $attribute = $this->get_product_attribute( $id ); - - if ( is_wp_error( $attribute ) ) { - return $attribute; - } - - $attribute_name = isset( $data['name'] ) ? $data['name'] : $attribute['product_attribute']['name']; - $attribute_type = isset( $data['type'] ) ? $data['type'] : $attribute['product_attribute']['type']; - $attribute_order_by = isset( $data['order_by'] ) ? $data['order_by'] : $attribute['product_attribute']['order_by']; - - if ( isset( $data['slug'] ) ) { - $attribute_slug = wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ); - } else { - $attribute_slug = $attribute['product_attribute']['slug']; - } - $attribute_slug = preg_replace( '/^pa\_/', '', $attribute_slug ); - - if ( isset( $data['has_archives'] ) ) { - $attribute_public = true === $data['has_archives'] ? 1 : 0; - } else { - $attribute_public = $attribute['product_attribute']['has_archives']; - } - - // Validate the attribute data. - $this->validate_attribute_data( $attribute_name, $attribute_slug, $attribute_type, $attribute_order_by, false ); - - $update = $wpdb->update( - $wpdb->prefix . 'woocommerce_attribute_taxonomies', - array( - 'attribute_label' => $attribute_name, - 'attribute_name' => $attribute_slug, - 'attribute_type' => $attribute_type, - 'attribute_orderby' => $attribute_order_by, - 'attribute_public' => $attribute_public, - ), - array( 'attribute_id' => $id ), - array( '%s', '%s', '%s', '%s', '%d' ), - array( '%d' ) - ); - - // Checks for an error in the product creation. - if ( false === $update ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute', __( 'Could not edit the attribute', 'woocommerce' ), 400 ); - } - - do_action( 'woocommerce_api_edit_product_attribute', $id, $data ); - - // Clear transients. - wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); - delete_transient( 'wc_attribute_taxonomies' ); - WC_Cache_Helper::incr_cache_prefix( 'woocommerce-attributes' ); - - return $this->get_product_attribute( $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete a product attribute. - * - * @since 2.5.0 - * - * @param int $id the product attribute ID. - * - * @return array|WP_Error - */ - public function delete_product_attribute( $id ) { - global $wpdb; - - try { - // Check permissions. - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute', __( 'You do not have permission to delete product attributes', 'woocommerce' ), 401 ); - } - - $id = absint( $id ); - - $attribute_name = $wpdb->get_var( $wpdb->prepare( " - SELECT attribute_name - FROM {$wpdb->prefix}woocommerce_attribute_taxonomies - WHERE attribute_id = %d - ", $id ) ); - - if ( is_null( $attribute_name ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $deleted = $wpdb->delete( - $wpdb->prefix . 'woocommerce_attribute_taxonomies', - array( 'attribute_id' => $id ), - array( '%d' ) - ); - - if ( false === $deleted ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute', __( 'Could not delete the attribute', 'woocommerce' ), 401 ); - } - - $taxonomy = wc_attribute_taxonomy_name( $attribute_name ); - - if ( taxonomy_exists( $taxonomy ) ) { - $terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' ); - foreach ( $terms as $term ) { - wp_delete_term( $term->term_id, $taxonomy ); - } - } - - do_action( 'woocommerce_attribute_deleted', $id, $attribute_name, $taxonomy ); - do_action( 'woocommerce_api_delete_product_attribute', $id, $this ); - - // Clear transients. - wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); - delete_transient( 'wc_attribute_taxonomies' ); - WC_Cache_Helper::incr_cache_prefix( 'woocommerce-attributes' ); - - return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get a listing of product attribute terms. - * - * @since 2.5.0 - * - * @param int $attribute_id Attribute ID. - * @param string|null $fields Fields to limit response to. - * - * @return array|WP_Error - */ - public function get_product_attribute_terms( $attribute_id, $fields = null ) { - try { - // Permissions check. - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attribute_terms', __( 'You do not have permission to read product attribute terms', 'woocommerce' ), 401 ); - } - - $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); - - if ( ! $taxonomy ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $terms = get_terms( $taxonomy, array( 'hide_empty' => false ) ); - $attribute_terms = array(); - - foreach ( $terms as $term ) { - $attribute_terms[] = array( - 'id' => $term->term_id, - 'slug' => $term->slug, - 'name' => $term->name, - 'count' => $term->count, - ); - } - - return array( 'product_attribute_terms' => apply_filters( 'woocommerce_api_product_attribute_terms_response', $attribute_terms, $terms, $fields, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get the product attribute term for the given ID. - * - * @since 2.5.0 - * - * @param int $attribute_id Attribute ID. - * @param string $id Product attribute term ID. - * @param string|null $fields Fields to limit response to. - * - * @return array|WP_Error - */ - public function get_product_attribute_term( $attribute_id, $id, $fields = null ) { - global $wpdb; - - try { - $id = absint( $id ); - - // Validate ID - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_term_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 ); - } - - // Permissions check - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attribute_terms', __( 'You do not have permission to read product attribute terms', 'woocommerce' ), 401 ); - } - - $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); - - if ( ! $taxonomy ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $term = get_term( $id, $taxonomy ); - - if ( is_wp_error( $term ) || is_null( $term ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_term_id', __( 'A product attribute term with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $attribute_term = array( - 'id' => $term->term_id, - 'name' => $term->name, - 'slug' => $term->slug, - 'count' => $term->count, - ); - - return array( 'product_attribute_term' => apply_filters( 'woocommerce_api_product_attribute_response', $attribute_term, $id, $fields, $term, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Create a new product attribute term. - * - * @since 2.5.0 - * - * @param int $attribute_id Attribute ID. - * @param array $data Posted data. - * - * @return array|WP_Error - */ - public function create_product_attribute_term( $attribute_id, $data ) { - global $wpdb; - - try { - if ( ! isset( $data['product_attribute_term'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute_term' ), 400 ); - } - - $data = $data['product_attribute_term']; - - // Check permissions. - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 ); - } - - $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); - - if ( ! $taxonomy ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $data = apply_filters( 'woocommerce_api_create_product_attribute_term_data', $data, $this ); - - // Check if attribute term name is specified. - if ( ! isset( $data['name'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 ); - } - - $args = array(); - - // Set the attribute term slug. - if ( isset( $data['slug'] ) ) { - $args['slug'] = sanitize_title( wp_unslash( $data['slug'] ) ); - } - - $term = wp_insert_term( $data['name'], $taxonomy, $args ); - - // Checks for an error in the term creation. - if ( is_wp_error( $term ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $term->get_error_message(), 400 ); - } - - $id = $term['term_id']; - - do_action( 'woocommerce_api_create_product_attribute_term', $id, $data ); - - $this->server->send_status( 201 ); - - return $this->get_product_attribute_term( $attribute_id, $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Edit a product attribute term. - * - * @since 2.5.0 - * - * @param int $attribute_id Attribute ID. - * @param int $id the attribute ID. - * @param array $data - * - * @return array|WP_Error - */ - public function edit_product_attribute_term( $attribute_id, $id, $data ) { - global $wpdb; - - try { - if ( ! isset( $data['product_attribute_term'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute_term' ), 400 ); - } - - $id = absint( $id ); - $data = $data['product_attribute_term']; - - // Check permissions. - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 ); - } - - $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); - - if ( ! $taxonomy ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $data = apply_filters( 'woocommerce_api_edit_product_attribute_term_data', $data, $this ); - - $args = array(); - - // Update name. - if ( isset( $data['name'] ) ) { - $args['name'] = wc_clean( wp_unslash( $data['name'] ) ); - } - - // Update slug. - if ( isset( $data['slug'] ) ) { - $args['slug'] = sanitize_title( wp_unslash( $data['slug'] ) ); - } - - $term = wp_update_term( $id, $taxonomy, $args ); - - if ( is_wp_error( $term ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute_term', $term->get_error_message(), 400 ); - } - - do_action( 'woocommerce_api_edit_product_attribute_term', $id, $data ); - - return $this->get_product_attribute_term( $attribute_id, $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete a product attribute term. - * - * @since 2.5.0 - * - * @param int $attribute_id Attribute ID. - * @param int $id the product attribute ID. - * - * @return array|WP_Error - */ - public function delete_product_attribute_term( $attribute_id, $id ) { - global $wpdb; - - try { - // Check permissions. - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute_term', __( 'You do not have permission to delete product attribute terms', 'woocommerce' ), 401 ); - } - - $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); - - if ( ! $taxonomy ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $id = absint( $id ); - $term = wp_delete_term( $id, $taxonomy ); - - if ( ! $term ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute_term', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product_attribute_term' ), 500 ); - } elseif ( is_wp_error( $term ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute_term', $term->get_error_message(), 400 ); - } - - do_action( 'woocommerce_api_delete_product_attribute_term', $id, $this ); - - return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Clear product - * - * @param int $product_id - */ - protected function clear_product( $product_id ) { - if ( ! is_numeric( $product_id ) || 0 >= $product_id ) { - return; - } - - // Delete product attachments - $attachments = get_children( array( - 'post_parent' => $product_id, - 'post_status' => 'any', - 'post_type' => 'attachment', - ) ); - - foreach ( (array) $attachments as $attachment ) { - wp_delete_attachment( $attachment->ID, true ); - } - - // Delete product - $product = wc_get_product( $product_id ); - $product->delete( true ); - } - - /** - * Bulk update or insert products - * Accepts an array with products in the formats supported by - * WC_API_Products->create_product() and WC_API_Products->edit_product() - * - * @since 2.4.0 - * - * @param array $data - * - * @return array|WP_Error - */ - public function bulk( $data ) { - - try { - if ( ! isset( $data['products'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_products_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'products' ), 400 ); - } - - $data = $data['products']; - $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'products' ); - - // Limit bulk operation - if ( count( $data ) > $limit ) { - throw new WC_API_Exception( 'woocommerce_api_products_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); - } - - $products = array(); - - foreach ( $data as $_product ) { - $product_id = 0; - $product_sku = ''; - - // Try to get the product ID - if ( isset( $_product['id'] ) ) { - $product_id = intval( $_product['id'] ); - } - - if ( ! $product_id && isset( $_product['sku'] ) ) { - $product_sku = wc_clean( $_product['sku'] ); - $product_id = wc_get_product_id_by_sku( $product_sku ); - } - - if ( $product_id ) { - - // Product exists / edit product - $edit = $this->edit_product( $product_id, array( 'product' => $_product ) ); - - if ( is_wp_error( $edit ) ) { - $products[] = array( - 'id' => $product_id, - 'sku' => $product_sku, - 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), - ); - } else { - $products[] = $edit['product']; - } - } else { - - // Product don't exists / create product - $new = $this->create_product( array( 'product' => $_product ) ); - - if ( is_wp_error( $new ) ) { - $products[] = array( - 'id' => $product_id, - 'sku' => $product_sku, - 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), - ); - } else { - $products[] = $new['product']; - } - } - } - - return array( 'products' => apply_filters( 'woocommerce_api_products_bulk_response', $products, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get a listing of product shipping classes. - * - * @since 2.5.0 - * @param string|null $fields Fields to limit response to - * @return array|WP_Error List of product shipping classes if succeed, - * otherwise WP_Error will be returned - */ - public function get_product_shipping_classes( $fields = null ) { - try { - // Permissions check - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_shipping_classes', __( 'You do not have permission to read product shipping classes', 'woocommerce' ), 401 ); - } - - $product_shipping_classes = array(); - - $terms = get_terms( 'product_shipping_class', array( 'hide_empty' => false, 'fields' => 'ids' ) ); - - foreach ( $terms as $term_id ) { - $product_shipping_classes[] = current( $this->get_product_shipping_class( $term_id, $fields ) ); - } - - return array( 'product_shipping_classes' => apply_filters( 'woocommerce_api_product_shipping_classes_response', $product_shipping_classes, $terms, $fields, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get the product shipping class for the given ID. - * - * @since 2.5.0 - * @param string $id Product shipping class term ID - * @param string|null $fields Fields to limit response to - * @return array|WP_Error Product shipping class if succeed, otherwise - * WP_Error will be returned - */ - public function get_product_shipping_class( $id, $fields = null ) { - try { - $id = absint( $id ); - if ( ! $id ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_id', __( 'Invalid product shipping class ID', 'woocommerce' ), 400 ); - } - - // Permissions check - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_shipping_classes', __( 'You do not have permission to read product shipping classes', 'woocommerce' ), 401 ); - } - - $term = get_term( $id, 'product_shipping_class' ); - - if ( is_wp_error( $term ) || is_null( $term ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_id', __( 'A product shipping class with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $term_id = intval( $term->term_id ); - - $product_shipping_class = array( - 'id' => $term_id, - 'name' => $term->name, - 'slug' => $term->slug, - 'parent' => $term->parent, - 'description' => $term->description, - 'count' => intval( $term->count ), - ); - - return array( 'product_shipping_class' => apply_filters( 'woocommerce_api_product_shipping_class_response', $product_shipping_class, $id, $fields, $term, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Create a new product shipping class. - * - * @since 2.5.0 - * @param array $data Posted data - * @return array|WP_Error Product shipping class if succeed, otherwise - * WP_Error will be returned - */ - public function create_product_shipping_class( $data ) { - global $wpdb; - - try { - if ( ! isset( $data['product_shipping_class'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_shipping_class_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_shipping_class' ), 400 ); - } - - // Check permissions - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_shipping_class', __( 'You do not have permission to create product shipping classes', 'woocommerce' ), 401 ); - } - - $defaults = array( - 'name' => '', - 'slug' => '', - 'description' => '', - 'parent' => 0, - ); - - $data = wp_parse_args( $data['product_shipping_class'], $defaults ); - $data = apply_filters( 'woocommerce_api_create_product_shipping_class_data', $data, $this ); - - // Check parent. - $data['parent'] = absint( $data['parent'] ); - if ( $data['parent'] ) { - $parent = get_term_by( 'id', $data['parent'], 'product_shipping_class' ); - if ( ! $parent ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_parent', __( 'Product shipping class parent is invalid', 'woocommerce' ), 400 ); - } - } - - $insert = wp_insert_term( $data['name'], 'product_shipping_class', $data ); - if ( is_wp_error( $insert ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_shipping_class', $insert->get_error_message(), 400 ); - } - - $id = $insert['term_id']; - - do_action( 'woocommerce_api_create_product_shipping_class', $id, $data ); - - $this->server->send_status( 201 ); - - return $this->get_product_shipping_class( $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Edit a product shipping class. - * - * @since 2.5.0 - * @param int $id Product shipping class term ID - * @param array $data Posted data - * @return array|WP_Error Product shipping class if succeed, otherwise - * WP_Error will be returned - */ - public function edit_product_shipping_class( $id, $data ) { - global $wpdb; - - try { - if ( ! isset( $data['product_shipping_class'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_shipping_class', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_shipping_class' ), 400 ); - } - - $id = absint( $id ); - $data = $data['product_shipping_class']; - - // Check permissions - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_shipping_class', __( 'You do not have permission to edit product shipping classes', 'woocommerce' ), 401 ); - } - - $data = apply_filters( 'woocommerce_api_edit_product_shipping_class_data', $data, $this ); - $shipping_class = $this->get_product_shipping_class( $id ); - - if ( is_wp_error( $shipping_class ) ) { - return $shipping_class; - } - - $update = wp_update_term( $id, 'product_shipping_class', $data ); - if ( is_wp_error( $update ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_shipping_class', __( 'Could not edit the shipping class', 'woocommerce' ), 400 ); - } - - do_action( 'woocommerce_api_edit_product_shipping_class', $id, $data ); - - return $this->get_product_shipping_class( $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete a product shipping class. - * - * @since 2.5.0 - * @param int $id Product shipping class term ID - * @return array|WP_Error Success message if succeed, otherwise WP_Error - * will be returned - */ - public function delete_product_shipping_class( $id ) { - global $wpdb; - - try { - // Check permissions - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_shipping_class', __( 'You do not have permission to delete product shipping classes', 'woocommerce' ), 401 ); - } - - $id = absint( $id ); - $deleted = wp_delete_term( $id, 'product_shipping_class' ); - if ( ! $deleted || is_wp_error( $deleted ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_shipping_class', __( 'Could not delete the shipping class', 'woocommerce' ), 401 ); - } - - do_action( 'woocommerce_api_delete_product_shipping_class', $id, $this ); - - return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_shipping_class' ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } -} diff --git a/includes/legacy/api/v3/class-wc-api-reports.php b/includes/legacy/api/v3/class-wc-api-reports.php deleted file mode 100644 index 552fd253fda..00000000000 --- a/includes/legacy/api/v3/class-wc-api-reports.php +++ /dev/null @@ -1,330 +0,0 @@ -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 ) - ); - } -} diff --git a/includes/legacy/api/v3/class-wc-api-resource.php b/includes/legacy/api/v3/class-wc-api-resource.php deleted file mode 100644 index 4aface69c0c..00000000000 --- a/includes/legacy/api/v3/class-wc-api-resource.php +++ /dev/null @@ -1,471 +0,0 @@ -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 - * `_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 ); - } -} diff --git a/includes/legacy/api/v3/class-wc-api-server.php b/includes/legacy/api/v3/class-wc-api-server.php deleted file mode 100644 index 9c22f6ef6fa..00000000000 --- a/includes/legacy/api/v3/class-wc-api-server.php +++ /dev/null @@ -1,777 +0,0 @@ - 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; - } -} diff --git a/includes/legacy/api/v3/class-wc-api-taxes.php b/includes/legacy/api/v3/class-wc-api-taxes.php deleted file mode 100644 index 5fafe782e44..00000000000 --- a/includes/legacy/api/v3/class-wc-api-taxes.php +++ /dev/null @@ -1,691 +0,0 @@ - - * - * @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/ - $routes[ $this->base . '/(?P\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/ - $routes[ $this->base . '/classes/(?P\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() ) ); - } - } -} diff --git a/includes/legacy/api/v3/class-wc-api-webhooks.php b/includes/legacy/api/v3/class-wc-api-webhooks.php deleted file mode 100644 index 83121936eaf..00000000000 --- a/includes/legacy/api/v3/class-wc-api-webhooks.php +++ /dev/null @@ -1,509 +0,0 @@ -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/ - $routes[ $this->base . '/(?P\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//deliveries - $routes[ $this->base . '/(?P\d+)/deliveries' ] = array( - array( array( $this, 'get_webhook_deliveries' ), WC_API_Server::READABLE ), - ); - - # GET /webhooks//deliveries/ - $routes[ $this->base . '/(?P\d+)/deliveries/(?P\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; - } -} diff --git a/includes/legacy/api/v3/interface-wc-api-handler.php b/includes/legacy/api/v3/interface-wc-api-handler.php deleted file mode 100644 index 484f9f57f02..00000000000 --- a/includes/legacy/api/v3/interface-wc-api-handler.php +++ /dev/null @@ -1,47 +0,0 @@ - Date: Wed, 26 Jun 2019 11:47:08 +0100 Subject: [PATCH 5/5] git mv legacy api --- .../api}/class-wc-rest-legacy-coupons-controller.php | 0 .../api}/class-wc-rest-legacy-orders-controller.php | 0 .../api}/class-wc-rest-legacy-products-controller.php | 0 .../{api/legacy => legacy/api}/v1/class-wc-api-authentication.php | 0 includes/{api/legacy => legacy/api}/v1/class-wc-api-coupons.php | 0 includes/{api/legacy => legacy/api}/v1/class-wc-api-customers.php | 0 .../{api/legacy => legacy/api}/v1/class-wc-api-json-handler.php | 0 includes/{api/legacy => legacy/api}/v1/class-wc-api-orders.php | 0 includes/{api/legacy => legacy/api}/v1/class-wc-api-products.php | 0 includes/{api/legacy => legacy/api}/v1/class-wc-api-reports.php | 0 includes/{api/legacy => legacy/api}/v1/class-wc-api-resource.php | 0 includes/{api/legacy => legacy/api}/v1/class-wc-api-server.php | 0 .../{api/legacy => legacy/api}/v1/class-wc-api-xml-handler.php | 0 .../{api/legacy => legacy/api}/v1/interface-wc-api-handler.php | 0 .../{api/legacy => legacy/api}/v2/class-wc-api-authentication.php | 0 includes/{api/legacy => legacy/api}/v2/class-wc-api-coupons.php | 0 includes/{api/legacy => legacy/api}/v2/class-wc-api-customers.php | 0 includes/{api/legacy => legacy/api}/v2/class-wc-api-exception.php | 0 .../{api/legacy => legacy/api}/v2/class-wc-api-json-handler.php | 0 includes/{api/legacy => legacy/api}/v2/class-wc-api-orders.php | 0 includes/{api/legacy => legacy/api}/v2/class-wc-api-products.php | 0 includes/{api/legacy => legacy/api}/v2/class-wc-api-reports.php | 0 includes/{api/legacy => legacy/api}/v2/class-wc-api-resource.php | 0 includes/{api/legacy => legacy/api}/v2/class-wc-api-server.php | 0 includes/{api/legacy => legacy/api}/v2/class-wc-api-webhooks.php | 0 .../{api/legacy => legacy/api}/v2/interface-wc-api-handler.php | 0 .../{api/legacy => legacy/api}/v3/class-wc-api-authentication.php | 0 includes/{api/legacy => legacy/api}/v3/class-wc-api-coupons.php | 0 includes/{api/legacy => legacy/api}/v3/class-wc-api-customers.php | 0 includes/{api/legacy => legacy/api}/v3/class-wc-api-exception.php | 0 .../{api/legacy => legacy/api}/v3/class-wc-api-json-handler.php | 0 includes/{api/legacy => legacy/api}/v3/class-wc-api-orders.php | 0 includes/{api/legacy => legacy/api}/v3/class-wc-api-products.php | 0 includes/{api/legacy => legacy/api}/v3/class-wc-api-reports.php | 0 includes/{api/legacy => legacy/api}/v3/class-wc-api-resource.php | 0 includes/{api/legacy => legacy/api}/v3/class-wc-api-server.php | 0 includes/{api/legacy => legacy/api}/v3/class-wc-api-taxes.php | 0 includes/{api/legacy => legacy/api}/v3/class-wc-api-webhooks.php | 0 .../{api/legacy => legacy/api}/v3/interface-wc-api-handler.php | 0 39 files changed, 0 insertions(+), 0 deletions(-) rename includes/{api/legacy => legacy/api}/class-wc-rest-legacy-coupons-controller.php (100%) rename includes/{api/legacy => legacy/api}/class-wc-rest-legacy-orders-controller.php (100%) rename includes/{api/legacy => legacy/api}/class-wc-rest-legacy-products-controller.php (100%) rename includes/{api/legacy => legacy/api}/v1/class-wc-api-authentication.php (100%) rename includes/{api/legacy => legacy/api}/v1/class-wc-api-coupons.php (100%) rename includes/{api/legacy => legacy/api}/v1/class-wc-api-customers.php (100%) rename includes/{api/legacy => legacy/api}/v1/class-wc-api-json-handler.php (100%) rename includes/{api/legacy => legacy/api}/v1/class-wc-api-orders.php (100%) rename includes/{api/legacy => legacy/api}/v1/class-wc-api-products.php (100%) rename includes/{api/legacy => legacy/api}/v1/class-wc-api-reports.php (100%) rename includes/{api/legacy => legacy/api}/v1/class-wc-api-resource.php (100%) rename includes/{api/legacy => legacy/api}/v1/class-wc-api-server.php (100%) rename includes/{api/legacy => legacy/api}/v1/class-wc-api-xml-handler.php (100%) rename includes/{api/legacy => legacy/api}/v1/interface-wc-api-handler.php (100%) rename includes/{api/legacy => legacy/api}/v2/class-wc-api-authentication.php (100%) rename includes/{api/legacy => legacy/api}/v2/class-wc-api-coupons.php (100%) rename includes/{api/legacy => legacy/api}/v2/class-wc-api-customers.php (100%) rename includes/{api/legacy => legacy/api}/v2/class-wc-api-exception.php (100%) rename includes/{api/legacy => legacy/api}/v2/class-wc-api-json-handler.php (100%) rename includes/{api/legacy => legacy/api}/v2/class-wc-api-orders.php (100%) rename includes/{api/legacy => legacy/api}/v2/class-wc-api-products.php (100%) rename includes/{api/legacy => legacy/api}/v2/class-wc-api-reports.php (100%) rename includes/{api/legacy => legacy/api}/v2/class-wc-api-resource.php (100%) rename includes/{api/legacy => legacy/api}/v2/class-wc-api-server.php (100%) rename includes/{api/legacy => legacy/api}/v2/class-wc-api-webhooks.php (100%) rename includes/{api/legacy => legacy/api}/v2/interface-wc-api-handler.php (100%) rename includes/{api/legacy => legacy/api}/v3/class-wc-api-authentication.php (100%) rename includes/{api/legacy => legacy/api}/v3/class-wc-api-coupons.php (100%) rename includes/{api/legacy => legacy/api}/v3/class-wc-api-customers.php (100%) rename includes/{api/legacy => legacy/api}/v3/class-wc-api-exception.php (100%) rename includes/{api/legacy => legacy/api}/v3/class-wc-api-json-handler.php (100%) rename includes/{api/legacy => legacy/api}/v3/class-wc-api-orders.php (100%) rename includes/{api/legacy => legacy/api}/v3/class-wc-api-products.php (100%) rename includes/{api/legacy => legacy/api}/v3/class-wc-api-reports.php (100%) rename includes/{api/legacy => legacy/api}/v3/class-wc-api-resource.php (100%) rename includes/{api/legacy => legacy/api}/v3/class-wc-api-server.php (100%) rename includes/{api/legacy => legacy/api}/v3/class-wc-api-taxes.php (100%) rename includes/{api/legacy => legacy/api}/v3/class-wc-api-webhooks.php (100%) rename includes/{api/legacy => legacy/api}/v3/interface-wc-api-handler.php (100%) diff --git a/includes/api/legacy/class-wc-rest-legacy-coupons-controller.php b/includes/legacy/api/class-wc-rest-legacy-coupons-controller.php similarity index 100% rename from includes/api/legacy/class-wc-rest-legacy-coupons-controller.php rename to includes/legacy/api/class-wc-rest-legacy-coupons-controller.php diff --git a/includes/api/legacy/class-wc-rest-legacy-orders-controller.php b/includes/legacy/api/class-wc-rest-legacy-orders-controller.php similarity index 100% rename from includes/api/legacy/class-wc-rest-legacy-orders-controller.php rename to includes/legacy/api/class-wc-rest-legacy-orders-controller.php diff --git a/includes/api/legacy/class-wc-rest-legacy-products-controller.php b/includes/legacy/api/class-wc-rest-legacy-products-controller.php similarity index 100% rename from includes/api/legacy/class-wc-rest-legacy-products-controller.php rename to includes/legacy/api/class-wc-rest-legacy-products-controller.php diff --git a/includes/api/legacy/v1/class-wc-api-authentication.php b/includes/legacy/api/v1/class-wc-api-authentication.php similarity index 100% rename from includes/api/legacy/v1/class-wc-api-authentication.php rename to includes/legacy/api/v1/class-wc-api-authentication.php diff --git a/includes/api/legacy/v1/class-wc-api-coupons.php b/includes/legacy/api/v1/class-wc-api-coupons.php similarity index 100% rename from includes/api/legacy/v1/class-wc-api-coupons.php rename to includes/legacy/api/v1/class-wc-api-coupons.php diff --git a/includes/api/legacy/v1/class-wc-api-customers.php b/includes/legacy/api/v1/class-wc-api-customers.php similarity index 100% rename from includes/api/legacy/v1/class-wc-api-customers.php rename to includes/legacy/api/v1/class-wc-api-customers.php diff --git a/includes/api/legacy/v1/class-wc-api-json-handler.php b/includes/legacy/api/v1/class-wc-api-json-handler.php similarity index 100% rename from includes/api/legacy/v1/class-wc-api-json-handler.php rename to includes/legacy/api/v1/class-wc-api-json-handler.php diff --git a/includes/api/legacy/v1/class-wc-api-orders.php b/includes/legacy/api/v1/class-wc-api-orders.php similarity index 100% rename from includes/api/legacy/v1/class-wc-api-orders.php rename to includes/legacy/api/v1/class-wc-api-orders.php diff --git a/includes/api/legacy/v1/class-wc-api-products.php b/includes/legacy/api/v1/class-wc-api-products.php similarity index 100% rename from includes/api/legacy/v1/class-wc-api-products.php rename to includes/legacy/api/v1/class-wc-api-products.php diff --git a/includes/api/legacy/v1/class-wc-api-reports.php b/includes/legacy/api/v1/class-wc-api-reports.php similarity index 100% rename from includes/api/legacy/v1/class-wc-api-reports.php rename to includes/legacy/api/v1/class-wc-api-reports.php diff --git a/includes/api/legacy/v1/class-wc-api-resource.php b/includes/legacy/api/v1/class-wc-api-resource.php similarity index 100% rename from includes/api/legacy/v1/class-wc-api-resource.php rename to includes/legacy/api/v1/class-wc-api-resource.php diff --git a/includes/api/legacy/v1/class-wc-api-server.php b/includes/legacy/api/v1/class-wc-api-server.php similarity index 100% rename from includes/api/legacy/v1/class-wc-api-server.php rename to includes/legacy/api/v1/class-wc-api-server.php diff --git a/includes/api/legacy/v1/class-wc-api-xml-handler.php b/includes/legacy/api/v1/class-wc-api-xml-handler.php similarity index 100% rename from includes/api/legacy/v1/class-wc-api-xml-handler.php rename to includes/legacy/api/v1/class-wc-api-xml-handler.php diff --git a/includes/api/legacy/v1/interface-wc-api-handler.php b/includes/legacy/api/v1/interface-wc-api-handler.php similarity index 100% rename from includes/api/legacy/v1/interface-wc-api-handler.php rename to includes/legacy/api/v1/interface-wc-api-handler.php diff --git a/includes/api/legacy/v2/class-wc-api-authentication.php b/includes/legacy/api/v2/class-wc-api-authentication.php similarity index 100% rename from includes/api/legacy/v2/class-wc-api-authentication.php rename to includes/legacy/api/v2/class-wc-api-authentication.php diff --git a/includes/api/legacy/v2/class-wc-api-coupons.php b/includes/legacy/api/v2/class-wc-api-coupons.php similarity index 100% rename from includes/api/legacy/v2/class-wc-api-coupons.php rename to includes/legacy/api/v2/class-wc-api-coupons.php diff --git a/includes/api/legacy/v2/class-wc-api-customers.php b/includes/legacy/api/v2/class-wc-api-customers.php similarity index 100% rename from includes/api/legacy/v2/class-wc-api-customers.php rename to includes/legacy/api/v2/class-wc-api-customers.php diff --git a/includes/api/legacy/v2/class-wc-api-exception.php b/includes/legacy/api/v2/class-wc-api-exception.php similarity index 100% rename from includes/api/legacy/v2/class-wc-api-exception.php rename to includes/legacy/api/v2/class-wc-api-exception.php diff --git a/includes/api/legacy/v2/class-wc-api-json-handler.php b/includes/legacy/api/v2/class-wc-api-json-handler.php similarity index 100% rename from includes/api/legacy/v2/class-wc-api-json-handler.php rename to includes/legacy/api/v2/class-wc-api-json-handler.php diff --git a/includes/api/legacy/v2/class-wc-api-orders.php b/includes/legacy/api/v2/class-wc-api-orders.php similarity index 100% rename from includes/api/legacy/v2/class-wc-api-orders.php rename to includes/legacy/api/v2/class-wc-api-orders.php diff --git a/includes/api/legacy/v2/class-wc-api-products.php b/includes/legacy/api/v2/class-wc-api-products.php similarity index 100% rename from includes/api/legacy/v2/class-wc-api-products.php rename to includes/legacy/api/v2/class-wc-api-products.php diff --git a/includes/api/legacy/v2/class-wc-api-reports.php b/includes/legacy/api/v2/class-wc-api-reports.php similarity index 100% rename from includes/api/legacy/v2/class-wc-api-reports.php rename to includes/legacy/api/v2/class-wc-api-reports.php diff --git a/includes/api/legacy/v2/class-wc-api-resource.php b/includes/legacy/api/v2/class-wc-api-resource.php similarity index 100% rename from includes/api/legacy/v2/class-wc-api-resource.php rename to includes/legacy/api/v2/class-wc-api-resource.php diff --git a/includes/api/legacy/v2/class-wc-api-server.php b/includes/legacy/api/v2/class-wc-api-server.php similarity index 100% rename from includes/api/legacy/v2/class-wc-api-server.php rename to includes/legacy/api/v2/class-wc-api-server.php diff --git a/includes/api/legacy/v2/class-wc-api-webhooks.php b/includes/legacy/api/v2/class-wc-api-webhooks.php similarity index 100% rename from includes/api/legacy/v2/class-wc-api-webhooks.php rename to includes/legacy/api/v2/class-wc-api-webhooks.php diff --git a/includes/api/legacy/v2/interface-wc-api-handler.php b/includes/legacy/api/v2/interface-wc-api-handler.php similarity index 100% rename from includes/api/legacy/v2/interface-wc-api-handler.php rename to includes/legacy/api/v2/interface-wc-api-handler.php diff --git a/includes/api/legacy/v3/class-wc-api-authentication.php b/includes/legacy/api/v3/class-wc-api-authentication.php similarity index 100% rename from includes/api/legacy/v3/class-wc-api-authentication.php rename to includes/legacy/api/v3/class-wc-api-authentication.php diff --git a/includes/api/legacy/v3/class-wc-api-coupons.php b/includes/legacy/api/v3/class-wc-api-coupons.php similarity index 100% rename from includes/api/legacy/v3/class-wc-api-coupons.php rename to includes/legacy/api/v3/class-wc-api-coupons.php diff --git a/includes/api/legacy/v3/class-wc-api-customers.php b/includes/legacy/api/v3/class-wc-api-customers.php similarity index 100% rename from includes/api/legacy/v3/class-wc-api-customers.php rename to includes/legacy/api/v3/class-wc-api-customers.php diff --git a/includes/api/legacy/v3/class-wc-api-exception.php b/includes/legacy/api/v3/class-wc-api-exception.php similarity index 100% rename from includes/api/legacy/v3/class-wc-api-exception.php rename to includes/legacy/api/v3/class-wc-api-exception.php diff --git a/includes/api/legacy/v3/class-wc-api-json-handler.php b/includes/legacy/api/v3/class-wc-api-json-handler.php similarity index 100% rename from includes/api/legacy/v3/class-wc-api-json-handler.php rename to includes/legacy/api/v3/class-wc-api-json-handler.php diff --git a/includes/api/legacy/v3/class-wc-api-orders.php b/includes/legacy/api/v3/class-wc-api-orders.php similarity index 100% rename from includes/api/legacy/v3/class-wc-api-orders.php rename to includes/legacy/api/v3/class-wc-api-orders.php diff --git a/includes/api/legacy/v3/class-wc-api-products.php b/includes/legacy/api/v3/class-wc-api-products.php similarity index 100% rename from includes/api/legacy/v3/class-wc-api-products.php rename to includes/legacy/api/v3/class-wc-api-products.php diff --git a/includes/api/legacy/v3/class-wc-api-reports.php b/includes/legacy/api/v3/class-wc-api-reports.php similarity index 100% rename from includes/api/legacy/v3/class-wc-api-reports.php rename to includes/legacy/api/v3/class-wc-api-reports.php diff --git a/includes/api/legacy/v3/class-wc-api-resource.php b/includes/legacy/api/v3/class-wc-api-resource.php similarity index 100% rename from includes/api/legacy/v3/class-wc-api-resource.php rename to includes/legacy/api/v3/class-wc-api-resource.php diff --git a/includes/api/legacy/v3/class-wc-api-server.php b/includes/legacy/api/v3/class-wc-api-server.php similarity index 100% rename from includes/api/legacy/v3/class-wc-api-server.php rename to includes/legacy/api/v3/class-wc-api-server.php diff --git a/includes/api/legacy/v3/class-wc-api-taxes.php b/includes/legacy/api/v3/class-wc-api-taxes.php similarity index 100% rename from includes/api/legacy/v3/class-wc-api-taxes.php rename to includes/legacy/api/v3/class-wc-api-taxes.php diff --git a/includes/api/legacy/v3/class-wc-api-webhooks.php b/includes/legacy/api/v3/class-wc-api-webhooks.php similarity index 100% rename from includes/api/legacy/v3/class-wc-api-webhooks.php rename to includes/legacy/api/v3/class-wc-api-webhooks.php diff --git a/includes/api/legacy/v3/interface-wc-api-handler.php b/includes/legacy/api/v3/interface-wc-api-handler.php similarity index 100% rename from includes/api/legacy/v3/interface-wc-api-handler.php rename to includes/legacy/api/v3/interface-wc-api-handler.php