diff --git a/api/class-wc-rest-dev-data-continents-controller.php b/api/class-wc-rest-dev-data-continents-controller.php new file mode 100644 index 0000000..ffe89f3 --- /dev/null +++ b/api/class-wc-rest-dev-data-continents-controller.php @@ -0,0 +1,280 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\w-]+)', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => array( + 'continent' => array( + 'description' => __( '2 character continent code.', 'woocommerce' ), + 'type' => 'string', + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Return the list of countries and states for a given continent. + * + * @since 3.1.0 + * @param string $continent_code + * @param WP_REST_Request $request + * @return array|mixed Response data, ready for insertion into collection data. + */ + public function get_continent( $continent_code = false, $request ) { + $continents = WC()->countries->get_continents(); + $countries = WC()->countries->get_countries(); + $states = WC()->countries->get_states(); + $data = array(); + + if ( ! array_key_exists( $continent_code, $continents ) ) { + return false; + } + + $continent_list = $continents[ $continent_code ]; + + $continent = array( + 'code' => $continent_code, + 'name' => $continent_list['name'], + ); + + $local_countries = array(); + foreach ( $continent_list['countries'] as $country_code ) { + if ( isset( $countries[ $country_code ] ) ) { + $country = array( + 'code' => $country_code, + 'name' => $countries[ $country_code ], + ); + + $local_states = array(); + if ( isset( $states[ $country_code ] ) ) { + foreach ( $states[ $country_code ] as $state_code => $state_name ) { + $local_states[] = array( + 'code' => $state_code, + 'name' => $state_name, + ); + } + } + $country['states'] = $local_states; + $local_countries[] = $country; + } + } + + $continent['countries'] = $local_countries; + return $continent; + } + + /** + * Return the list of states for all continents. + * + * @since 3.1.0 + * @param WP_REST_Request $request + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $continents = WC()->countries->get_continents(); + $data = array(); + + foreach ( array_keys( $continents ) as $continent_code ) { + $continent = $this->get_continent( $continent_code, $request ); + $response = $this->prepare_item_for_response( $continent, $request ); + $data[] = $this->prepare_response_for_collection( $response ); + } + + return rest_ensure_response( $data ); + } + + /** + * Return the list of locations for a given continent. + * + * @since 3.1.0 + * @param WP_REST_Request $request + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $data = $this->get_continent( strtoupper( $request['location'] ), $request ); + if ( empty( $data ) ) { + return new WP_Error( 'woocommerce_rest_data_invalid_location', __( 'There are no locations matching these parameters.', 'woocommerce' ), array( 'status' => 404 ) ); + } + return $this->prepare_item_for_response( $data, $request ); + } + + /** + * Prepare the data object for response. + * + * @since 3.1.0 + * @param object $item Data object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $item, $request ) { + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, 'view' ); + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item ) ); + + /** + * Filter the location list returned from the API. + * + * Allows modification of the loction data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param array $item The original list of continent(s), countries, and states. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_data_continent', $response, $item, $request ); + } + + /** + * Prepare links for the request. + * + * @param object $item Data object. + * @return array Links for the given continent. + */ + protected function prepare_links( $item ) { + $continent_code = strtolower( $item['code'] ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $continent_code ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + return $links; + } + + /** + * Get the location schema, conforming to JSON Schema. + * + * @since 3.1.0 + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'data_continents', + 'type' => 'object', + 'properties' => array( + 'code' => array( + 'type' => 'string', + 'description' => __( '2 character continent code.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'type' => 'string', + 'description' => __( 'Full name of continent.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'countries' => array( + 'type' => 'array', + 'description' => __( 'List of countries on this continent.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'code' => array( + 'type' => 'string', + 'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'type' => 'string', + 'description' => __( 'Full name of country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'states' => array( + 'type' => 'array', + 'description' => __( 'List of states in this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'code' => array( + 'type' => 'string', + 'description' => __( 'State code.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'type' => 'string', + 'description' => __( 'Full name of state.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/api/class-wc-rest-dev-data-controller.php b/api/class-wc-rest-dev-data-controller.php new file mode 100644 index 0000000..025ef3c --- /dev/null +++ b/api/class-wc-rest-dev-data-controller.php @@ -0,0 +1,168 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read site data. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Return the list of data resources. + * + * @since 3.1.0 + * @param WP_REST_Request $request + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $data = array(); + $resources = array( + array( + 'slug' => 'continents', + 'description' => __( 'List of supported continents, countries, and states.', 'woocommerce' ), + ), + array( + 'slug' => 'countries', + 'description' => __( 'List of supported states in a given country.', 'woocommerce' ), + ), + ); + + foreach ( $resources as $resource ) { + $item = $this->prepare_item_for_response( (object) $resource, $request ); + $data[] = $this->prepare_response_for_collection( $item ); + } + + return rest_ensure_response( $data ); + } + + /** + * Prepare a data resource object for serialization. + * + * @param stdClass $report Report data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $resource, $request ) { + $data = array( + 'slug' => $resource->slug, + 'description' => $resource->description, + ); + + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, 'view' ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $resource ) ); + + return $response; + } + + /** + * Prepare links for the request. + * + * @param object $item Data object. + * @return array Links for the given country. + */ + protected function prepare_links( $item ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $item->slug ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Get the data index schema, conforming to JSON Schema. + * + * @since 3.1.0 + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'data_index', + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'description' => __( 'Data resource ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'Data resource description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/api/class-wc-rest-dev-data-countries-controller.php b/api/class-wc-rest-dev-data-countries-controller.php new file mode 100644 index 0000000..006e278 --- /dev/null +++ b/api/class-wc-rest-dev-data-countries-controller.php @@ -0,0 +1,240 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\w-]+)', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => array( + 'location' => array( + 'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ), + 'type' => 'string', + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Get a list of countries and states. + * + * @param string $country_code + * @param WP_REST_Request $request + * @return array|mixed Response data, ready for insertion into collection data. + */ + public function get_country( $country_code = false, $request ) { + $countries = WC()->countries->get_countries(); + $states = WC()->countries->get_states(); + $data = array(); + + if ( ! array_key_exists( $country_code, $countries ) ) { + return false; + } + + $country = array( + 'code' => $country_code, + 'name' => $countries[ $country_code ], + ); + + $local_states = array(); + if ( isset( $states[ $country_code ] ) ) { + foreach ( $states[ $country_code ] as $state_code => $state_name ) { + $local_states[] = array( + 'code' => $state_code, + 'name' => $state_name, + ); + } + } + $country['states'] = $local_states; + return $country; + } + + /** + * Return the list of states for all countries. + * + * @since 3.1.0 + * @param WP_REST_Request $request + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $countries = WC()->countries->get_countries(); + $data = array(); + + foreach ( array_keys( $countries ) as $country_code ) { + $country = $this->get_country( $country_code, $request ); + $response = $this->prepare_item_for_response( $country, $request ); + $data[] = $this->prepare_response_for_collection( $response ); + } + + return rest_ensure_response( $data ); + } + + /** + * Return the list of states for a given country. + * + * @since 3.1.0 + * @param WP_REST_Request $request + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $data = $this->get_country( strtoupper( $request['location'] ), $request ); + if ( empty( $data ) ) { + return new WP_Error( 'woocommerce_rest_data_invalid_location', __( 'There are no locations matching these parameters.', 'woocommerce' ), array( 'status' => 404 ) ); + } + return $this->prepare_item_for_response( $data, $request ); + } + + /** + * Prepare the data object for response. + * + * @since 3.1.0 + * @param object $item Data object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $item, $request ) { + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, 'view' ); + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item ) ); + + /** + * Filter the states list for a country returned from the API. + * + * Allows modification of the loction data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param array $data The original country's states list. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_data_country', $response, $item, $request ); + } + + /** + * Prepare links for the request. + * + * @param object $item Data object. + * @return array Links for the given country. + */ + protected function prepare_links( $item ) { + $country_code = strtolower( $item['code'] ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $country_code ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + + /** + * Get the location schema, conforming to JSON Schema. + * + * @since 3.1.0 + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'data_countries', + 'type' => 'object', + 'properties' => array( + 'code' => array( + 'type' => 'string', + 'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'type' => 'string', + 'description' => __( 'Full name of country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'states' => array( + 'type' => 'array', + 'description' => __( 'List of states in this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'code' => array( + 'type' => 'string', + 'description' => __( 'State code.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'type' => 'string', + 'description' => __( 'Full name of state.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/tests/unit-tests/data.php b/tests/unit-tests/data.php new file mode 100644 index 0000000..ce5bb21 --- /dev/null +++ b/tests/unit-tests/data.php @@ -0,0 +1,152 @@ +user = $this->factory->user->create( array( + 'role' => 'administrator', + ) ); + } + + /** + * Test route registration. + * + * @since 3.1.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v3/data', $routes ); + $this->assertArrayHasKey( '/wc/v3/data/continents', $routes ); + $this->assertArrayHasKey( '/wc/v3/data/countries', $routes ); + } + + /** + * Test getting the data index. + * @since 3.1.0 + */ + public function test_get_index() { + wp_set_current_user( $this->user ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/data' ) ); + $index = $response->get_data(); + $this->assertEquals( 200, $response->get_status() ); + $this->assertCount( 2, $index ); + $this->assertEquals( 'continents', $index[0]['slug'] ); + $this->assertEquals( 'countries', $index[1]['slug'] ); + } + + /** + * Test getting locations. + * @since 3.1.0 + */ + public function test_get_locations() { + wp_set_current_user( $this->user ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/data/continents' ) ); + $locations = $response->get_data(); + $this->assertEquals( 200, $response->get_status() ); + $this->assertTrue( is_array( $locations ) ); + $this->assertGreaterThan( 1, count( $locations ) ); + $this->assertNotEmpty( $locations[0]['code'] ); + $this->assertNotEmpty( $locations[0]['name'] ); + $this->assertNotEmpty( $locations[0]['countries'] ); + $this->assertNotEmpty( $locations[0]['_links'] ); + } + + /** + * Test getting locations restricted to one continent. + * @since 3.1.0 + */ + public function test_get_locations_from_continent() { + wp_set_current_user( $this->user ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/data/continents/na' ) ); + $locations = $response->get_data(); + $this->assertEquals( 200, $response->get_status() ); + $this->assertTrue( is_array( $locations ) ); + $this->assertEquals( 'NA', $locations['code'] ); + $this->assertNotEmpty( $locations['name'] ); + $this->assertNotEmpty( $locations['countries'] ); + $links = $response->get_links(); + $this->assertCount( 2, $links ); + } + + /** + * Test getting locations with no country specified + * @since 3.1.0 + */ + public function test_get_locations_all_countries() { + wp_set_current_user( $this->user ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/data/countries' ) ); + $locations = $response->get_data(); + $this->assertEquals( 200, $response->get_status() ); + $this->assertGreaterThan( 1, count( $locations ) ); + $this->assertNotEmpty( $locations[0]['code'] ); + $this->assertNotEmpty( $locations[0]['name'] ); + $this->assertArrayHasKey( 'states', $locations[0] ); + $this->assertNotEmpty( $locations[0]['_links'] ); + } + + /** + * Test getting locations restricted to one country. + * @since 3.1.0 + */ + public function test_get_locations_from_country() { + wp_set_current_user( $this->user ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/data/countries/us' ) ); + $locations = $response->get_data(); + $this->assertEquals( 200, $response->get_status() ); + $this->assertTrue( is_array( $locations ) ); + $this->assertEquals( 'US', $locations['code'] ); + $this->assertNotEmpty( $locations['name'] ); + $this->assertCount( 54, $locations['states'] ); + $links = $response->get_links(); + $this->assertCount( 2, $links ); + } + + /** + * Test getting locations from an invalid code. + * @since 3.1.0 + */ + public function test_get_locations_from_invalid_continent() { + wp_set_current_user( $this->user ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/data/continents/xx' ) ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test getting locations from an invalid code. + * @since 3.1.0 + */ + public function test_get_locations_from_invalid_country() { + wp_set_current_user( $this->user ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/data/countries/xx' ) ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test getting locations without permissions. + * @since 3.1.0 + */ + public function test_get_continents_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/data/continents' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test getting locations without permissions. + * @since 3.1.0 + */ + public function test_get_countries_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/data/countries' ) ); + $this->assertEquals( 401, $response->get_status() ); + } +} diff --git a/wc-api-dev.php b/wc-api-dev.php index bf15de8..1fce248 100644 --- a/wc-api-dev.php +++ b/wc-api-dev.php @@ -76,6 +76,9 @@ public function includes() { include_once( dirname( __FILE__ ) . '/api/class-wc-rest-dev-coupons-controller.php' ); include_once( dirname( __FILE__ ) . '/api/class-wc-rest-dev-customer-downloads-controller.php' ); include_once( dirname( __FILE__ ) . '/api/class-wc-rest-dev-customers-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-dev-data-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-dev-data-continents-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-dev-data-countries-controller.php' ); include_once( dirname( __FILE__ ) . '/api/class-wc-rest-dev-orders-controller.php' ); include_once( dirname( __FILE__ ) . '/api/class-wc-rest-dev-order-notes-controller.php' ); include_once( dirname( __FILE__ ) . '/api/class-wc-rest-dev-order-refunds-controller.php' ); @@ -115,6 +118,9 @@ public function register_routes() { 'WC_REST_Dev_Coupons_Controller', 'WC_REST_Dev_Customer_Downloads_Controller', 'WC_REST_Dev_Customers_Controller', + 'WC_REST_Dev_Data_Controller', + 'WC_REST_Dev_Data_Continents_Controller', + 'WC_REST_Dev_Data_Countries_Controller', 'WC_REST_Dev_Order_Notes_Controller', 'WC_REST_Dev_Order_Refunds_Controller', 'WC_REST_Dev_Orders_Controller',