diff --git a/includes/admin/class-wc-admin-profile.php b/includes/admin/class-wc-admin-profile.php index 37850320b8b..ebeb709c8a5 100644 --- a/includes/admin/class-wc-admin-profile.php +++ b/includes/admin/class-wc-admin-profile.php @@ -26,6 +26,12 @@ class WC_Admin_Profile { add_action( 'personal_options_update', array( $this, 'save_customer_meta_fields' ) ); add_action( 'edit_user_profile_update', array( $this, 'save_customer_meta_fields' ) ); + + add_action( 'show_user_profile', array( $this, 'add_api_key_field' ) ); + add_action( 'edit_user_profile', array( $this, 'add_api_key_field' ) ); + + add_action( 'personal_options_update', array( $this, 'generate_api_key' ) ); + add_action( 'edit_user_profile_update', array( $this, 'generate_api_key' ) ); } /** @@ -176,8 +182,116 @@ class WC_Admin_Profile { update_user_meta( $user_id, $key, woocommerce_clean( $_POST[ $key ] ) ); } + /** + * Display the API key info for a user + * + * @since 2.1 + * @param WP_User $user + */ + public function add_api_key_field( $user ) { + + if ( ! current_user_can( 'manage_woocommerce' ) ) + return; + + $permissions = array( + 'read' => __( 'Read', 'woocommerce' ), + 'write' => __( 'Write', 'woocommerce' ), + 'read_write' => __( 'Read/Write', 'woocommerce' ), + ); + + if ( current_user_can( 'edit_user', $user->ID ) ) { + ?> +
+ |
+ woocommerce_api_consumer_key ) ) : ?>
+
+
+
+ woocommerce_api_consumer_key ?> + woocommerce_api_consumer_secret; ?> + + + + + |
+
---|
, 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[] = $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 ) {
+ global $wpdb;
+
+ $id = $this->validate_request( $id, 'shop_coupon', 'read' );
+
+ if ( is_wp_error( $id ) )
+ return $id;
+
+ // get the coupon code
+ $code = $wpdb->get_var( $wpdb->prepare( "SELECT post_title FROM $wpdb->posts WHERE id = %s AND post_type = 'shop_coupon' AND post_status = 'publish'", $id ) );
+
+ if ( is_null( $code ) )
+ return new WP_Error( 'woocommerce_api_invalid_coupon_id', __( 'Invalid coupon ID', 'woocommerce' ), array( 'status' => 404 ) );
+
+ $coupon = new WC_Coupon( $code );
+
+ $coupon_post = get_post( $coupon->id );
+
+ $coupon_data = array(
+ 'id' => $coupon->id,
+ 'code' => $coupon->code,
+ 'type' => $coupon->type,
+ 'created_at' => $this->server->format_datetime( $coupon_post->post_date_gmt ),
+ 'updated_at' => $this->server->format_datetime( $coupon_post->post_modified_gmt ),
+ 'amount' => woocommerce_format_decimal( $coupon->amount ),
+ 'individual_use' => $coupon->individual_use,
+ 'product_ids' => $coupon->product_ids,
+ 'exclude_product_ids' => $coupon->exclude_product_ids,
+ 'usage_limit' => $coupon->usage_limit,
+ 'usage_limit_per_user' => $coupon->usage_limit_per_user,
+ 'limit_usage_to_x_items' => $coupon->limit_usage_to_x_items,
+ 'usage_count' => $coupon->usage_count,
+ 'expiry_date' => $this->server->format_datetime( $coupon->expiry_date ),
+ 'apply_before_tax' => $coupon->apply_before_tax(),
+ 'enable_free_shipping' => $coupon->enable_free_shipping(),
+ 'product_categories' => $coupon->product_categories,
+ 'exclude_product_categories' => $coupon->exclude_product_categories,
+ 'exclude_sale_items' => $coupon->exclude_sale_items(),
+ 'minimum_amount' => $coupon->minimum_amount,
+ 'customer_email' => $coupon->customer_email,
+ );
+
+ return 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
+ */
+ public function get_coupons_count( $filter = array() ) {
+
+ $query = $this->query_coupons( $filter );
+
+ // TODO: permissions?
+
+ return array( 'count' => $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'", $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
+ *
+ * @since 2.1
+ * @param array $data
+ * @return array
+ */
+ public function create_coupon( $data ) {
+
+ // TODO: permissions check
+
+ // TODO: implement - what's the minimum set of data required?
+
+ return array();
+ }
+
+ /**
+ * Edit a coupon
+ *
+ * @since 2.1
+ * @param int $id the coupon ID
+ * @param array $data
+ * @return array
+ */
+ public function edit_coupon( $id, $data ) {
+
+ $id = $this->validate_request( $id, 'shop_coupon', 'edit' );
+
+ if ( is_wp_error( $id ) )
+ return $id;
+
+ // TODO: implement
+
+ return $this->get_coupon( $id );
+ }
+
+ /**
+ * Delete a coupon
+ *
+ * @since 2.1
+ * @param int $id the coupon ID
+ * @param bool $force true to permanently delete coupon, false to move to trash
+ * @return array
+ */
+ 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/api/class-wc-api-customers.php b/includes/api/class-wc-api-customers.php
new file mode 100644
index 00000000000..fdd6d64e6e0
--- /dev/null
+++ b/includes/api/class-wc-api-customers.php
@@ -0,0 +1,475 @@
+
+ * GET /customers//orders
+ *
+ * @since 2.1
+ * @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//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[] = $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
+ */
+ 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 WP_User( $id );
+
+ // get info about user's last order
+ $last_order = $wpdb->get_row( "SELECT id, post_date_gmt
+ FROM $wpdb->posts AS posts
+ LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id
+ WHERE meta.meta_key = '_customer_user'
+ AND meta.meta_value = {$customer->ID}
+ AND posts.post_type = 'shop_order'
+ AND posts.post_status = 'publish'
+ " );
+
+ $customer_data = array(
+ 'id' => $customer->ID,
+ 'created_at' => $this->server->format_datetime( $customer->user_registered ),
+ 'email' => $customer->user_email,
+ 'first_name' => $customer->first_name,
+ 'last_name' => $customer->last_name,
+ 'username' => $customer->user_login,
+ 'last_order_id' => is_object( $last_order ) ? $last_order->id : null,
+ 'last_order_date' => is_object( $last_order ) ? $this->server->format_datetime( $last_order->post_date_gmt ) : null,
+ 'orders_count' => (int) $customer->_order_count,
+ 'total_spent' => woocommerce_format_decimal( $customer->_money_spent ),
+ 'avatar_url' => $this->get_avatar_url( $customer->customer_email ),
+ 'billing_address' => array(
+ 'first_name' => $customer->billing_first_name,
+ 'last_name' => $customer->billing_last_name,
+ 'company' => $customer->billing_company,
+ 'address_1' => $customer->billing_address_1,
+ 'address_2' => $customer->billing_address_2,
+ 'city' => $customer->billing_city,
+ 'state' => $customer->billing_state,
+ 'postcode' => $customer->billing_postcode,
+ 'country' => $customer->billing_country,
+ 'email' => $customer->billing_email,
+ 'phone' => $customer->billing_phone,
+ ),
+ 'shipping_address' => array(
+ 'first_name' => $customer->shipping_first_name,
+ 'last_name' => $customer->shipping_last_name,
+ 'company' => $customer->shipping_company,
+ 'address_1' => $customer->shipping_address_1,
+ 'address_2' => $customer->shipping_address_2,
+ 'city' => $customer->shipping_city,
+ 'state' => $customer->shipping_state,
+ 'postcode' => $customer->shipping_postcode,
+ 'country' => $customer->shipping_country,
+ ),
+ );
+
+ return 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
+ */
+ 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_customer', __( 'You do not have permission to read customers', 'woocommerce' ), array( 'status' => 401 ) );
+
+ return array( 'count' => count( $query->get_results() ) );
+ }
+
+
+ /**
+ * Create a customer
+ *
+ * @since 2.1
+ * @param array $data
+ * @return array
+ */
+ 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 ) );
+
+ // TODO: implement - woocommerce_create_new_customer()
+
+ return array();
+ }
+
+ /**
+ * Edit a customer
+ *
+ * @since 2.1
+ * @param int $id the customer ID
+ * @param array $data
+ * @return array
+ */
+ public function edit_customer( $id, $data ) {
+
+ $id = $this->validate_request( $id, 'customer', 'edit' );
+
+ if ( ! is_wp_error( $id ) )
+ return $id;
+
+ // TODO: implement
+
+ return $this->get_customer( $id );
+ }
+
+ /**
+ * Delete a customer
+ *
+ * @since 2.1
+ * @param int $id the customer ID
+ * @return array
+ */
+ 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
+ */
+ 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 = $wpdb->get_col( $wpdb->prepare( "SELECT id
+ FROM $wpdb->posts AS posts
+ LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id
+ WHERE meta.meta_key = '_customer_user'
+ AND meta.meta_value = '%s'
+ AND posts.post_type = 'shop_order'
+ AND posts.post_status = 'publish'
+ ", $id ) );
+
+ if ( empty( $order_ids ) )
+ return array( 'orders' => array() );
+
+ $orders = array();
+
+ foreach ( $order_ids as $order_id ) {
+ $orders[] = 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 array
+ */
+ 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 = absint( $args['page'] );
+
+ // 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->customer_user ) {
+
+ $order_data['customer'] = 'guest';
+
+ } else {
+
+ $order_data['customer'] = $this->get_customer( $order->customer_user );
+ }
+
+ 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 ) );
+ }
+
+ /**
+ * Wrapper for @see get_avatar() which doesn't simply return
+ * the URL so we need to pluck it from the HTML img tag
+ *
+ * @since 2.1
+ * @param string $email the customer's email
+ * @return string the URL to the customer's avatar
+ */
+ private function get_avatar_url( $email ) {
+
+ $dom = new DOMDocument();
+
+ $dom->loadHTML( get_avatar( $email ) );
+
+ $url = $dom->getElementsByTagName( 'img' )->item( 0 )->getAttribute( 'src' );
+
+ return ( ! empty( $url ) ) ? $url : null;
+ }
+
+ /**
+ * 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/api/class-wc-api-json-handler.php b/includes/api/class-wc-api-json-handler.php
new file mode 100644
index 00000000000..c08ebd4789a
--- /dev/null
+++ b/includes/api/class-wc-api-json-handler.php
@@ -0,0 +1,73 @@
+api->server->send_status( 400 );
+
+ $data = array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) );
+ }
+
+ // Check for invalid characters (only alphanumeric allowed)
+ if ( preg_match( '/\W/', $_GET['_jsonp'] ) ) {
+
+ WC()->api->server->send_status( 400 );
+
+ $data = array( array( 'code' => 'woocommerce_api_json_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) );
+ }
+
+ return $_GET['_jsonp'] . '(' . json_encode( $data ) . ')';
+ }
+
+ return json_encode( $data );
+ }
+
+}
diff --git a/includes/api/class-wc-api-orders.php b/includes/api/class-wc-api-orders.php
new file mode 100644
index 00000000000..7650fd83be9
--- /dev/null
+++ b/includes/api/class-wc-api-orders.php
@@ -0,0 +1,374 @@
+
+ * 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|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 /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[] = $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
+ */
+ 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 = new WC_Order( $id );
+
+ $order_post = get_post( $id );
+
+ $order_data = array(
+ 'id' => $order->id,
+ 'order_number' => $order->get_order_number(),
+ 'created_at' => $this->server->format_datetime( $order_post->post_date_gmt ),
+ 'updated_at' => $this->server->format_datetime( $order_post->post_modified_gmt ),
+ 'completed_at' => $this->server->format_datetime( $order->completed_date, true ),
+ 'status' => $order->status,
+ 'currency' => $order->order_currency,
+ 'total' => woocommerce_format_decimal( $order->get_total() ),
+ 'total_line_items_quantity' => $order->get_item_count(),
+ 'total_tax' => woocommerce_format_decimal( $order->get_total_tax() ),
+ 'total_shipping' => woocommerce_format_decimal( $order->get_total_shipping() ),
+ 'cart_tax' => woocommerce_format_decimal( $order->get_cart_tax() ),
+ 'shipping_tax' => woocommerce_format_decimal( $order->get_shipping_tax() ),
+ 'total_discount' => woocommerce_format_decimal( $order->get_total_discount() ),
+ 'cart_discount' => woocommerce_format_decimal( $order->get_cart_discount() ),
+ 'order_discount' => woocommerce_format_decimal( $order->get_order_discount() ),
+ 'shipping_methods' => $order->get_shipping_method(),
+ 'payment_details' => array(
+ 'method_id' => $order->payment_method,
+ 'method_title' => $order->payment_method_title,
+ 'paid' => isset( $order->paid_date ),
+ ),
+ 'billing_address' => array(
+ 'first_name' => $order->billing_first_name,
+ 'last_name' => $order->billing_last_name,
+ 'company' => $order->billing_company,
+ 'address_1' => $order->billing_address_1,
+ 'address_2' => $order->billing_address_2,
+ 'city' => $order->billing_city,
+ 'state' => $order->billing_state,
+ 'postcode' => $order->billing_postcode,
+ 'country' => $order->billing_country,
+ 'email' => $order->billing_email,
+ 'phone' => $order->billing_phone,
+ ),
+ 'shipping_address' => array(
+ 'first_name' => $order->shipping_first_name,
+ 'last_name' => $order->shipping_last_name,
+ 'company' => $order->shipping_company,
+ 'address_1' => $order->shipping_address_1,
+ 'address_2' => $order->shipping_address_2,
+ 'city' => $order->shipping_city,
+ 'state' => $order->shipping_state,
+ 'postcode' => $order->shipping_postcode,
+ 'country' => $order->shipping_country,
+ ),
+ 'note' => $order->customer_note,
+ 'customer_ip' => $order->customer_ip_address,
+ 'customer_user_agent' => $order->customer_user_agent,
+ 'customer_id' => $order->customer_user,
+ '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 = $order->get_product_from_item( $item );
+
+ $order_data['line_items'][] = array(
+ 'id' => $item_id,
+ 'subtotal' => woocommerce_format_decimal( $order->get_line_subtotal( $item ) ),
+ 'total' => woocommerce_format_decimal( $order->get_line_total( $item ) ),
+ 'total_tax' => woocommerce_format_decimal( $order->get_line_tax( $item ) ),
+ 'quantity' => (int) $item['qty'],
+ 'tax_class' => ( ! empty( $item['tax_class'] ) ) ? $item['tax_class'] : null,
+ 'name' => $item['name'],
+ 'product_id' => ( isset( $product->variation_id ) ) ? $product->variation_id : $product->id,
+ 'sku' => $product->get_sku(),
+ );
+ }
+
+ // 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['method_id'],
+ 'method_title' => $shipping_item['name'],
+ 'total' => woocommerce_format_decimal( $shipping_item['cost'] ),
+ );
+ }
+
+ // add taxes
+ foreach ( $order->get_tax_totals() as $tax_code => $tax ) {
+
+ $order_data['tax_lines'][] = array(
+ 'code' => $tax_code,
+ 'title' => $tax->label,
+ 'total' => woocommerce_format_decimal( $tax->amount ),
+ '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['name'],
+ 'tax_class' => ( ! empty( $fee_item['tax_class'] ) ) ? $fee_item['tax_class'] : null,
+ 'total' => woocommerce_format_decimal( $order->get_line_total( $fee_item ) ),
+ 'total_tax' => woocommerce_format_decimal( $order->get_line_tax( $fee_item ) ),
+ );
+ }
+
+ // 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['name'],
+ 'amount' => woocommerce_format_decimal( $coupon_item['discount_amount'] ),
+ );
+ }
+
+ return 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
+ */
+ public function get_orders_count( $status = null, $filter = array() ) {
+
+ if ( ! empty( $status ) )
+ $filter['status'] = $status;
+
+ $query = $this->query_orders( $filter );
+
+ // TODO: permissions?
+
+ return array( 'count' => $query->found_posts );
+ }
+
+
+ /**
+ * Edit an order
+ *
+ * @since 2.1
+ * @param int $id the order ID
+ * @param array $data
+ * @return array
+ */
+ public function edit_order( $id, $data ) {
+
+ $id = $this->validate_request( $id, 'shop_order', 'write' );
+
+ if ( is_wp_error( $id ) )
+ return $id;
+
+ // TODO: implement, especially for status change
+
+ return $this->get_order( $id );
+ }
+
+ /**
+ * Delete an order
+ *
+ * @since 2.1
+ * @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
+ */
+ public function get_order_notes( $id, $fields = null ) {
+
+ // ensure ID is valid order ID
+ $id = $this->validate_request( $id, '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' => get_comment_meta( $note->comment_ID, 'is_customer_note', true ) ? true : false,
+ );
+ }
+
+ 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' => 'publish',
+ );
+
+ // add status argument
+ if ( ! empty( $args['status'] ) ) {
+
+ $statuses = explode( ',', $args['status'] );
+
+ $query_args['tax_query'] = array(
+ array(
+ 'taxonomy' => 'shop_order_status',
+ 'field' => 'slug',
+ 'terms' => $statuses,
+ ),
+ );
+
+ unset( $args['status'] );
+ }
+
+ $query_args = $this->merge_query_args( $query_args, $args );
+
+ return new WP_Query( $query_args );
+ }
+
+}
diff --git a/includes/api/class-wc-api-products.php b/includes/api/class-wc-api-products.php
new file mode 100644
index 00000000000..aad0bd933fd
--- /dev/null
+++ b/includes/api/class-wc-api-products.php
@@ -0,0 +1,541 @@
+
+ * 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|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 ),
+ );
+
+ 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[] = $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
+ */
+ public function get_product( $id, $fields = null ) {
+
+ $id = $this->validate_request( $id, 'product', 'read' );
+
+ if ( is_wp_error( $id ) )
+ return $id;
+
+ $product = 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->parent );
+ }
+
+ return 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
+ */
+ public function get_products_count( $type = null, $filter = array() ) {
+
+ if ( ! empty( $type ) )
+ $filter['type'] = $type;
+
+ // TODO: permissions?
+
+ $query = $this->query_products( $filter );
+
+ return array( 'count' => $query->found_posts );
+ }
+
+ /**
+ * Edit a product
+ *
+ * @since 2.1
+ * @param int $id the product ID
+ * @param array $data
+ * @return array
+ */
+ public function edit_product( $id, $data ) {
+
+ $id = $this->validate_request( $id, 'product', 'edit' );
+
+ if ( is_wp_error( $id ) )
+ return $id;
+
+ // TODO: implement
+
+ return $this->get_product( $id );
+ }
+
+ /**
+ * Delete a product
+ *
+ * @since 2.1
+ * @param int $id the product ID
+ * @param bool $force true to permanently delete order, false to move to trash
+ * @return array
+ */
+ 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
+ */
+ 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' => (bool) woocommerce_customer_bought_product( $comment->comment_author_email, $comment->user_id, $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',
+ 'post_parent' => 0,
+ '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 $product
+ * @return array
+ */
+ private function get_product_data( $product ) {
+
+ return array(
+ 'title' => $product->get_title(),
+ 'id' => (int) $product->is_type( 'variation' ) ? $product->get_variation_id() : $product->id,
+ 'created_at' => $this->server->format_datetime( $product->get_post_data()->post_date_gmt ),
+ 'updated_at' => $this->server->format_datetime( $product->get_post_data()->post_modified_gmt ),
+ 'type' => $product->product_type,
+ 'status' => $product->get_post_data()->post_status,
+ 'downloadable' => $product->is_downloadable(),
+ 'virtual' => $product->is_virtual(),
+ 'permalink' => $product->get_permalink(),
+ 'sku' => $product->get_sku(),
+ 'price' => woocommerce_format_decimal( $product->get_price() ),
+ 'regular_price' => woocommerce_format_decimal( $product->get_regular_price() ),
+ 'sale_price' => $product->get_sale_price() ? woocommerce_format_decimal( $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' => (int) $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->visibility,
+ 'on_sale' => $product->is_on_sale(),
+ 'weight' => $product->get_weight() ? woocommerce_format_decimal( $product->get_weight() ) : null,
+ 'dimensions' => array(
+ 'length' => $product->length,
+ 'width' => $product->width,
+ 'height' => $product->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_post_data()->post_content ),
+ 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_post_data()->post_excerpt ),
+ 'reviews_allowed' => ( 'open' === $product->get_post_data()->comment_status ),
+ 'average_rating' => woocommerce_format_decimal( $product->get_average_rating() ),
+ 'rating_count' => (int) $product->get_rating_count(),
+ 'related_ids' => array_map( 'absint', array_values( $product->get_related() ) ),
+ 'upsell_ids' => array_map( 'absint', $product->get_upsells() ),
+ 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sells() ),
+ 'categories' => wp_get_post_terms( $product->id, 'product_cat', array( 'fields' => 'names' ) ),
+ 'tags' => wp_get_post_terms( $product->id, 'product_tag', array( 'fields' => 'names' ) ),
+ 'images' => $this->get_images( $product ),
+ 'attributes' => $this->get_attributes( $product ),
+ 'downloads' => $this->get_downloads( $product ),
+ 'download_limit' => (int) $product->download_limit,
+ 'download_expiry' => (int) $product->download_expiry,
+ 'download_type' => $product->download_type,
+ 'purchase_note' => apply_filters( 'the_content', $product->purchase_note ),
+ '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 = $product->get_child( $child_id );
+
+ if ( ! $variation->exists() )
+ continue;
+
+ $variations[] = array(
+ 'id' => $variation->get_variation_id(),
+ 'created_at' => $this->server->format_datetime( $variation->get_post_data()->post_date_gmt ),
+ 'updated_at' => $this->server->format_datetime( $variation->get_post_data()->post_modified_gmt ),
+ 'downloadable' => $variation->is_downloadable(),
+ 'virtual' => $variation->is_virtual(),
+ 'permalink' => $variation->get_permalink(),
+ 'sku' => $variation->get_sku(),
+ 'price' => woocommerce_format_decimal( $variation->get_price() ),
+ 'regular_price' => woocommerce_format_decimal( $variation->get_regular_price() ),
+ 'sale_price' => $variation->get_sale_price() ? woocommerce_format_decimal( $variation->get_sale_price() ) : 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() ? woocommerce_format_decimal( $variation->get_weight() ) : null,
+ 'dimensions' => array(
+ 'length' => $variation->length,
+ 'width' => $variation->width,
+ 'height' => $variation->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->download_limit,
+ 'download_expiry' => (int) $product->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();
+
+ if ( $product->is_type( 'variation' ) ) {
+
+ if ( has_post_thumbnail( $product->get_variation_id() ) ) {
+
+ // add variation image if set
+ $attachment_ids[] = get_post_thumbnail_id( $product->get_variation_id() );
+
+ } elseif ( has_post_thumbnail( $product->id ) ) {
+
+ // otherwise use the parent product featured image if set
+ $attachment_ids[] = get_post_thumbnail_id( $product->id );
+ }
+
+ } else {
+
+ // add featured image
+ if ( has_post_thumbnail( $product->id ) ) {
+ $attachment_ids[] = get_post_thumbnail_id( $product->id );
+ }
+
+ // add gallery images
+ $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_attachment_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' => woocommerce_placeholder_img_src(),
+ 'title' => __( 'Placeholder', 'woocommerce' ),
+ 'alt' => __( 'Placeholder', 'woocommerce' ),
+ 'position' => 0,
+ );
+ }
+
+ return $images;
+ }
+
+ /**
+ * 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_', '', str_replace( 'pa_', '', $attribute_name ) ) ),
+ 'option' => $attribute,
+ );
+ }
+
+ } else {
+
+ foreach ( $product->get_attributes() as $attribute ) {
+
+ // taxonomy-based attributes are comma-separated, others are pipe (|) separated
+ if ( $attribute['is_taxonomy'] )
+ $options = explode( ',', $product->get_attribute( $attribute['name'] ) );
+ else
+ $options = explode( '|', $product->get_attribute( $attribute['name'] ) );
+
+ $attributes[] = array(
+ 'name' => ucwords( str_replace( 'pa_', '', $attribute['name'] ) ),
+ 'position' => $attribute['position'],
+ 'visible' => (bool) $attribute['is_visible'],
+ 'variation' => (bool) $attribute['is_variation'],
+ 'options' => array_map( 'trim', $options ),
+ );
+ }
+ }
+
+ 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_files() 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/api/class-wc-api-reports.php b/includes/api/class-wc-api-reports.php
new file mode 100644
index 00000000000..a1939a9b1bb
--- /dev/null
+++ b/includes/api/class-wc-api-reports.php
@@ -0,0 +1,385 @@
+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 ),
+ );
+
+ return $routes;
+ }
+
+ /**
+ * Get a simple listing of available reports
+ *
+ * @since 2.1
+ * @return array
+ */
+ public function get_reports() {
+
+ return array( 'reports' => array( 'sales' ) );
+ }
+
+ /**
+ * Get the sales report
+ *
+ * @since 2.1
+ * @param string $fields fields to include in response
+ * @param array $filter date filtering
+ * @return array
+ */
+ 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,
+ ) );
+
+ // 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;
+ }
+
+ $period_totals[ $time ] = array(
+ 'sales' => woocommerce_format_decimal( 0.00 ),
+ 'orders' => 0,
+ 'items' => 0,
+ 'tax' => woocommerce_format_decimal( 0.00 ),
+ 'shipping' => woocommerce_format_decimal( 0.00 ),
+ 'discount' => woocommerce_format_decimal( 0.00 ),
+ );
+ }
+
+ // 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'] = woocommerce_format_decimal( $order->total_sales );
+ $period_totals[ $time ]['orders'] = (int) $order->total_orders;
+ $period_totals[ $time ]['tax'] = woocommerce_format_decimal( $order->total_tax + $order->total_shipping_tax );
+ $period_totals[ $time ]['shipping'] = woocommerce_format_decimal( $order->total_shipping );
+ }
+
+ // 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'] = woocommerce_format_decimal( $discount->discount_amount );
+ }
+
+ $sales_data = array(
+ 'sales' => woocommerce_format_decimal( $totals->sales ),
+ 'average' => woocommerce_format_decimal( $totals->sales / ( $this->report->chart_interval + 1 ) ),
+ 'orders' => (int) $totals->order_count,
+ 'items' => $total_items,
+ 'tax' => woocommerce_format_decimal( $totals->tax + $totals->shipping_tax ),
+ 'shipping' => woocommerce_format_decimal( $totals->shipping ),
+ 'discount' => is_null( $total_discount ) ? woocommerce_format_decimal( 0.00 ) : woocommerce_format_decimal( $total_discount ),
+ 'totals_grouped_by' => $this->report->chart_groupby,
+ 'totals' => $period_totals,
+ );
+
+ return array( 'sales' => apply_filters( 'woocommerce_api_report_response', $sales_data, 'sales', $fields, $this->report, $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 bool true if the request is valid and should be processed, false otherwise
+ */
+ 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/api/class-wc-api-resource.php b/includes/api/class-wc-api-resource.php
new file mode 100644
index 00000000000..770b62faded
--- /dev/null
+++ b/includes/api/class-wc-api-resource.php
@@ -0,0 +1,399 @@
+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, ARRAY_A );
+
+ // 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'] = absint( $request_args['page'] );
+
+ 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 );
+
+ } elseif ( is_a( $resource, 'WC_Product_Variation' ) ) {
+
+ // product variation meta
+ $meta = (array) get_post_meta( $resource->get_variation_id() );
+
+ } else {
+
+ // coupon/order/product meta
+ $meta = (array) get_post_meta( $resource->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 ( 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, ARRAY_A );
+
+ 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_post, $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/api/class-wc-api-server.php b/includes/api/class-wc-api-server.php
new file mode 100644
index 00000000000..c65ee4bb801
--- /dev/null
+++ b/includes/api/class-wc-api-server.php
@@ -0,0 +1,740 @@
+ 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
+ * @return WC_API_Server
+ */
+ 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'] );
+ }
+
+ // 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 );
+
+ // WP_Errors are handled in serve_request()
+ elseif ( ! is_wp_error( $user ) )
+ $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 $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( 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' ), 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' ), 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' ), 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 array $callback the endpoint callback
+ * @param array $provided the provided request parameters
+ * @return array
+ */
+ 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' ), $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(
+ 'name' => get_option( 'blogname' ),
+ 'description' => get_option( 'blogdescription' ),
+ 'URL' => get_option( 'siteurl' ),
+ 'routes' => array(),
+ 'meta' => array(
+ 'timezone' => $this->get_timezone(),
+ 'currency' => get_woocommerce_currency(),
+ 'money_format' => get_woocommerce_currency_symbol(),
+ 'tax_included' => ( 'yes' === get_option( 'woocommerce_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' ) ),
+ 'links' => array(
+ 'help' => 'http://docs.woothemes.com/document/woocommerce-rest-api/',
+ 'profile' => 'https://raw.github.com/rmccue/WP-API/master/docs/schema.json', // TODO: update this
+ ),
+ ),
+ );
+
+ // 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['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 assocative array
+ */
+ public function link_header( $rel, $link, $other = array() ) {
+ $header = 'Link: <' . $link . '>; rel="' . $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 * $query->max_num_pages;
+ $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 parmeter 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 ) );
+
+ // return full URL
+ return get_woocommerce_api_url( str_replace( '/wc-api/v1/', '', $request ) );
+ }
+
+ /**
+ * Retrieve the raw request entity (body)
+ *
+ * @since 2.1
+ * @return string
+ */
+ public function get_raw_data() {
+ 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;
+ }
+
+ /**
+ * 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
+ * @return string RFC3339 datetime
+ */
+ public function format_datetime( $timestamp, $convert_to_utc = false ) {
+
+ if ( $convert_to_utc ) {
+ $timezone = new DateTimeZone( woocommerce_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/api/class-wc-api-xml-handler.php b/includes/api/class-wc-api-xml-handler.php
new file mode 100644
index 00000000000..6847924065d
--- /dev/null
+++ b/includes/api/class-wc-api-xml-handler.php
@@ -0,0 +1,52 @@
+query_vars['wc-api'] = $_GET['wc-api'];
+ if ( ! empty( $_GET['wc-api-route'] ) )
+ $wp->query_vars['wc-api-route'] = $_GET['wc-api-route'];
+
+ // REST API request
+ if ( ! empty( $wp->query_vars['wc-api-route'] ) ) {
+
+ define( 'WC_API_REQUEST', true );
+
+ // load required files
+ $this->includes();
+
+ $this->server = new WC_API_Server( $wp->query_vars['wc-api-route'] );
+
+ // load API resource classes
+ $this->register_resources( $this->server );
+
+ // Fire off the request
+ $this->server->serve_request();
+
+ exit;
+ }
+
+ // legacy API requests
if ( ! empty( $wp->query_vars['wc-api'] ) ) {
+
// Buffer, we won't want any output here
ob_start();
@@ -76,4 +129,56 @@ class WC_API {
die('1');
}
}
-}
\ No newline at end of file
+
+
+ /**
+ * Include required files for REST API request
+ *
+ * @since 2.1
+ */
+ private function includes() {
+
+ // TODO: are all these required?
+ include_once( ABSPATH . WPINC . '/class-IXR.php' );
+ include_once( ABSPATH . WPINC . '/class-wp-xmlrpc-server.php' );
+
+ include_once( 'api/class-wc-api-server.php' );
+ include_once( 'api/interface-wc-api-handler.php' );
+ include_once( 'api/class-wc-api-json-handler.php' );
+ include_once( 'api/class-wc-api-xml-handler.php' );
+
+ include_once( 'api/class-wc-api-authentication.php' );
+ $this->authentication = new WC_API_Authentication();
+
+ include_once( 'api/class-wc-api-resource.php' );
+ include_once( 'api/class-wc-api-orders.php' );
+ include_once( 'api/class-wc-api-products.php' );
+ include_once( 'api/class-wc-api-coupons.php' );
+ include_once( 'api/class-wc-api-customers.php' );
+ include_once( 'api/class-wc-api-reports.php' );
+
+ // TODO: some action to allow actors to load additional resource types or handlers
+ }
+
+ /**
+ * Register API resources available
+ *
+ * @since 2.1
+ * @param object $server the REST server
+ */
+ public function register_resources( $server ) {
+
+ $api_classes = apply_filters( 'woocommerce_api_classes', array(
+ 'WC_API_Customers',
+ 'WC_API_Orders',
+ 'WC_API_Products',
+ 'WC_API_Coupons',
+ 'WC_API_Reports',
+ ) );
+
+ foreach ( $api_classes as $api_class ) {
+ $this->$api_class = new $api_class( $server );
+ }
+ }
+
+}
diff --git a/includes/class-wc-comments.php b/includes/class-wc-comments.php
index 11ef46685c7..cf06282588c 100644
--- a/includes/class-wc-comments.php
+++ b/includes/class-wc-comments.php
@@ -28,7 +28,7 @@ class WC_Comments {
add_action( 'edit_comment', array( $this, 'clear_transients' ) );
// Secure order notes
- add_filter( 'comments_clauses', array( $this, 'exclude_order_comments' ), 10, 1);
+ add_filter( 'comments_clauses', array( __CLASS__, 'exclude_order_comments' ), 10, 1 );
add_action( 'comment_feed_join', array( $this, 'exclude_order_comments_from_feed_join' ) );
add_action( 'comment_feed_where', array( $this, 'exclude_order_comments_from_feed_where' ) );
}
@@ -40,12 +40,12 @@ class WC_Comments {
* and are not filtered, however, the code current_user_can( 'read_post', $comment->comment_post_ID ) should keep them safe since only admin and
* shop managers can view orders anyway.
*
- * The frontend view order pages get around this filter by using remove_filter('comments_clauses', array( 'WC_Comments' ,'exclude_order_comments') );
+ * The frontend view order pages get around this filter by using remove_filter('comments_clauses', array( 'WC_Comments' ,'exclude_order_comments'), 10, 1 );
*
* @param array $clauses
* @return array
*/
- public function exclude_order_comments( $clauses ) {
+ public static function exclude_order_comments( $clauses ) {
global $wpdb, $typenow, $pagenow;
if ( is_admin() && $typenow == 'shop_order' && current_user_can( 'manage_woocommerce' ) )
@@ -149,4 +149,4 @@ class WC_Comments {
}
}
-new WC_Comments();
\ No newline at end of file
+new WC_Comments();
diff --git a/includes/class-wc-shortcodes.php b/includes/class-wc-shortcodes.php
index ea7c5424cac..e99ce45cc92 100644
--- a/includes/class-wc-shortcodes.php
+++ b/includes/class-wc-shortcodes.php
@@ -962,4 +962,4 @@ class WC_Shortcodes {
return ob_get_clean();
}
-}
\ No newline at end of file
+}
diff --git a/includes/wc-core-functions.php b/includes/wc-core-functions.php
index 2ec0aa2ef07..1c897c22880 100644
--- a/includes/wc-core-functions.php
+++ b/includes/wc-core-functions.php
@@ -325,7 +325,7 @@ function wc_print_js() {
/**
* Set a cookie - wrapper for setcookie using WP constants
- *
+ *
* @param string $name Name of the cookie being set
* @param string $value Value of the cookie
* @param integer $expire Expiry of the cookie
@@ -336,4 +336,21 @@ function wc_setcookie( $name, $value, $expire = 0 ) {
} elseif ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
trigger_error( "Cookie cannot be set - headers already sent", E_USER_NOTICE );
}
-}
\ No newline at end of file
+}
+
+/**
+ * Get the URL to the WooCommerce REST API
+ *
+ * @since 2.1
+ * @param string $path an endpoint to include in the URL
+ * @return string the URL
+ */
+function get_woocommerce_api_url( $path ) {
+
+ $url = get_home_url( null, 'wc-api/v' . WC_API::VERSION . '/', ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ) ? 'https' : 'http' );
+
+ if ( ! empty( $path ) && is_string( $path ) )
+ $url .= ltrim( $path, '/' );
+
+ return $url;
+}
diff --git a/includes/wc-formatting-functions.php b/includes/wc-formatting-functions.php
index f35e6c0638b..b9aef48dcee 100644
--- a/includes/wc-formatting-functions.php
+++ b/includes/wc-formatting-functions.php
@@ -380,6 +380,51 @@ function woocommerce_time_format() {
return apply_filters( 'woocommerce_time_format', get_option( 'time_format' ) );
}
+/**
+ * WooCommerce Timezone - helper to retrieve the timezone string for a site until
+ * a WP core method exists (see http://core.trac.wordpress.org/ticket/24730)
+ *
+ * Adapted from http://www.php.net/manual/en/function.timezone-name-from-abbr.php#89155
+ *
+ * @since 2.1
+ * @access public
+ * @return string a valid PHP timezone string for the site
+ */
+function woocommerce_timezone_string() {
+
+ // if site timezone string exists, return it
+ if ( $timezone = get_option( 'timezone_string' ) )
+ return $timezone;
+
+ // get UTC offset, if it isn't set then return UTC
+ if ( 0 === ( $utc_offset = get_option( 'gmt_offset', 0 ) ) )
+ return 'UTC';
+
+ // adjust UTC offset from hours to seconds
+ $utc_offset *= 3600;
+
+ // attempt to guess the timezone string from the UTC offset
+ $timezone = timezone_name_from_abbr( '', $utc_offset );
+
+ // last try, guess timezone string manually
+ if ( false === $timezone ) {
+
+ $is_dst = date( 'I' );
+
+ foreach ( timezone_abbreviations_list() as $abbr ) {
+ foreach ( $abbr as $city ) {
+
+ if ( $city['dst'] == $is_dst && $city['offset'] == $utc_offset ) {
+ return $city['timezone_id'];
+ }
+ }
+ }
+ }
+
+ // fallback to UTC
+ return 'UTC';
+}
+
if ( ! function_exists( 'woocommerce_rgb_from_hex' ) ) {
/**
@@ -534,4 +579,4 @@ function wc_format_postcode( $postcode, $country ) {
function wc_format_phone_number( $tel ) {
$tel = str_replace( '.', '-', $tel );
return $tel;
-}
\ No newline at end of file
+}