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 || 'shop_webhook' === $type ) $resource_name = str_replace( 'shop_', '', $type ); else $resource_name = $type; $id = absint( $id ); // validate ID if ( empty( $id ) ) return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) ); // only custom post types have per-post type/permission checks if ( 'customer' !== $type ) { $post = get_post( $id ); // for checking permissions, product variations are the same as the product post type $post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type; // validate post type if ( $type !== $post_type ) return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) ); // validate permissions switch ( $context ) { case 'read': if ( ! $this->is_readable( $post ) ) return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); break; case 'edit': if ( ! $this->is_editable( $post ) ) return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); break; case 'delete': if ( ! $this->is_deletable( $post ) ) return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); break; } } return $id; } /** * Add common request arguments to argument list before WP_Query is run * * @since 2.1 * @param array $base_args required arguments for the query (e.g. `post_type`, etc) * @param array $request_args arguments provided in the request * @return array */ protected function merge_query_args( $base_args, $request_args ) { $args = array(); // date if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) { $args['date_query'] = array(); // resources created after specified date if ( ! empty( $request_args['created_at_min'] ) ) { $args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true ); } // resources created before specified date if ( ! empty( $request_args['created_at_max'] ) ) { $args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true ); } // resources updated after specified date if ( ! empty( $request_args['updated_at_min'] ) ) { $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true ); } // resources updated before specified date if ( ! empty( $request_args['updated_at_max'] ) ) { $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true ); } } // search if ( ! empty( $request_args['q'] ) ) { $args['s'] = $request_args['q']; } // resources per response if ( ! empty( $request_args['limit'] ) ) { $args['posts_per_page'] = $request_args['limit']; } // resource offset if ( ! empty( $request_args['offset'] ) ) { $args['offset'] = $request_args['offset']; } // order (ASC or DESC, ASC by default) if ( ! empty( $request_args['order'] ) ) { $args['order'] = $request_args['order']; } // orderby if ( ! empty( $request_args['orderby'] ) ) { $args['orderby'] = $request_args['orderby']; // allow sorting by meta value if ( ! empty( $request_args['orderby_meta_key'] ) ) { $args['meta_key'] = $request_args['orderby_meta_key']; } } // allow post status change if ( ! empty( $request_args['post_status'] ) ) { $args['post_status'] = $request_args['post_status']; unset( $request_args['post_status'] ); } // resource page $args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1; return array_merge( $base_args, $args ); } /** * Add meta to resources when requested by the client. Meta is added as a top-level * `_meta` attribute (e.g. `order_meta`) as a list of key/value pairs * * @since 2.1 * @param array $data the resource data * @param object $resource the resource object (e.g WC_Order) * @return mixed */ public function maybe_add_meta( $data, $resource ) { if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) { // don't attempt to add meta more than once if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) ) return $data; // define the top-level property name for the meta switch ( get_class( $resource ) ) { case 'WC_Order': $meta_name = 'order_meta'; break; case 'WC_Coupon': $meta_name = 'coupon_meta'; break; case 'WP_User': $meta_name = 'customer_meta'; break; default: $meta_name = 'product_meta'; break; } if ( is_a( $resource, 'WP_User' ) ) { // customer meta $meta = (array) get_user_meta( $resource->ID ); } 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 ( ! is_array( $data ) || empty( $fields ) ) return $data; $fields = explode( ',', $fields ); $sub_fields = array(); // get sub fields foreach ( $fields as $field ) { if ( false !== strpos( $field, '.' ) ) { list( $name, $value ) = explode( '.', $field ); $sub_fields[ $name ] = $value; } } // iterate through top-level fields foreach ( $data as $data_field => $data_value ) { // if a field has sub-fields and the top-level field has sub-fields to filter if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) { // iterate through each sub-field foreach ( $data_value as $sub_field => $sub_field_value ) { // remove non-matching sub-fields if ( ! in_array( $sub_field, $sub_fields ) ) { unset( $data[ $data_field ][ $sub_field ] ); } } } else { // remove non-matching top-level fields if ( ! in_array( $data_field, $fields ) ) { unset( $data[ $data_field ] ); } } } return $data; } /** * Delete a given resource * * @since 2.1 * @param int $id the resource ID * @param string $type the resource post type, or `customer` * @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`) * @return array|WP_Error */ protected function delete( $id, $type, $force = false ) { if ( 'shop_order' === $type || 'shop_coupon' === $type ) $resource_name = str_replace( 'shop_', '', $type ); else $resource_name = $type; if ( 'customer' === $type ) { $result = wp_delete_user( $id ); if ( $result ) return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) ); else return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) ); } else { // delete order/coupon/product/webhook $result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id ); if ( ! $result ) return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) ); if ( $force ) { return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) ); } else { $this->server->send_status( '202' ); return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) ); } } } /** * Checks if the given post is readable by the current user * * @since 2.1 * @see WC_API_Resource::check_permission() * @param WP_Post|int $post * @return bool */ protected function is_readable( $post ) { return $this->check_permission( $post, 'read' ); } /** * Checks if the given post is editable by the current user * * @since 2.1 * @see WC_API_Resource::check_permission() * @param WP_Post|int $post * @return bool */ protected function is_editable( $post ) { return $this->check_permission( $post, 'edit' ); } /** * Checks if the given post is deletable by the current user * * @since 2.1 * @see WC_API_Resource::check_permission() * @param WP_Post|int $post * @return bool */ protected function is_deletable( $post ) { return $this->check_permission( $post, 'delete' ); } /** * Checks the permissions for the current user given a post and context * * @since 2.1 * @param WP_Post|int $post * @param string $context the type of permission to check, either `read`, `write`, or `delete` * @return bool true if the current user has the permissions to perform the context on the post */ private function check_permission( $post, $context ) { if ( ! is_a( $post, 'WP_Post' ) ) $post = get_post( $post ); if ( is_null( $post ) ) return false; $post_type = get_post_type_object( $post->post_type ); if ( 'read' === $context ) return current_user_can( $post_type->cap->read_private_posts, $post->ID ); elseif ( 'edit' === $context ) return current_user_can( $post_type->cap->edit_post, $post->ID ); elseif ( 'delete' === $context ) return current_user_can( $post_type->cap->delete_post, $post->ID ); else return false; } }