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 ); // TODO: redo this check, it's a bit janky $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(); // TODO: convert all dates from provided timezone into UTC // TODO: return all dates in provided timezone, else UTC // 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' => $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' => $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' => $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' => $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 if ( empty( $request_args['page'] ) ) $args['paged'] = 1; else $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'] ) { // TODO: implement } return $data; } /** * Restrict the fields included in the response if the request specified certain only certain fields should be returned * * @TODO this should also work with sub-fields, like billing_address.country * * @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 mixed */ public function filter_response_fields( $data, $resource, $fields ) { if ( empty( $fields ) ) return $data; $fields = explode( ',', $fields ); foreach ( $data as $data_field => $data_value ) { 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; } }